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

使用@Version实现Hibernate乐观锁的@Test测试未按预期工作

问题原因分析
  • 会话缓存复用实体实例:JPA的一级缓存(会话级缓存)会在同一个事务/会话内复用查询到的实体对象。你两次查询获取的Account其实是同一个内存引用,修改的是同一个对象的状态,自然不会触发乐观锁的版本冲突检查。
  • 版本号未递增的本质:同一个会话中,实体处于托管状态,第一次保存后内存中的version会更新,但由于缓存存在,第二次查询拿到的是已经更新version的同一对象;若未手动将变更刷入数据库,数据库中的version也不会同步,导致后续操作无法触发冲突。
正确编写乐观锁测试用例的步骤

核心是模拟不同会话/事务下的独立实体实例,让两个操作拿到带有相同旧版本号的不同对象,再依次修改保存触发冲突。

方案1:清除会话缓存+手动刷新

通过清除JPA的一级缓存,让第二次查询从数据库重新加载实体,确保两个操作拿到不同的实例且版本号一致。

示例代码

Account实体类(确保@Version注解正确)

@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private BigDecimal balance;
    @Version // 乐观锁注解,类型需为JPA支持的类型(Integer/Long等)
    private Integer version;

    // 省略getter、setter、构造方法
}

测试用例

@SpringBootTest
@Transactional
public class AccountOptimisticLockTest {

    @Autowired
    private AccountRepository accountRepository;
    @Autowired
    private EntityManager entityManager;

    @Test
    void testOptimisticLockConflict() {
        // 1. 初始化账户,保存到数据库
        Account initialAccount = new Account();
        initialAccount.setBalance(new BigDecimal("100.00"));
        accountRepository.save(initialAccount);
        Long accountId = initialAccount.getId();
        // 刷新会话,确保初始数据写入数据库
        entityManager.flush();
        // 清除缓存,避免后续查询复用初始实例
        entityManager.clear();

        // 2. 模拟设备1查询账户(此时version为0)
        Account device1Account = accountRepository.findById(accountId).orElseThrow();
        // 3. 清除缓存,模拟设备2从数据库重新查询(version仍为0)
        entityManager.clear();
        Account device2Account = accountRepository.findById(accountId).orElseThrow();

        // 4. 设备1修改并保存,version递增为1
        device1Account.setBalance(device1Account.getBalance().subtract(new BigDecimal("10.00")));
        accountRepository.save(device1Account);
        entityManager.flush(); // 强制刷入数据库,更新version

        // 5. 设备2修改并保存,此时数据库version已为1,device2Account的version还是0,触发乐观锁异常
        assertThrows(ObjectOptimisticLockingFailureException.class, () -> {
            device2Account.setBalance(device2Account.getBalance().subtract(new BigDecimal("20.00")));
            accountRepository.save(device2Account);
            entityManager.flush();
        });
    }
}

方案2:使用独立事务

通过开启新事务,让每个设备的操作在独立的会话中执行,天然隔离缓存。

示例代码

@SpringBootTest
public class AccountOptimisticLockTest {

    @Autowired
    private AccountRepository accountRepository;
    @Autowired
    private TestTransaction testTransaction;

    @Test
    void testOptimisticLockWithSeparateTransactions() {
        // 初始化账户
        Account initialAccount = new Account();
        initialAccount.setBalance(new BigDecimal("100.00"));
        accountRepository.save(initialAccount);
        Long accountId = initialAccount.getId();
        testTransaction.end(); // 结束初始化事务

        // 设备1操作:开启新事务
        testTransaction.start();
        Account device1Account = accountRepository.findById(accountId).orElseThrow();
        device1Account.setBalance(device1Account.getBalance().subtract(new BigDecimal("10.00")));
        accountRepository.save(device1Account);
        testTransaction.end(); // 提交事务,数据库version更新为1

        // 设备2操作:开启新事务
        testTransaction.start();
        Account device2Account = accountRepository.findById(accountId).orElseThrow();
        // 模拟设备2不知道设备1的修改,尝试保存
        assertThrows(ObjectOptimisticLockingFailureException.class, () -> {
            device2Account.setBalance(device2Account.getBalance().subtract(new BigDecimal("20.00")));
            accountRepository.save(device2Account);
            testTransaction.end();
        });
    }
}

关键注意事项

  • 确保@Version注解的字段类型是JPA提供者(如Hibernate)支持的类型,常用的有Integer、Long、Timestamp。
  • 测试时必须保证两次修改操作的实体实例是独立的内存对象,且初始版本号一致。
  • 手动flush会话或提交事务,确保数据库中的版本号及时更新,否则冲突检查可能无法触发。

内容的提问来源于stack exchange,提问作者Sobhan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 01:05:17