为何EntityManager#flush会引发LockAcquisitionException?
问题分析:Hibernate flush时触发LockAcquisitionException的原因
问题场景
项目基于Hibernate作为持久化框架,在@Transactional事务边界内,通过Spring Data JPA Repository的@Lock(LockModeType.PESSIMISTIC_WRITE)(对应SQL的FOR UPDATE)查询并锁定最多10条数据,更新实体后调用flush()方法时触发LockAcquisitionException,且仅在高并发时段复现。
代码示例
@Transactional public void method() { // 使用@Lock(LockModeType.PESSIMISTIC_WRITE)锁定数据 List<Foo> entities = fooRepository.selectForUpdate(); // 遍历实体并执行更新操作 for (Foo foo : entities) { foo.setX(321); } // 保存实体列表 fooRepository.saveAll(entities); // flush() 触发LockAcquisitionException fooRepository.flush(); }
异常堆栈(关键片段)
org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement ... at com.sun.proxy.$Proxy150.flush(Unknown Source) at xxx.xxx.xxx.xxx.MyClass.method(MyClass.java:764)
核心原因
- 悲观锁的并发冲突:
@Lock(LockModeType.PESSIMISTIC_WRITE)对应的FOR UPDATE锁在查询时获取,持有至事务结束。高并发下,其他线程可能在当前事务的查询到flush阶段,尝试获取同批数据的锁,若锁等待超时则触发异常。 - 冗余操作引发缓存混乱:查询出的
Foo实体处于Hibernate的托管状态,事务内的属性变更会被自动跟踪,手动调用saveAll属于冗余操作,可能导致Hibernate缓存状态异常,flush时生成额外SQL,扩大锁冲突范围。 - 行锁升级或更新范围不一致:若
selectForUpdate()的查询条件未使用有效索引,数据库可能将行锁升级为表锁,大幅提升并发冲突概率;另外,若flush时更新的行超出selectForUpdate()锁定的范围(如缓存中存在未被锁定的Foo实体),会尝试获取新锁,高并发下易失败。 - 数据库锁超时设置过短:数据库的锁等待超时阈值(如MySQL的
innodb_lock_wait_timeout)设置过短,高并发下事务执行时间超过阈值时,flush阶段的SQL执行会触发超时。
解决建议
- 移除冗余的
saveAll调用:托管状态的实体变更无需手动保存,Hibernate会在事务提交时自动同步,减少缓存操作和不必要的SQL生成,降低锁冲突风险。 - 确保查询与更新的行范围一致:检查业务逻辑,保证
selectForUpdate()锁定的行与后续修改的行完全匹配,避免flush时更新未被锁定的实体。 - 优化数据库索引与锁配置:为
selectForUpdate()的查询条件添加有效索引,避免行锁升级;适当调整数据库锁等待超时时间,平衡并发性能与死锁风险。 - 显式设置锁超时:在
@Lock注解中指定超时参数,如@Lock(value = LockModeType.PESSIMISTIC_WRITE, timeout = 5000),明确锁等待时长,避免无限等待。
内容的提问来源于stack exchange,提问作者Matheus
相关产品推荐
相关产品推荐

