JPA @Version字段连续更新触发OptimisticLockException问题咨询
乐观锁更新失败问题排查
问题背景
我们给实体添加@Version字段来使用Hibernate的乐观锁:
@Entity public class MyEntity { @Id @GeneratedValue public Long id; @Version public Long rowVersion; public String field; }
随后编写了@QuarkusTest测试连续更新并获取实体的逻辑:
@QuarkusTest public class RowVersionTest { @Inject EntityManager em; @Test public void testRowVersion(){ MyEntity entity = createEntity(); Long id = entity.id; for (int i=0; i < 10; i++){ MyEntity entityById = getById(id); entityById.field += i; System.out.println("Before: " +entityById.field + " - RowVersion: " + entityById.rowVersion); update(entityById); System.out.println("After: " +entityById.field + " - RowVersion: " + entityById.rowVersion); } } @Transactional public MyEntity createEntity() { MyEntity newEntity = new MyEntity(); newEntity.field = "Hello"; em.persist(newEntity); return newEntity; } public MyEntity getById(Long id){ TypedQuery<MyEntity> query = em.createQuery("Select e from MyEntity e where e.id = :id", MyEntity.class); query.setParameter("id", id); return query.getSingleResult(); } @Transactional public void update(MyEntity entity){ em.merge(entity); } }
预期流程
- 持久化实体并通过
@Transactional边界提交 - 从数据库获取带有当前
rowVersion的实体 - 更新(merge)实体并修改字段,使数据库中的
rowVersion递增 - 重复步骤2-3
实际结果
仅首次更新成功,第二次尝试时触发乐观锁异常,无法获取最新的rowVersion:
Before: Hello0 - RowVersion: 0 After: Hello0 - RowVersion: 0 Before: Hello01 - RowVersion: 0 jakarta.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [org.acme.MyEntity#1]
疑问
为何会出现此行为?我们在事务边界外操作,难道每次都不能获取正确的rowVersion吗?
原因分析
问题核心在于EntityManager的一级缓存(持久化上下文)机制:
getById方法未加@Transactional,查询操作会复用当前EntityManager的持久化上下文。EntityManager会缓存首次查询到的实体,后续调用getById时直接返回缓存里的旧对象,不会去数据库拉取最新的rowVersion。- 第一次
update执行后,数据库中的rowVersion已递增为1,但缓存中的实体rowVersion还是初始的0。第二次循环调用getById拿到的依然是缓存里的旧对象,用这个过期的rowVersion=0去执行merge时,Hibernate检测到数据库版本不匹配,直接抛出乐观锁异常。 - 另外,
update方法的merge操作会返回更新后的托管实体,但你没把这个返回值赋值给entityById,导致循环里的变量始终是旧的缓存对象,rowVersion不会同步更新。
解决方案
方案一:给查询方法加事务,强制刷新缓存
给getById添加@Transactional注解,每次查询都会开启新事务,事务结束后持久化上下文会被清理,下次查询必然从数据库获取最新数据:
@Transactional public MyEntity getById(Long id){ TypedQuery<MyEntity> query = em.createQuery("Select e from MyEntity e where e.id = :id", MyEntity.class); query.setParameter("id", id); return query.getSingleResult(); }
方案二:手动清除缓存或刷新实体
如果不想给查询加事务,可以在查询前清除EntityManager缓存,或者查询后手动刷新实体:
public MyEntity getById(Long id){ // 清除缓存,强制从数据库查询 em.clear(); TypedQuery<MyEntity> query = em.createQuery("Select e from MyEntity e where e.id = :id", MyEntity.class); query.setParameter("id", id); MyEntity entity = query.getSingleResult(); // 或者手动刷新实体,也能拿到最新版本 // em.refresh(entity); return entity; }
补充:正确接收merge的返回值
修改update方法返回merge后的实体,这样能拿到更新后的rowVersion:
@Transactional public MyEntity update(MyEntity entity){ return em.merge(entity); }
然后在测试循环中更新变量:
entityById = update(entityById);
通过以上调整,就能保证每次循环拿到的都是最新的实体和rowVersion,避免乐观锁异常。
内容的提问来源于stack exchange,提问作者arnehehe
相关产品推荐
相关产品推荐

