You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 19:53:15