蓝凌EKP产品:一次 Hibernate 乐观锁 + 死锁的深度踩坑实录
—— clear() 一个集合,为什么引发 OptimisticLockException 和数据库死锁?
这是一次看似“新增 / 查询”的普通业务操作,却最终演变成
Hibernate 乐观锁异常 + MySQL 死锁 + 批量更新失败 的连环事故。
一、问题现象
线上频繁出现如下异常:
javax.persistence.OptimisticLockException:
Batch update returned unexpected row count from update [0];
actual row count: 0; expected: 1
伴随的数据库日志还有:
Deadlock found when trying to get lock; try restarting transaction
SQL: delete from km_agreement_review_areader where fd_source_id=?
诡异点在于:
-
表面调用的是 新增审计意见 / 新增参数
-
堆栈中指向的是
find()查询 -
实际报错却是 update / delete
-
涉及的表包括:
-
km_agreement_review_areader -
lbpm_execution
-
怎么看都不像是一个地方的问题。
二、第一个误区:find / insert 不会触发乐观锁?
这是一个非常常见的误区。
在 Hibernate / JPA 中,只要触发了 flush,
所有被托管(managed)的实体都会被统一同步到数据库。
也就是说:
insert A
find B
在 Hibernate 内部真实执行顺序是:
flush Session
├─ update / delete / insert(之前积攒的)
└─ 然后才是 find B
异常只是“挂”在 find 上,
真正出问题的是 flush 里的 update / delete。
三、真正的导火索:BaseAuthModel 里的 clear()
所有业务 Model 都继承了一个公共父类:
public abstract class BaseAuthModel {
protected List authAllReaders;
protected void recalculateReaderField() {
if (authAllReaders == null) {
authAllReaders = new ArrayList();
} else {
authAllReaders.clear(); // 关键代码
}
// 重新 add 各种 reader
authAllReaders.add(getDocCreator());
...
}
}
这一行代码,看起来极其“正常”:
authAllReaders.clear();
但在 Hibernate 眼里,这是一个非常危险的操作。
四、Hibernate 视角:clear() 到底干了什么?
假设映射关系是:
@OneToMany
@JoinColumn(name = "fd_source_id")
private List<Reader> authAllReaders;
那么:
authAllReaders.clear();
在 Hibernate 中 等价于:
delete from km_agreement_review_areader
where fd_source_id = ?
如果接下来你又:
authAllReaders.add(...)
authAllReaders.add(...)
Hibernate flush 时会执行:
delete from km_agreement_review_areader where fd_source_id = ?
insert into km_agreement_review_areader ...
insert into km_agreement_review_areader ...
五、为什么会死锁?
关键点:fd_source_id 是外键
当多个并发请求同时操作同一个业务对象时:
-
线程 A:clear → delete(持有子表行锁)
-
线程 B:clear → delete(等待)
-
线程 A:后续 update 父表 / execution
-
线程 B:持有另一批锁
👉 典型的 InnoDB 死锁模型
DELETE 子表
→ UPDATE 父表
→ DELETE 子表(另一个事务)
六、为什么又会触发 OptimisticLockException?
系统中 只有一个地方配置了 version:
<version name="lockerVersion"
column="fd_locker_version"
type="long"/>
配置在:lbpm_execution 表。
而问题在于:
-
lbpm_execution在流程执行时 早已被加载进 Session -
clear / add Reader 的过程中:
-
execution 被标记为 dirty
-
-
flush 时 Hibernate 会顺带执行:
update lbpm_execution
set fd_locker_version = fd_locker_version + 1
where fd_id = ? and fd_locker_version = ?
如果前面发生了:
-
死锁回滚
-
并发已更新
-
行被别的事务影响
👉 update 0 行
👉 Hibernate 判定 并发冲突
👉 抛 OptimisticLockException
七、为什么堆栈看起来“很乱”?
因为这是 统一 flush 导致的错觉:
-
报错点挂在
find() -
实际执行的是:
-
delete 子表
-
update execution
-
-
Hibernate 不会告诉你是哪一个实体被判定 dirty
这也是 Hibernate 最反直觉的地方之一。
八、问题的本质总结(一句话)
这是一个典型的:
ORM 管理集合 + clear + 并发 + version
共同作用下的“架构级事故”
九、解法一(最推荐):绕过 ORM 管理这个集合
❌ 错误姿势(当前做法)
@OneToMany
private List authAllReaders;
并对其:
clear() + add()
✅ 正确姿势 1:不做批量异常在新增,只做增量
这里有现成方案,可以关注后续.....,
public void update(IBaseModel modelObj) throws Exception {
IFieldsCalculator fieldsRecalculator = modelObj.getFieldsCalculator();
if(fieldsRecalculator!=null){
fieldsRecalculator.recalculateFields();
}else{
modelObj.recalculateFields();
}
//modelObj.recalculateFields();
afterRecalculateFields(modelObj);
getHibernateTemplate().saveOrUpdate(modelObj);
}
model 实现接口IFieldsCalculator fieldsRecalculator = modelObj.getFieldsCalculator();完成model 的可阅读者的增量更新。
十、为什么这是“无解但必须改”的问题?
因为:
-
BaseAuthModel 是 全局父类
-
clear() 是 高频路径
-
并发下必然出现:
-
死锁
-
乐观锁冲突
-
-
Hibernate 在这里 帮不了你
👉 必须从 ORM 设计上规避
十一、最终总结
Hibernate 非常适合“领域模型”
但需谨慎操作“权限计算 / 批量 clear + 重建”这种模型
这次问题的真正收获不是“修一个 bug”,而是:
-
明确了 哪些集合不该交给 ORM 管
-
明确了 clear() 是 ORM 世界的核按钮
-
明确了 version 不该用在流程执行上下文上
更多推荐
所有评论(0)