项目持久层框架使用spring-data-jpa,jpa实现采用hibernate。实体使用乐观锁的方式加锁,也就是添加如下字段。
@Version
private Long version;
最近发现在日志中偶尔报org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class xxx optimistic locking failed异常。底层异常信息是hibernate抛出的org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction 。排查后发现是jpa并发更新同一个实体引起的。具体流程如下(以实体user1为例)
事务A | 事务B | 实体版本号version | 说明 |
加载实体user1 | 加载实体user1 | 1 | 这里谁前谁后没关系 |
修改属性:user1.name=张三 | 修改属性user1.name=李四 | 这里谁先修改谁后修改没关系 | |
save(user1) | 2 | 检查版本号发现没变化,修改为2 | |
save(user1) | 检查版本号,发现自己的版本号1和实际的版本号2不匹配,判定已经被其他事务修改(事务B),则本次更新失败,抛出如上异常。 |
思考后有如下解决方案:
- 不通过jpa的实体更新方式,通过原生sql更新语句进行更新,缺点就是舍弃了jpa的对象管理方式。
- 通过悲观锁(for update),使事务顺序执行。缺点就是相比乐观锁降低了并发性,也需要写sql或者额外加jpa注解的方式,不够方便。
- 最后一种也是最终项目采用的方案,通过AOP统一拦截这种异常并进行一定次数的重试,spring官方文档在讲AOP的时候也拿这种方式举例,如下:
@Aspect
@Component
@Order(1)
public class OptimisticLockInterceptor {
@Pointcut("within(com.test.apis..*)")
public void retryPointCut() {
}
@Around("retryPointCut()")
public Object test(ProceedingJoinPoint pjp) throws Throwable {
for (int i = 0; i <= 4; i++) {
try {
return pjp.proceed();
} catch (OptimisticLockingFailureException ex) {
if (i > 3) {
throw ex;
}
}
}
return null;
}
}
其中的order(1)表面此拦截在事务拦截的上层,否则会报错。
不过后来想想这种方式如果在重试模块不支持重试的情况下,就会存在问题。这就要看具体业务是什么样,设计不同的处理方式了。