1 背景
项目持久层框架使用spring-data-jpa,jpa实现采用hibernate。实体使用乐观锁的方式加锁,也就是添加version字段。
@Version private Long version;
乐观锁:给数据加一个版本, 每一操作数据就更新版本,不会上锁,但是在更新的时候你会判断这期间有没有人去更新这个数据
悲观锁:给数据加了一把锁 ,同事务只能一个线程进行操作,使用完了锁释放, 没释放前后面想要操作的人就得排队 ,效率低,但是很安全
2 问题描述
异常信息:
org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [com.sto.station.model.PracticalVehicleInfo] with identifier [81860]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.sto.station.model.PracticalVehicleInfo#81860] at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:337) at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:255) at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:531) at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242) at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:154) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:149) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212) at com.sun.proxy.$Proxy144.updateDirtyData(Unknown Source) at com.sto.station.schedule.PortSchedule.processDirtyDataJobHandler(PortSchedule.java:287) at com.sto.station.schedule.PortSchedule$$FastClassBySpringCGLIB$$9503b228.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:367) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) at com.sto.station.schedule.PortSchedule$$EnhancerBySpringCGLIB$$43901398.processDirtyDataJobHandler(<generated>) at sun.reflect.GeneratedMethodAccessor775.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.xxl.job.core.handler.impl.MethodJobHandler.execute(MethodJobHandler.java:31) at com.xxl.job.core.thread.JobThread.run(JobThread.java:163) Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.sto.station.model.PracticalVehicleInfo#81860] at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:2651) at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3495) at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3358) at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3772) at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:201) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604) at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478) at java.util.LinkedHashMap.forEach(LinkedHashMap.java:684) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475) at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:348) at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40) at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:102) at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1362) at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1349) at org.hibernate.query.internal.NativeQueryImpl.beforeQuery(NativeQueryImpl.java:267) at org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1591) at org.hibernate.query.Query.getResultList(Query.java:165) at org.springframework.data.jpa.repository.query.JpaQueryExecution$CollectionExecution.doExecute(JpaQueryExecution.java:126) at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:88) at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:154) at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:142) at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor$QueryMethodInvoker.invoke(QueryExecutorMethodInterceptor.java:195) at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:152) at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:130) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:367) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139) ... 24 common frames omitted
日志中偶尔抛出org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class xxx optimistic locking failed异常。底层异常信息是hibernate抛出的org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction 。排查后发现是jpa并发更新同一个实体引起的。
这种异常一般是由于数据库里的某一条记录的某一个版本对应的信息,同时被两个事务读取到并试图进行写操作(包括更新和删除);这种情况下第一个写成功的事务不会有影响,第二个事务再对同一版本的同一记录进行写操作时,抛出乐观锁异常ObjectOptimisticLockingFailureException。
3 问题复现
3.1 全局异常拦截
将乐观锁异常加到全局异常拦截器中,封装该异常信息(非必要条件,可加可不加)。
3.2 编写服务端测试方法
Controller:
Service:
3.3 多线程模拟并发测试类
package com.test.service; import cn.hutool.http.HttpUtil; import org.apache.commons.lang3.RandomStringUtils; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class MyTest { public static void main(String[] args) throws Exception { ThreadPoolExecutor executor = new ThreadPoolExecutor(2000, 5000, 2, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(5000)); // 使用CountDownLatch模拟并发请求 CountDownLatch latch = new CountDownLatch(1); // 模拟多个用户(i小于几就是几个) for (int i = 0; i < 6; i++) { JpaOptimisticLockTest jpaTest = new JpaOptimisticLockTest(latch); executor.execute(jpaTest); } // 计数器減一 所有线程释放 并发访问 latch.countDown(); } static class JpaOptimisticLockTest implements Runnable { CountDownLatch latch; public JpaOptimisticLockTest(CountDownLatch latch) { this.latch = latch; } @Override public void run() { long starTime = 0; // 使用随机字符(如果是相同的,走更新不会触发异常;jpa相同数据行记录version不会变) String randomOrgName = RandomStringUtils.randomAlphanumeric(10); try { starTime = System.currentTimeMillis(); // 调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行 latch.await(); System.out.println("请求开始了"); // 调用服务接口 String s = HttpUtil.get("http://127.0.0.1:8080/test?id=4191&orgName="+randomOrgName); System.out.println("返回的内容为:"+s); } catch (InterruptedException e) { e.printStackTrace(); } long endTime = System.currentTimeMillis(); Long t = endTime - starTime; System.out.println(t / 1000F + "秒"); } } }
运行结果:
模拟2个线程并发:
服务端:
模拟10个线程并发:
模拟100个线程并发:
可以看到在多线程并发访问接口,用jpa乐观锁的形式修改数据时,大部分都是异常状态,没有办法成功更新数据行。
4 解决方案
JPA乐观锁异常有多种解决方案,主要包括重试机制、使用悲观锁、业务层加分布式锁等;根据不同项目的实际业务场景采取各自适合的方式。
4.1 异常添加重试机制
并发场景不多,并发量不大的情况可以使用重试机制,同时为了降低代码侵入,采用注解+AOP实现重试机制。针对存在并发,多线程访问的api方法,且需要失败重试的业务场景,可以加上重试注解。
注解:@RetryJpaOptimisticLockAspect
package com.sto.station.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author xiongbangwen */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RetryJpaOptimisticLock { /** * 自定义次数 默认3次 */ int times() default 3; }
Aop:RetryJpaOptimisticLockAspect
package com.sto.station.aop; import com.sto.station.annotation.RetryJpaOptimisticLock; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.annotation.Order; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * jpa乐观锁异常ObjectOptimisticLockingFailureException重试机制 通过AOP统一拦截这种异常并进行一定次数的重试 * * @author xiongbangwen * @className RetryOnOptimisticLockingFailure * @date 2021/9/16 15:23 */ @Aspect @Component @Order(1) @Slf4j public class RetryJpaOptimisticLockAspect { private int maxRetries; public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } @Pointcut("@annotation(com.sto.station.annotation.RetryJpaOptimisticLock)") public void retryOnFailure(){} @Around("retryOnFailure()") public Object retry(ProceedingJoinPoint pjp) throws Throwable { // 获取拦截的方法名 MethodSignature msig = (MethodSignature) pjp.getSignature(); // 返回被织入增加处理目标对象 Object target = pjp.getTarget(); // 为了获取注解信息 Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes()); // 获取注解信息 RetryJpaOptimisticLock annotation = currentMethod.getAnnotation(RetryJpaOptimisticLock.class); // 设置重试次数 this.setMaxRetries(annotation.times()); // 重试次数 int count = 0; OptimisticLockingFailureException lockFailureException; do { count++; try { // 再次执行业务代码 return pjp.proceed(); } catch (OptimisticLockingFailureException ex) { log.error("jpa乐观锁异常ObjectOptimisticLockingFailureException重试失败, count:{}",count); lockFailureException = ex; } } while (count < maxRetries); log.error("============ jpa乐观锁异常ObjectOptimisticLockingFailureException".concat(String.valueOf(maxRetries)).concat("次机会全部重试失败 ===========")); throw lockFailureException; } }
测试:在test方法上添加注解
执行并发测试类,并发设置2或3
服务端:
并发设置10
设置重试次数为10次,再执行测试类 @RetryJpaOptimisticLock(times = 10)
往上递增亦如此
总结:当重试次数>=并发数时,不会出现乐观锁异常,会在重试过程中全部执行成功;当重试次数<并发数时,还是会出现乐观锁异常。
4.2 数据库层面使用悲观锁
写操作使用MySQL行锁的悲观锁(for update),在数据库层面最大程度避免了并发问题,但数据库锁时间长,开销大,更应该在应用层做处理
4.3 Redis分布式锁
服务端业务层添加redis分布式锁:
新增重试机制,retryCount小于等于0 无限循环,一直尝试加锁,retryCount大于0 尝试指定次数后,退出
总结:可以看到返回的version是按前后顺序依次返回的,既redis分布式锁将异步任务变相的转为同步,若是在高并发场景下,对执行顺序有要求的可以使用。