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

Spring Hibernate遗留应用中OptimisticLockException处理方案咨询

单事务内Hibernate乐观锁异常排查与处理

异常现象

javax.persistence.OptimisticLockException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1.

该异常通常由多事务并发访问同表触发,但本次仅在单事务内间歇性出现。开启show_sql或添加10ms延迟后异常无法复现,推测问题源于单事务内多次同表操作的时机冲突。

相关映射文件

User.hbm.xml

<class lazy="false" name="UserBO" table="USER">           
    <id name="id" type="java.lang.Long" column="ID" unsaved-value="null">              
        <generator class="native"/>
    </id>
    <many-to-one name="addressSet" column="ADDRESS_SET" class="AddressSetDO"/> 
</class>

AddressSet.hbm.xml

<class lazy="false" name="AddressSetDO" table="ADDRESS_SET_TABLE">
    <id name="id" type="java.lang.Long" column="ID" unsaved-value="null">            
        <generator class="native"/>
    </id>
    <version column="HIB_VERSION" name="HibVersion"/>      
    <set name="address" inverse="true" lazy="false" cascade="delete">            
        <key column="ADDRESS_SET_ID" />
        <one-to-many class="AddressDO"/>                 
    </set>       
</class>

Address.hbm.xml

<class lazy="false" name="Address" table="ADDRESS">     
    <id name="id" type="java.lang.Long" column="ID" unsaved-value="null">           
        <generator class="native"/>
    </id>        
    <version column="HIB_VERSION" name="HibVersion"/>
    <many-to-one name="addressSet" not-null="true" class="AddressSetDO" index="ADDRESSSET" inverse="true" >      
        <column name="ADDRESS_SET_ID"  not-null="true" /> 
    </many-to-one>  
</class>

代码执行流程

Service层(单事务内三次调用save)

Transaction start
// 业务逻辑
dao.save(data);
// 业务逻辑
dao.save(data);
// 业务逻辑
dao.save(data);
Transaction end

Dao层(手动保存关联表)

saveOrUpdate(userBo); // 保存UserBO
if(userBo.getAddressSet() != null) {
    saveOrUpdate(userBo.getAddressSet()); // 保存AddressSetDO
    saveOrUpdate(userBo.getAddressSet().getAddress()); // 保存Address集合
}

saveOrUpdate实现

void saveOrUpdate(obj) {
    try {
        hibernateSession().saveOrUpdate(obj);
        checkReadOnlySession();
        hibernateSession().flush();
    } catch (PersistenceException e) {
        throw e;
    }
}

注意:现有逻辑已运行多年,修改风险极高。

问题解答

1. 最优处理方案

优先选择移除手动flush操作,让Hibernate自动管理会话刷新时机:

  • 当前代码每次saveOrUpdate后立刻调用flush(),导致单事务内多次强制刷新会话,触发重复update语句。当同一AddressSetDO实例被多次调用saveOrUpdate时,前一次flush已经更新了数据库中的版本号,后续flush用旧版本号执行update就会出现更新行数为0的情况。
  • 保留saveOrUpdate但删除hibernateSession().flush(),Spring事务默认会在提交前自动统一flush,避免单事务内重复刷新导致的版本号不一致问题。
  • 若无法修改saveOrUpdate方法,退而求其次:在Dao的save方法中对同一实例做幂等处理,比如用session.contains(obj)判断实例是否已在会话中,避免重复调用saveOrUpdate。

2. 重试机制的选择

必须选择从新事务从头重试:

  • 同一事务内重试无意义,当前事务的会话中已存在版本号不一致的实例状态,重试只会重复触发相同异常。
  • 新事务重试需要重新获取数据、重新执行所有业务逻辑,确保会话状态干净,版本号与数据库一致。
  • 注意:限制重试次数(如3次)避免死循环,同时确保业务逻辑是幂等的,重复执行不会产生脏数据。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 14:10:11