Spring Boot+Hibernate+PostgreSQL并行更新删除请求死锁问题与事务调优
死锁问题解决办法及事务调优方案
一、死锁原因分析
问题核心在于doSomeWorkAndUpdateSomeEntity()的事务持有锁时间过长:该方法将1.2秒的耗时业务逻辑与数据库更新操作放在同一个事务中,导致实体行的排他锁被长时间占用。当后续的deleteSomeEntity()请求过来时,会因无法获取锁进入等待;而更新事务最终提交时,又会因删除事务的等待状态触发死锁检测,抛出死锁异常。
二、死锁解决办法
1. 缩短锁持有时间(最直接有效)
将耗时的业务逻辑移出事务范围,仅把数据库更新操作放在事务内,大幅缩短锁的持有时间,从根源上减少锁竞争窗口:
// 原方法 @Transactional public void doSomeWorkAndUpdateSomeEntity(Long id) { // 1.2秒耗时业务逻辑 doLongRunningBusinessLogic(); // 更新实体 SomeEntity entity = entityManager.find(SomeEntity.class, id); entity.setSomeField("new value"); entityManager.merge(entity); } // 修改后 public void doSomeWorkAndUpdateSomeEntity(Long id) { // 非事务执行耗时业务逻辑 doLongRunningBusinessLogic(); // 单独启动事务执行数据库更新 updateSomeEntityInTransaction(id); } @Transactional private void updateSomeEntityInTransaction(Long id) { SomeEntity entity = entityManager.find(SomeEntity.class, id); if (entity == null) { throw new EntityNotFoundException("SomeEntity不存在,ID: " + id); } entity.setSomeField("new value"); entityManager.merge(entity); }
2. 增加冲突检测与重试机制
在删除方法中提前检测实体状态,并设置事务超时,同时捕获死锁异常进行重试:
@Transactional(timeout = 1) // 设置事务超时1秒,避免无限等待 public void deleteSomeEntity(Long id) { SomeEntity entity = entityManager.find(SomeEntity.class, id); if (entity == null) { return; // 实体已被删除,直接返回 } entityManager.remove(entity); }
在控制器层捕获死锁相关异常(PostgreSQL死锁错误码为40P01),进行2-3次重试:
@DeleteMapping("/{id}") public ResponseEntity<Void> delete(@PathVariable Long id) { int retryCount = 0; final int MAX_RETRY = 3; while (retryCount < MAX_RETRY) { try { someEntityService.deleteSomeEntity(id); return ResponseEntity.noContent().build(); } catch (PersistenceException e) { // 判断是否为PostgreSQL死锁异常 if (e.getCause() instanceof SQLException && "40P01".equals(((SQLException) e.getCause()).getSQLState())) { retryCount++; Thread.sleep(100); // 短暂等待后重试 } else { throw e; } } } return ResponseEntity.status(HttpStatus.CONFLICT).build(); }
3. 引入乐观锁机制
在实体类中添加版本字段,利用Hibernate的乐观锁避免锁竞争:
@Entity public class SomeEntity { @Id private Long id; @Version // 乐观锁版本字段 private Integer version; // 其他业务字段及getter/setter }
此时更新和删除操作会自动校验版本号,若存在并发修改则直接抛出OptimisticLockingFailureException,不会产生锁等待和死锁:
@Transactional private void updateSomeEntityInTransaction(Long id) { SomeEntity entity = entityManager.find(SomeEntity.class, id); if (entity == null) { throw new EntityNotFoundException("SomeEntity不存在,ID: " + id); } entity.setSomeField("new value"); entityManager.merge(entity); // 自动校验版本号 } @Transactional public void deleteSomeEntity(Long id) { SomeEntity entity = entityManager.find(SomeEntity.class, id); if (entity == null) { return; } try { entityManager.remove(entity); // 自动校验版本号 } catch (OptimisticLockingFailureException e) { throw new RuntimeException("实体已被其他事务修改,无法删除", e); } }
三、事务调优方案
1. 最小化事务范围
仅将必须的数据库操作包裹在事务中,所有非数据库操作(如业务计算、外部接口调用、MQ发送等)都放在事务外,减少事务资源占用和锁持有时间。
2. 调整事务隔离级别
PostgreSQL默认隔离级别为可重复读,若业务无需该级别保证,可降级为读提交,降低锁竞争概率:
@Transactional(isolation = Isolation.READ_COMMITTED) public void updateSomeEntityInTransaction(Long id) { // 数据库操作 }
3. 避免长事务
长事务不仅容易引发锁冲突,还会占用数据库连接、增大回滚日志体积。拆分长事务,将复杂业务拆分为多个短事务,必要时通过分布式事务中间件保证一致性(若业务需要)。
4. 数据库层面优化
- 确保实体主键及常用查询字段有索引,让update/delete操作快速定位行,减少锁等待时间;
- 开启PostgreSQL死锁日志,方便排查问题:在
postgresql.conf中设置log_lock_waits = on log_deadlocks = on
内容的提问来源于stack exchange,提问作者Clarence
相关产品推荐
相关产品推荐

