Spring Boot JPA中@Version与@Transactional联用失效问题
为什么@Transactional会导致@Version乐观锁逻辑异常?
核心原因:持久化上下文的快照机制
当服务方法被@Transactional修饰时,整个方法处于同一个JPA持久化上下文(EntityManager)中,Hibernate的托管实体机制会覆盖你手动设置的版本号:
- 调用
rep.findById(id).get()时,实体被加载到持久化上下文,进入托管状态。Hibernate会自动为该实体创建一份快照,记录加载时的所有属性值,包括数据库中当前有效的rowVersion。 - 你手动调用
entity.setRowVersion(rowVersion)修改版本号,只是改变了实体对象的当前状态,但Hibernate的快照中仍然保留着加载时的正确版本号。 - 事务提交阶段,Hibernate执行脏检查并生成更新SQL时,会忽略你手动设置的@Version字段值,而是使用快照中的原始版本号去和数据库中的版本号做乐观锁比对。这就导致即使你传入错误的版本号,也不会触发并发冲突——因为Hibernate根本没用到你设置的值。
正确的乐观锁实现方式
要实现预期的并发控制逻辑,你需要在加载实体后手动验证版本号,而不是直接修改实体的rowVersion:
@Service @Transactional public class SomeService { private final SomeEntityRepository rep; public void update(Long id, Long rowVersion, String value) { var entity = rep.findById(id).orElseThrow(() -> new RuntimeException("实体不存在")); // 手动验证传入的版本号与数据库当前版本是否一致 if (!Objects.equals(entity.getRowVersion(), rowVersion)) { throw new OptimisticLockingFailureException("实体已被其他用户修改"); } entity.setValue(value); // 无需手动设置rowVersion,Hibernate会自动递增版本号 rep.save(entity); } }
补充说明:非事务场景的差异
如果没有@Transactional,调用rep.findById(id).get()后,实体加载完成就会脱离持久化上下文(进入分离状态)。此时你手动设置rowVersion再调用save,Hibernate会将其视为分离实体的更新,这时候才会使用你设置的版本号去做乐观锁检查——但这种场景下每次操作都会开启新的持久化上下文,效率较低,也不符合事务管理的最佳实践。
内容的提问来源于stack exchange,提问作者aweis
相关产品推荐
相关产品推荐

