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

