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

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);
    }
}

预期流程

  1. 持久化实体并通过@Transactional边界提交
  2. 从数据库获取带有当前rowVersion的实体
  3. 更新(merge)实体并修改字段,使数据库中的rowVersion递增
  4. 重复步骤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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 00:07:46