Java 21出现Hibernate OptimisticLockException,Java 17运行正常
Java 21 + Hibernate 6 乐观锁异常问题排查与解决
问题场景
项目从Java 17升级至Java 21后,某Hibernate测试用例抛出如下错误:
jakarta.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)
测试用例代码:
@Test void shouldRemoveFeature() { ComplianceScraper scraper = ComplianceScraper.forSite("2features.com") .withFeature(ComplianceFeature.forXpath("//price").build()) .build(); inTx.runC(scraper, dao::save); inTx.runBC(scraper, scraper.getComplianceFeature(), dao::removeFeature); scraper = inTx.run("2features.com", dao::fetchBySite).orElseThrow(RuntimeException::new); assertNull(scraper.getComplianceFeature()); }
原removeFeature()方法在Java 17下运行正常:
public void removeFeature(ComplianceScraper scraper, ComplianceFeature feature) { scraper = getById(scraper.getId()); scraper.removeFeature(feature); // orphanRemoval = true }
但在Java 21中事务提交时会抛出OptimisticLockException,添加em.flush()和em.clear()后测试通过:
public void removeFeature(ComplianceScraper scraper, ComplianceFeature feature) { scraper = getById(scraper.getId()); scraper.removeFeature(feature); em.flush(); em.clear(); }
环境信息:
- Java版本:21
- Hibernate:6.6.31.Final
- JPA提供者:Jakarta Persistence
- 数据库:H2(测试用)
- Flyway:已升级至最新版本
问题解答
1. 为何Java 17正常,Java 21报错?
核心原因是Hibernate 6对实体状态跟踪的校验逻辑升级,和Java版本本身无关——升级Java时同步升级了适配Java 21的Hibernate 6.x版本,而旧版Hibernate(适配Java 17)的状态校验更宽松。
具体逻辑差异:
- 第一个事务提交后,传入第二个事务的
scraper处于游离状态(detached) - 第二个事务中,通过
getById加载了一个新的托管状态(managed)scraper实例 - Hibernate 6在事务提交时,会严格校验持久化上下文中所有关联实体的状态一致性,发现游离实例与托管实例的状态冲突,触发乐观锁异常;而旧版Hibernate不会检测这类冲突,因此Java 17下无报错。
2. em.flush()和em.clear()背后发生了什么?
em.flush():强制将持久化上下文中的待执行SQL立即发送到数据库执行,包括移除关联特性的更新语句,确保数据库状态与内存中托管实体状态完全同步。em.clear():清空整个持久化上下文,移除所有托管状态的实体实例。这样事务提交时,不会再检测到游离scraper实例与托管实例的状态冲突,因为上下文中已无需要校验的实体。
3. 添加这两行是否为正确方案?Hibernate 6/Java 21下有更优处理方式吗?
添加flush()+clear()是可行的临时方案,但并非最优——它绕过了Hibernate的正常状态管理逻辑,复杂场景下可能引发其他状态不一致问题。推荐两种更优方案:
方案一:避免传入游离实体
修改测试用例,仅传递实体ID而非整个游离实例到事务方法:
// 测试用例修改 inTx.runBC(scraper.getId(), scraper.getComplianceFeature(), dao::removeFeatureById); // DAO方法修改 public void removeFeatureById(Long scraperId, ComplianceFeature feature) { ComplianceScraper scraper = getById(scraperId); scraper.removeFeature(feature); }
此方案确保持久化上下文中仅存在一个托管实例,从根源避免状态冲突。
方案二:合并游离实体(谨慎使用)
若必须传入游离实体,可使用em.merge()将其合并到当前持久化上下文:
public void removeFeature(ComplianceScraper scraper, ComplianceFeature feature) { ComplianceScraper managedScraper = em.merge(scraper); managedScraper.removeFeature(feature); }
merge()会将游离实体的状态同步到数据库对应的托管实例(或创建新托管实例),避免多实例状态冲突。注意此操作会触发额外数据库查询,需根据场景评估性能。
内容的提问来源于stack exchange,提问作者Jashwanth Sai
相关产品推荐
相关产品推荐

