@Transactional与synchronized共存下短间隔并发请求异常原因问询
问题原因分析:Spring事务与synchronized的执行顺序冲突
这个问题的核心在于Spring事务的代理机制和你代码里synchronized块的执行时机不匹配,导致事务提交滞后于同步块的释放,进而引发并发读取旧数据的问题。咱们一步步拆解:
1. 加@Transactional时的执行流程(出问题的场景)
Spring的@Transactional默认是基于动态代理实现的——也就是说,你的save方法实际被代理类包裹了一层事务逻辑,完整执行流程是这样的:
- 代理类先开启数据库事务
- 调用目标对象的
save方法:- 获取
synchronized(this)锁(此时第二个请求会被阻塞) - 执行
getDataToSaveOrUpdate:从数据库读取当前数据状态 - 执行
repository.saveAll:把新数据写入数据库,但这只是在当前事务内的修改,还没提交到数据库 - 释放
synchronized锁(第二个请求此时可以进入同步块) - 执行
logger.debug("exiting the method")
- 获取
- 代理类提交数据库事务(这一步才把之前的修改真正持久化到数据库)
看到问题了吗?当第一个请求释放同步锁时,它的数据库事务还没提交!第二个请求立刻进入同步块执行getDataToSaveOrUpdate,这时候数据库里的数据还是旧的(因为第一个事务的修改还没落地),所以会重复生成要保存的数据,最终导致异常。
2. 移除@Transactional后的执行流程(正常运行的场景)
没有事务代理时,save方法的执行逻辑直接在目标对象上运行:
- 获取
synchronized(this)锁 - 执行
getDataToSaveOrUpdate读取数据 - 执行
repository.saveAll:因为没有手动事务,JDBC会自动提交修改,数据立刻持久化到数据库 - 释放
synchronized锁 - 执行
logger.debug
这时候第一个请求的修改在释放锁前就已经写入数据库了,第二个请求进入同步块时,getDataToSaveOrUpdate读到的是最新的数据,自然就正常了。
3. 关键细节补充
- 为什么
synchronized没挡住?因为synchronized只保证目标方法内的同步块互斥,但Spring的事务逻辑是在目标方法之外的代理层执行的,同步块的释放早于事务提交,这就给了并发请求读取旧数据的窗口。 - 数据库隔离级别也在其中起作用:默认的
READ_COMMITTED(读已提交)隔离级别下,第二个事务无法读取第一个事务未提交的修改,所以getDataToSaveOrUpdate只能读到旧数据,进一步加剧了问题。
内容的提问来源于stack exchange,提问作者Sunil Kumar
相关产品推荐
相关产品推荐

