【经验篇】Spring Data JPA开启批量更新时乐观锁失效问题

乐观锁机制

什么是乐观锁?

乐观锁的基本思想是,认为在大多数情况下,数据访问不会导致冲突。因此,乐观锁允许多个事务同时读取和修改相同的数据,而不进行显式的锁定。在提交事务之前,会检查是否有其他事务对该数据进行了修改。如果没有冲突,则提交成功;如果发现冲突,就需要回滚并重新尝试。

乐观锁通常使用版本号时间戳来实现。每个数据项都会包含一个表示当前版本的标识符。在读取数据时,会将版本标识符保存下来。在提交更新时,会检查数据的当前版本是否与保存的版本匹配。如果匹配,则更新成功;否则,表示数据已被其他事务修改,需要处理冲突。

乐观锁适用于读操作频率较高、写操作冲突较少的场景。它减少了锁的使用,提高了并发性能,但需要处理冲突和重试的情况。

乐观锁是一种广义的思想,不是某一框架或语言特有的。

乐观锁的优缺点

优点

  • 增强吞吐量:由于在事务持续时间的大部分时间内没有持有锁,因此等待时间最少,吞吐量也是最⼤的。
  • 最小化死锁:死锁是一种事务无限期地等待其他人锁定的资源的情况,这种情况的可能性要小得多,因为数据不会长时间锁定。
  • 更好的可扩展性:随着分布式系统和微服务架构的兴起,乐观锁在确保系统能够有效扩展而无需管理复杂锁机制的开销方面发挥着关键作用。

缺点

  • 冲突管理开销:在冲突频繁的场景中,管理和解决冲突可能会占用大量资源。
  • 复杂性:实现乐观锁需要经过深思熟虑的设计,特别是在处理失败的事务时。
  • 过时数据的可能性:由于数据在读取时未锁定,因此事务可能会使用过时或过时的数据,如果管理不正确,可能会导致逻辑错误或不一致。

JPA-乐观锁

此小节摘自《Spring Data JPA入门、实战与进阶》中的一个章节。

概述

JPA(Java Persistence API)协议对乐观锁的操作做了规定:通过指定@Version字段对数据增加版本号控制,进⽽在更新的时候判断版本号是否有变化。如果版本没有变化则更新成功;如果版本有变化,就会更新失败并抛出“OptimisticLockException”异常。我们⽤ SQL 表示⼀下乐观锁的做法,代码如下:

SELECT uid, name, version FROM user WHERE id = 1;
UPDATE user SET name = 'jack', version = version + 1 WHERE id = 1 AND version = 1;

假设本次查询的version=1,在更新操作时,只要version与上一个版本相同,就会更新成功,并且不会出现互相覆盖的问题,保证了数据的原⼦性。

实现方法

JPA 协议规定,想要实现乐观锁,可以通过@Version注解标注在某个字段上⾯,而此字段需要是可以持久化到DB的字段,并且只⽀持如下四种类型:

  • intInteger
  • shortShort
  • longLong
  • java.sql.Timestamp

我比较推荐使用Integer类型的字段,语义比较清晰、简单。

@Version的作用

@Version注解用于启用实体上的乐观锁,确保数据库中的数据更新不会出现并发修改问题。当实体中的某个字段标记为@Version时,JPA 将使用该字段来跟踪更改并确保一次只有一个事务可以更新特定行。

注意:Spring Data JPA ⾥⾯有两个@Version注解,请使⽤@javax.persistence.Version,⽽不是@org.springframework.data.annotation.Version

它是如何工作的?

每个用注解标记的实体都@Version将由 JPA 跟踪其版本。这是基本机制:

  • 初始化:当实体第一次被持久化(保存到数据库)时,版本字段(通常是整数或时间戳)被设置为其初始值,通常为零。
  • 读取:稍后获取实体时,JPA 会从数据库中检索当前版本。
  • 更新:在尝试更新或删除实体时,JPA 会根据实体的版本检查数据库中的当前版本。如果版本匹配,则操作继续,并且数据库中的版本增加(用于更新)。
  • 冲突:如果版本不匹配,则表明另一个事务同时更新了实体,导致 JPA 抛出OptimisticLockException

项目示例

引入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 驱动 -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 数据库连接池 -->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-dbcp2</artifactId>
</dependency>

项目配置

spring:datasource:url:  jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=falseusername: rootpassword: rootjpa:database: mysqldatabase-platform: org.hibernate.dialect.MySQL5InnoDBDialectshow-sql: truehibernate:ddl-auto: update # 一般使用update# create: 每次运行程序时,都会重新创建表,故而数据会丢失# create-drop: 每次运行程序时会先创建表结构,然后待程序结束时清空表# upadte: 每次运行程序,没有表时会创建表,如果对象发生改变会更新表结构,原有数据不会清空,只会更新(推荐使用)# validate: 运行程序会校验数据与数据库的字段类型是否相同,字段不同会报错# none: 禁用DDL处理open-in-view: falseproperties:hibernate:jdbc: # 开启批量更新/写入batch_size: 50batch_versioned_data: trueorder_inserts: trueorder_updates: true

实体添加@Version

User实体增加字段version,并添加注解@Version。当然,数据库也要加上version字段。

@Entity
@Table(name = "TEST_USER")
public class User {// ......@Versionprivate Integer version;// ......
}

创建UserInfoRepository

创建UserInfoRepository,⽅便进⾏DB操作

public interface UserInfoRepository extends JpaRepository<User, Long> {}

创建 UserInfoService

创建 UserInfoService,⽤来模拟Service的复杂业务逻辑。

public interface UserService {/*** 根据 UserId 产⽣的⼀些业务计算逻辑*/User calculate(Long userId);}@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserRepository userRepository;@Override@Transactionalpublic User calculate(Long userId) {User user = repository.getById(userId);// 模拟复杂的业务计算逻辑耗时操作;try {TimeUnit.SECONDS.sleep(2L);} catch (InterruptedException ignored) {}user.setAge(user.getAge() + 1);return userRepository.saveAndFlush(user);}
}

其中,我们通过 @Transactional 开启事务,并且在查询⽅法后⾯模拟复杂业务逻辑,⽤来呈现多线程的并发问题。

测试方法

@ExtendWith(SpringExtension.class)
@DataJpaTest
@ComponentScan(basePackageClasses = UserServiceImpl.class)
class UserServiceTest {@Autowiredprivate UserService userService;@Autowiredprivate UserRepository userRepository;@Testvoid testVersion() {// 加⼀条数据User user1 = userRepository.save(User.builder().age(20).name("zzn").build());// 验证⼀下数据库⾥⾯的值Assertions.assertEquals(0, user1.getVersion());Assertions.assertEquals(20, user1.getAge());userService.calculate(user1.getId());// 验证⼀下更新成功的值User user2 = userRepository.getById(user1.getId());Assertions.assertEquals(1, user2.getVersion());Assertions.assertEquals(21, user2.getAge());}@SneakyThrows@Test@Rollback(false)@Transactional(propagation = Propagation.NEVER)void testVersionException() {// 加⼀条数据userRepository.save(User.builder().age(20).name("zzn").build());// 模拟多线程执⾏两次new Thread(() -> userService.calculate(1L)).start();TimeUnit.SECONDS.sleep(1L);// 如果两个线程同时执⾏会发⽣乐观锁异常;Exception exception = Assertions.assertThrows(ObjectOptimisticLockingFailureException.class,() -> userService.calculate(1L));log.info("error info:", exception);}
}

从上⾯的测试得到的结果中,我们执⾏testVersion(),会发现在 save 的时候, Version会⾃动 +1,第⼀次初始化为 0;update 的时候也会附带 Version 条件,我们通过下图的 SQL,也可以看到 Version 的变化。
在这里插入图片描述
⽽当⾯我们调⽤testVersionException()测试⽅法的时候,利⽤多线程模拟两个并发情况,会发现两个线程同时取到了历史数据,并在稍后都对历史数据进⾏了更新。

由此你会发现,第⼆次测试的结果是乐观锁异常,更新不成功。

通过⽇志⼜会发现,两个SQL同时更新的时候,Version是⼀样的,是它导致了乐观锁异常。

注意:乐观锁异常不仅仅是同⼀个⽅法多线程才会出现的问题,我们只是为了⽅便测试⽽采⽤同⼀个⽅法;不同的⽅法、不同的项⽬,都有可能导致乐观锁异常。乐观锁的本质是 SQL 层⾯发⽣的,和使⽤的框架、技术没有关系。

问题描述

一句废话:正常情况下,一切正常!

运行环境

Java1.8.0
SpringBoot2.3.12.RELEASE
Spring Data JPA2.3.9.RELEASE
Hibernate5.4.32.Final
Database Driver:ojdbc6 11.2.0.3
Database PlatformOracle 10g

问题现象

上述代码示例运行在MySQL数据库上,一切正常,但是切换到Oracle数据库时,不开启批量更新模式时,也符合预期,但是开启批量更新模式时,不符合预期:并发更新同一实体时,未抛出ObjectOptimisticLockingFailureException异常。

数据库类型开启批量不开启批量
Oracle不生效生效
MySQL生效生效

异常信息

批量模式下,乐观锁异常栈:

Caused by: org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: update test_user set update_time=?, version=?, remark=? where user_id=? and version=?at org.hibernate.jdbc.Expectations$BasicExpectation.checkBatched(Expectations.java:67) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.jdbc.Expectations$BasicExpectation.verifyOutcome(Expectations.java:54) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.engine.jdbc.batch.internal.BatchingBatch.checkRowCounts(BatchingBatch.java:151) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.engine.jdbc.batch.internal.BatchingBatch.performExecution(BatchingBatch.java:126) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.engine.jdbc.batch.internal.BatchingBatch.doExecuteBatch(BatchingBatch.java:106) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.engine.jdbc.batch.internal.AbstractBatchImpl.execute(AbstractBatchImpl.java:148) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.executeBatch(JdbcCoordinatorImpl.java:198) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:633) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at java.util.LinkedHashMap.forEach(LinkedHashMap.java:676) ~[?:1.8.0_73]at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:344) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:99) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1362) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1349) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_73]at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_73]at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_73]at java.lang.reflect.Method.invoke(Method.java:497) ~[?:1.8.0_73]at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:314) ~[spring-orm-5.2.22.RELEASE.jar:5.2.22.RELEASE]at com.sun.proxy.$Proxy156.flush(Unknown Source) ~[?:?]at org.springframework.data.jpa.repository.support.SimpleJpaRepository.flush(SimpleJpaRepository.java:601) ~[spring-data-jpa-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAndFlush(SimpleJpaRepository.java:570) ~[spring-data-jpa-2.3.9.RELEASE.jar:2.3.9.RELEASE]at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_73]at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_73]at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_73]at java.lang.reflect.Method.invoke(Method.java:497) ~[?:1.8.0_73]at org.springframework.data.repository.core.support.ImplementationInvocationMetadata.invoke(ImplementationInvocationMetadata.java:72) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:382) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:205) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:550) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.22.RELEASE.jar:5.2.22.RELEASE]at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:155) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:130) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.22.RELEASE.jar:5.2.22.RELEASE]at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.22.RELEASE.jar:5.2.22.RELEASE]at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:367) ~[spring-tx-5.2.22.RELEASE.jar:5.2.22.RELEASE]at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) ~[spring-tx-5.2.22.RELEASE.jar:5.2.22.RELEASE]at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.22.RELEASE.jar:5.2.22.RELEASE]at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139) ~[spring-tx-5.2.22.RELEASE.jar:5.2.22.RELEASE]... 109 more

非批量模式下,乐观锁异常栈:

Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.esunny.option.domain.user.User#990]at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:2649) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3492) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3355) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3769) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:201) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at java.util.LinkedHashMap.forEach(LinkedHashMap.java:676) ~[?:1.8.0_73]at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:344) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:99) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1362) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1349) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_73]at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_73]at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_73]at java.lang.reflect.Method.invoke(Method.java:497) ~[?:1.8.0_73]at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:314) ~[spring-orm-5.2.22.RELEASE.jar:5.2.22.RELEASE]at com.sun.proxy.$Proxy156.flush(Unknown Source) ~[?:?]at org.springframework.data.jpa.repository.support.SimpleJpaRepository.flush(SimpleJpaRepository.java:601) ~[spring-data-jpa-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAndFlush(SimpleJpaRepository.java:570) ~[spring-data-jpa-2.3.9.RELEASE.jar:2.3.9.RELEASE]at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_73]at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_73]at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_73]at java.lang.reflect.Method.invoke(Method.java:497) ~[?:1.8.0_73]at org.springframework.data.repository.core.support.ImplementationInvocationMetadata.invoke(ImplementationInvocationMetadata.java:72) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:382) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:205) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:550) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.22.RELEASE.jar:5.2.22.RELEASE]at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:155) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:130) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.22.RELEASE.jar:5.2.22.RELEASE]at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80) ~[spring-data-commons-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.22.RELEASE.jar:5.2.22.RELEASE]at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:367) ~[spring-tx-5.2.22.RELEASE.jar:5.2.22.RELEASE]at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) ~[spring-tx-5.2.22.RELEASE.jar:5.2.22.RELEASE]at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.22.RELEASE.jar:5.2.22.RELEASE]at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139) ~[spring-tx-5.2.22.RELEASE.jar:5.2.22.RELEASE]... 109 more

代码分析

从以上两种模式下的异常栈分析代码路径:

org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAndFlushorg.springframework.data.jpa.repository.support.SimpleJpaRepository.flushorg.hibernate.internal.SessionImpl.flushorg.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListenerorg.hibernate.event.internal.DefaultFlushEventListener.onFlushorg.hibernate.event.internal.AbstractFlushingEventListener.performExecutionsorg.hibernate.engine.spi.ActionQueue.executeActions

ActionQueue.executeActions逻辑如下:
hibernate-core-5.4.32.Final-sources.jar!/org/hibernate/engine/spi/ActionQueue.java

    /*** Perform all currently queued actions.* * @throws HibernateException error executing queued actions.*/public void executeActions() throws HibernateException {if ( hasUnresolvedEntityInsertActions() ) {throw new IllegalStateException( "About to execute actions, but there are unresolved entity insert actions." );}for ( ListProvider listProvider : EXECUTABLE_LISTS_MAP.values() ) {ExecutableList<?> l = listProvider.get( this );if ( l != null && !l.isEmpty() ) {executeActions( l );}}}/*** Perform {@link org.hibernate.action.spi.Executable#execute()} on each element of the list* * @param list The list of Executable elements to be performed** @throws HibernateException*/private <E extends Executable & Comparable<?> & Serializable> void executeActions(ExecutableList<E> list) throws HibernateException {// todo : consider ways to improve the double iteration of Executables here://        1) we explicitly iterate list here to perform Executable#execute()//        2) ExecutableList#getQuerySpaces also iterates the Executables to collect query spaces.try {for ( E e : list ) {try {e.execute();}finally {if( e.getBeforeTransactionCompletionProcess() != null ) {if( beforeTransactionProcesses == null ) {beforeTransactionProcesses = new BeforeTransactionCompletionProcessQueue( session );}beforeTransactionProcesses.register(e.getBeforeTransactionCompletionProcess());}if( e.getAfterTransactionCompletionProcess() != null ) {if( afterTransactionProcesses == null ) {afterTransactionProcesses = new AfterTransactionCompletionProcessQueue( session );}afterTransactionProcesses.register(e.getAfterTransactionCompletionProcess());}}}}finally {if ( session.getFactory().getSessionFactoryOptions().isQueryCacheEnabled() ) {// Strictly speaking, only a subset of the list may have been processed if a RuntimeException occurs.// We still invalidate all spaces. I don't see this as a big deal - after all, RuntimeExceptions are// unexpected.Set<Serializable> propertySpaces = list.getQuerySpaces();invalidateSpaces( propertySpaces.toArray( new Serializable[propertySpaces.size()] ) );}}list.clear();session.getJdbcCoordinator().executeBatch();}

这里在for循环里头调用了e.execute(),同时在循环之后,finally之后调用了session.getJdbcCoordinator().executeBatch()
其中,EXECUTABLE_LISTS_MAP中的Executable包括:EntityInsertActionEntityUpdateActionEntityDeleteAction等。

Executable.execute逻辑如下:
hibernate-core-5.4.32.Final-sources.jar!/org/hibernate/action/internal/EntityUpdateAction.java

@Overridepublic void execute() throws HibernateException {final Serializable id = getId();final EntityPersister persister = getPersister();final SharedSessionContractImplementor session = getSession();final Object instance = getInstance();final boolean veto = preUpdate();final SessionFactoryImplementor factory = session.getFactory();Object previousVersion = this.previousVersion;if ( persister.isVersionPropertyGenerated() ) {// we need to grab the version value from the entity, otherwise// we have issues with generated-version entities that may have// multiple actions queued during the same flushpreviousVersion = persister.getVersion( instance );}final Object ck;if ( persister.canWriteToCache() ) {final EntityDataAccess cache = persister.getCacheAccessStrategy();ck = cache.generateCacheKey(id,persister,factory,session.getTenantIdentifier());lock = cache.lockItem( session, ck, previousVersion );}else {ck = null;}if ( !veto ) {persister.update(id,state,dirtyFields,hasDirtyCollection,previousState,previousVersion,instance,rowId,session);}final EntityEntry entry = session.getPersistenceContextInternal().getEntry( instance );if ( entry == null ) {throw new AssertionFailure( "possible nonthreadsafe access to session" );}if ( entry.getStatus()==Status.MANAGED || persister.isVersionPropertyGenerated() ) {// get the updated snapshot of the entity state by cloning current state;// it is safe to copy in place, since by this time no-one else (should have)// has a reference  to the arrayTypeHelper.deepCopy(state,persister.getPropertyTypes(),persister.getPropertyCheckability(),state,session);if ( persister.hasUpdateGeneratedProperties() ) {// this entity defines property generation, so process those generated// values...persister.processUpdateGeneratedProperties( id, instance, state, session );if ( persister.isVersionPropertyGenerated() ) {nextVersion = Versioning.getVersion( state, persister );}}// have the entity entry doAfterTransactionCompletion post-update processing, passing it the// update state and the new version (if one).entry.postUpdate( instance, state, nextVersion );}final StatisticsImplementor statistics = factory.getStatistics();if ( persister.canWriteToCache() ) {if ( persister.isCacheInvalidationRequired() || entry.getStatus()!= Status.MANAGED ) {persister.getCacheAccessStrategy().remove( session, ck);}else if ( session.getCacheMode().isPutEnabled() ) {//TODO: inefficient if that cache is just going to ignore the updated state!final CacheEntry ce = persister.buildCacheEntry( instance,state, nextVersion, getSession() );cacheEntry = persister.getCacheEntryStructure().structure( ce );final boolean put = cacheUpdate( persister, previousVersion, ck );if ( put && statistics.isStatisticsEnabled() ) {statistics.entityCachePut(StatsHelper.INSTANCE.getRootEntityRole( persister ),getPersister().getCacheAccessStrategy().getRegion().getName());}}}session.getPersistenceContextInternal().getNaturalIdHelper().manageSharedNaturalIdCrossReference(persister,id,state,previousNaturalIdValues,CachedNaturalIdValueSource.UPDATE);postUpdate();if ( statistics.isStatisticsEnabled() && !veto ) {statistics.updateEntity( getPersister().getEntityName() );}}

调用了persister的update方法。

AbstractEntityPersister.update
hibernate-core-5.4.32.Final-sources.jar!/org/hibernate/persister/entity/AbstractEntityPersister.java

public boolean update(final Serializable id,final Object[] fields,final Object[] oldFields,final Object rowId,final boolean[] includeProperty,final int j,final Object oldVersion,final Object object,final String sql,final SharedSessionContractImplementor session) throws HibernateException {final Expectation expectation = Expectations.appropriateExpectation( updateResultCheckStyles[j] );final int jdbcBatchSizeToUse = session.getConfiguredJdbcBatchSize();// IMPLEMENTATION NOTE: If Session#saveOrUpdate or #update is used to update an entity, then//                      Hibernate does not have a database snapshot of the existing entity.//                      As a result, oldFields will be null.// Don't use a batch if oldFields == null and the jth table is optional (isNullableTable( j ),// because there is no way to know that there is actually a row to update. If the update// was batched in this case, the batch update would fail and there is no way to fallback to// an insert.final boolean useBatch =expectation.canBeBatched() &&isBatchable() &&jdbcBatchSizeToUse > 1 &&( oldFields != null || !isNullableTable( j ) );if ( useBatch && updateBatchKey == null ) {updateBatchKey = new BasicBatchKey(getEntityName() + "#UPDATE",expectation);}final boolean callable = isUpdateCallable( j );final boolean useVersion = j == 0 && isVersioned();if ( LOG.isTraceEnabled() ) {LOG.tracev( "Updating entity: {0}", MessageHelper.infoString( this, id, getFactory() ) );if ( useVersion ) {LOG.tracev( "Existing version: {0} -> New version:{1}", oldVersion, fields[getVersionProperty()] );}}try {int index = 1; // starting indexfinal PreparedStatement update;if ( useBatch ) {update = session.getJdbcCoordinator().getBatch( updateBatchKey ).getBatchStatement( sql, callable );}else {update = session.getJdbcCoordinator().getStatementPreparer().prepareStatement( sql, callable );}try {index += expectation.prepare( update );//Now write the values of fields onto the prepared statementindex = dehydrate(id,fields,rowId,includeProperty,propertyColumnUpdateable,j,update,session,index,true);// Write any appropriate versioning conditional parametersif ( useVersion && entityMetamodel.getOptimisticLockStyle().isVersion()) {if ( checkVersion( includeProperty ) ) {getVersionType().nullSafeSet( update, oldVersion, index, session );}}else if ( isAllOrDirtyOptLocking() && oldFields != null ) {boolean[] versionability = getPropertyVersionability(); //TODO: is this really necessary????boolean[] includeOldField = entityMetamodel.getOptimisticLockStyle().isAll()? getPropertyUpdateability(): includeProperty;Type[] types = getPropertyTypes();for ( int i = 0; i < entityMetamodel.getPropertySpan(); i++ ) {boolean include = includeOldField[i] &&isPropertyOfTable( i, j ) &&versionability[i]; //TODO: is this really necessary????if ( include ) {boolean[] settable = types[i].toColumnNullness( oldFields[i], getFactory() );types[i].nullSafeSet(update,oldFields[i],index,settable,session);index += ArrayHelper.countTrue( settable );}}}if ( useBatch ) {session.getJdbcCoordinator().getBatch( updateBatchKey ).addToBatch();return true;}else {return check(session.getJdbcCoordinator().getResultSetReturn().executeUpdate( update ),id,j,expectation,update,sql);}}catch (SQLException e) {if ( useBatch ) {session.getJdbcCoordinator().abortBatch();}throw e;}finally {if ( !useBatch ) {session.getJdbcCoordinator().getResourceRegistry().release( update );session.getJdbcCoordinator().afterStatementExecution();}}}catch (SQLException e) {throw getFactory().getSQLExceptionHelper().convert(e,"could not update: " + MessageHelper.infoString( this, id, getFactory() ),sql);}}

关键之处:

  • useBatch的赋值逻辑
public boolean isBatchable() {return optimisticLockStyle().isNone()|| !isVersioned() && optimisticLockStyle().isVersion()|| getFactory().getSessionFactoryOptions().isJdbcBatchVersionedData();}1. 配置了`spring.jpa.properties.hibernate.jdbc.batch_versioned_data`为true;
2. jdbcBatchSizeToUse > 1, 即`spring.jpa.properties.hibernate.jdbc.batch_size`大于0
  • 如果useBatch为true

调用session.getJdbcCoordinator().getBatch(updateBatchKey).addToBatch();
这里的updateBatchKeycom.example.domain.User#UPDATE;此处仅是将PreparedStatement放入待执行队列。
之后便执行session.getJdbcCoordinator().executeBatch()逻辑;请看BatchingBatch.performExecution

  • 如果useBatch为false

调用session.getJdbcCoordinator().getResultSetReturn().executeUpdate( update ),并调用check方法执行检查。
此处检查失败,则会抛出乐观锁异常!

BatchingBatch.performExecution
hibernate-core-5.4.32.Final-sources.jar!/org/hibernate/engine/jdbc/batch/internal/BatchingBatch.java

private void performExecution() {LOG.debugf( "Executing batch size: %s", batchPosition );try {for ( Map.Entry<String,PreparedStatement> entry : getStatements().entrySet() ) {try {final PreparedStatement statement = entry.getValue();final int[] rowCounts;try {getJdbcCoordinator().getJdbcSessionOwner().getJdbcSessionContext().getObserver().jdbcExecuteBatchStart();rowCounts = statement.executeBatch();}finally {getJdbcCoordinator().getJdbcSessionOwner().getJdbcSessionContext().getObserver().jdbcExecuteBatchEnd();}checkRowCounts( rowCounts, statement );}catch ( SQLException e ) {abortBatch();throw sqlExceptionHelper().convert( e, "could not execute batch", entry.getKey() );}}}catch ( RuntimeException re ) {LOG.unableToExecuteBatch( re.getMessage() );throw re;}finally {batchPosition = 0;}}

可以看到这里调用了statement.executeBatch(),并返回了int[] rowCounts;
然后调用checkRowCounts( rowCounts, statement ); >Expectations#BasicExpectation.checkBatched
此处检查失败,则会抛出乐观锁异常!

问题原因

非批量模式下,检查执行结果是调用的checkNonBatched方法,该方法仅检查更新条目数是否一致:

private void checkNonBatched(int rowCount, String statementSQL) {if ( expectedRowCount > rowCount ) {throw new StaleStateException("Unexpected row count: " + rowCount + "; expected: " + expectedRowCount+ "; statement executed: " + statementSQL);}if ( expectedRowCount < rowCount ) {String msg = "Unexpected row count: " + rowCount + "; expected: " + expectedRowCount;throw new TooManyRowsAffectedException( msg, expectedRowCount, rowCount );}}

批量模式下,检查执行结果是调用的checkBatched方法,检查逻辑如下:

private void checkBatched(int rowCount, int batchPosition, String statementSQL) {if ( rowCount == -2 ) {LOG.debugf( "Success of batch update unknown: %s", batchPosition );}else if ( rowCount == -3 ) {throw new BatchFailedException( "Batch update failed: " + batchPosition );}else {if ( expectedRowCount > rowCount ) {throw new StaleStateException("Batch update returned unexpected row count from update ["+ batchPosition + "]; actual row count: " + rowCount+ "; expected: " + expectedRowCount + "; statement executed: "+ statementSQL);}if ( expectedRowCount < rowCount ) {String msg = "Batch update returned unexpected row count from update [" +batchPosition + "]; actual row count: " + rowCount +"; expected: " + expectedRowCount;throw new BatchedTooManyRowsAffectedException( msg, expectedRowCount, rowCount, batchPosition );}}}

问题便在于此!

int[] executeBatch() throws SQLException 

返回值说明:

① 大于或等于零的数字,表示命令已成功处理,并且是更新计数,给出了数据库中受命令影响的行数执行;
SUCCESS_NO_INFO ( -2)的值,表示命令处理成功,但受影响的行数未知;
③ 如果批量更新中的命令之一无法正确执行,此方法引发BatchUpdateException,JDBC Driver可能会也可能不会继续处理剩余的命令。但是Driver的行为是与特定的DBMS绑定的,要么总是继续处理命令,要么从不继续处理命令。如果驱动程序继续处理,方法将返回EXECUTE_FAILED(-3)

在实际的测试过程中发现:

DB类型是否可以返回实际影响行数备注
MySQL
Oracle每个数组位置值均为-2

在Oracle的驱动中没有实现该功能,即提交成功后不能返回影响行数,所以返回-2。
Oracle驱动源码如下:oracle.jdbc.driver.OraclePreparedStatement#executeBatch

 public int[] executeBatch() throws SQLException {synchronized (this.connection) {int[] arrayOfInt = new int[this.currentRank];/* 此处省略N行代码 */if ((this.sqlKind != 1) && (this.sqlKind != 4)) {for (i = 0; i < arrayOfInt.length; i++) {arrayOfInt[i] = -2; // 关键看这行}}this.connection.registerHeartbeat();return arrayOfInt;}
}

根据StackOverflow上的说法,Oracle 11g之前的版本,executeBatch方法返回的均是-2,eg.
在这里插入图片描述
在这里插入图片描述

解决方案

  • 降级批处理

虽然批处理可以提高性能,但为了获取精确的受影响行数,可以考虑将批处理操作降级为单条更新操作,确保每条更新操作都能正确返回受影响的行数。

  • 升级驱动版本至Oracle 12c及以上版本。

尝试升级到ojdbc7或ojdbc8驱动,这些版本对批处理操作的支持更好,可能会解决返回值为-2的问题。

参考资料

  • Spring Data JPA 之乐观锁
  • 聊聊jpa的batch操作的实现
  • Batch insert for nested entities
  • ORACLE JDBC Batch execution doesn’t return actual count of affected rows

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/371473.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

浏览器插件利器-allWebPluginV2.0.0.14-stable版发布

allWebPlugin简介 allWebPlugin中间件是一款为用户提供安全、可靠、便捷的浏览器插件服务的中间件产品&#xff0c;致力于将浏览器插件重新应用到所有浏览器。它将现有ActiveX插件直接嵌入浏览器&#xff0c;实现插件加载、界面显示、接口调用、事件回调等。支持谷歌、火狐等浏…

【音视频 | RTSP】RTSP协议详解 及 抓包例子解析(详细而不赘述)

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

catia数控加工仿真铣平面粗加工

1&#xff0c;零件建模&#xff0c;毛坯建模 2 在毛坯上建立坐标系 3 添加资料刀具 4&#xff0c;双击对相关加工信息做设置 5 Roughing 加工设置 高亮红色区域是必选的&#xff0c;其他可以默认 6 完成加工仿真 7 加工余量

Android zygote访谈录

戳蓝字“牛晓伟”关注我哦&#xff01; 用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章&#xff0c;技术文章也可以有温度。 本文摘要 本文以访谈的方式来带大家了解zygote进程&#xff0c;了解zygote进程是啥&#xff1f;它的作用是啥&#xff1f;它是如何一步…

从零开始开发跑腿配送系统:技术选型与架构设计

开发一个跑腿配送系统涉及多个技术栈和模块&#xff0c;从前端到后端&#xff0c;再到数据库和实时通信&#xff0c;每一个环节都至关重要。本文将详细介绍从零开始开发跑腿配送系统的技术选型与架构设计&#xff0c;并提供部分代码示例以帮助理解。 一、技术选型 前端技术&am…

国产化新标杆:TiDB 助力广发银行新一代总账系统投产上线

随着全球金融市场的快速发展和数字化转型的深入推进&#xff0c;金融科技已成为推动银行业创新的核心力量。特别是在当前复杂多变的经济环境下&#xff0c;银行业务的高效运作和风险管理能力显得尤为重要。总账系统作为银行会计信息系统的核心&#xff0c;承载着记录、处理和汇…

Linux网络管理

一、linux网络管理 1.获取计算机的网络信息 基本语法&#xff1a; #ifconfig #ip address &#xff08;ip a&#xff09; 解析&#xff1a; ens33&#xff1a;默认网卡 lo&#xff1a;环回网卡&#xff0c;127.0.0.1作为固定ip代表本机 virbr0&#xff1a;虚拟网络接口&…

Python入门 2024/7/3

目录 for循环的基础语法 遍历字符串 练习&#xff1a;数一数有几个a range语句 三个语法 语法1 语法2 语法3 练习&#xff1a;有几个偶数 变量作用域 for循环的嵌套使用 打印九九乘法表 发工资案例 continue和break语句 函数的基础定义语法 函数声明 函数调用 …

探索Linux:开源世界的无限可能

Linux是一款开源操作系统&#xff0c;它的起源可以追溯到上世纪90年代初。这个故事始于一个名叫Linus Torvalds的芬兰大学生&#xff0c;他在1983年开始编写一个用于个人电脑的操作系统内核。在他的努力下&#xff0c;Linux逐渐发展成为一个稳定而强大的操作系统。 然而&#…

Apache Seata配置管理原理解析

本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 Apache Seata配置管理原理解析 说到Seata中的配置管理&#xff0c;大家可能会想到Seata中适配…

盘点8款国内顶尖局域网监控软件(2024年国产局域网监控软件排名)

局域网监控软件对于企业网络管理至关重要&#xff0c;它们可以帮助IT部门维护网络安全&#xff0c;优化网络性能&#xff0c;同时监控和控制内部员工的网络使用行为。以下是八款备受推崇的局域网监控软件&#xff0c;每一款都有其独特的优势和适用场景。 1.安企神软件 试用版领…

【机器学习实战】Datawhale夏令营:Baseline精读笔记2

# AI夏令营 # Datawhale # 夏令营 在原有的Baseline上除了交叉验证&#xff0c;还有一种关键的优化方式&#xff0c;即特征工程。 如何优化特征&#xff0c;关系着我们提高模型预测的精准度。特征工程往往是对问题的领域有深入了解的人员能够做好的部分&#xff0c;因为我们要…

护网在即,知攻善防助力每一位安服仔~

前言 是不是已经有师傅进场了呢~ 是不是有安服&#x1f412;在值守呢~ 您是不是被网上眼花缭乱的常用应急响应工具而烦恼呢&#xff1f; 何以解忧&#xff1f;唯有知攻善防&#xff01; 创作起源&#xff1a; 驻场、护网等&#xff0c;有的客户现场只允许用客户机器&…

Linux-gdb

目录 1.-g 生成含有debug信息的可执行文件 2.gdb开始以及gdb中的常用执行指令 3.断点的本质用法 4.快速跳出函数体 5.其他 1.-g 生成含有debug信息的可执行文件 2.gdb开始以及gdb中的常用执行指令 3.断点的本质用法 断点的本质是帮助我们缩小出问题的范围 比如&#xff0c;…

Linux系统安装软件包的方法rpm和yum详解

起因&#xff1a; 本篇文章是记录学习Centos7的历程 关于rpm 常见命令 1&#xff09;查看已经安装的软件包 rpm -q 软件包名 2&#xff09;查看文件的相关信息 rpm -qi 软件包名 3&#xff09;查看软件包的依赖关系 就是说要想安装这个软件包&#xff0c;就必须把一些前…

非NI GPIB卡与LabVIEW兼容性分析

在许多测试和测量应用中&#xff0c;通用接口总线&#xff08;GPIB&#xff09;是一种广泛使用的标准。尽管国家仪器公司&#xff08;NI&#xff09;提供的GPIB硬件和LabVIEW软件的组合被广泛接受和使用&#xff0c;但成本可能较高。因此&#xff0c;一些用户会考虑使用其他厂商…

ASP.NET Core----基础学习03----开发者异常页面 MVC工作原理及实现

文章目录 1. 开发者异常页面(1)Startup.cs 页面的基础配置(2)自定义显示报错代码的前后XX行 2. MVC 的原理3. MVC 的实现4.默认路由路径5.返回Json字符串 1. 开发者异常页面 (1)Startup.cs 页面的基础配置 namespace ASP.Net_Blank {public class Startup{private readonly IC…

半导体制造企业 文件共享存储应用

用户背景&#xff1a;半导体设备&#xff08;上海&#xff09;股份有限公司是一家以中国为基地、面向全球的微观加工高端设备公司&#xff0c;为集成电路和泛半导体行业提供具竞争力的高端设备和高质量的服务。 挑战&#xff1a;芯片的行业在国内迅猛发展&#xff0c;用户在上海…

英语学习交流小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;每日打卡管理&#xff0c;备忘录管理&#xff0c;学习计划管理&#xff0c;学习资源管理&#xff0c;论坛交流 微信端账号功能包括&#xff1a;系统首页&#xff0c;学习资源&…

Python之numpy常用知识点总结

文章目录 前言知识点1&#xff1a;np.maximum知识点2&#xff1a;ndarray数据类型知识点3&#xff1a;数据运算知识点4&#xff1a;数组和标量间的运算知识点5&#xff1a;数组的索引和切片知识点6&#xff1a;数组的转置和轴对称知识点7&#xff1a;检索数组元素 前言 在机器学…