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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 01:50:54