使用@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
相关产品推荐
相关产品推荐

