DB2带FK约束的Delete操作死锁问题排查与解决咨询
核心原因:无索引导致锁范围扩大 + 并发锁顺序不一致
外键无索引引发全表扫描与锁范围扩大
由于Movement_encoded_names.movement_id未建索引,删除Movement记录时,数据库为验证外键约束(确保无子表记录引用待删除主表行),会对Movement_encoded_names执行全表扫描。这会导致数据库锁住整个数据页(即日志中的PAGEID=141),而非仅锁住与当前Movement关联的子表行——原本无关的行(ROWID=132、187)也被纳入锁范围,大幅增加锁冲突概率。并发事务的锁获取顺序不一致形成循环等待
多机并发删除时,不同事务的操作顺序存在差异:- 部分事务先尝试删除
Movement主表行(如参与者1、3),扫描子表时需要锁住数据页内的多行; - 部分事务先删除
Movement_encoded_names子表行(如参与者2、4),同样会锁住数据页内的行。
当事务A持有行132的锁并等待行187的锁,事务B持有行187的锁并等待行132的锁,结合多实例并发场景,就形成了日志中1→2→3→4→1的循环等待链,触发死锁。
- 部分事务先尝试删除
1. 数据库层面:添加外键索引(必做)
给Movement_encoded_names.movement_id添加索引,这是解决问题的基础:
CREATE INDEX idx_movement_encoded_names_movement_id ON Movement_encoded_names(movement_id);
添加索引后,数据库可快速定位关联子表行,无需全表扫描,锁范围会缩小到仅关联行,从根源降低锁冲突的可能。
2. 代码层面优化方案
方案一:统一删除顺序,避免锁顺序混乱
手动控制删除流程,先删子表,再删主表,且确保子表删除按固定顺序执行(如按主键升序):
@Transactional public void deleteByMovementNumber(String movementNumber) { // 查询目标Movement Movement movement = movementRepository.findByMovementNumber(movementNumber); if (movement == null) { return; } // 将关联子表记录按主键升序排序,确保所有事务删除顺序一致 List<MovementEncodedNames> encodedNames = movement.getEncodedNames() .stream() .sorted(Comparator.comparing(MovementEncodedNames::getId)) .collect(Collectors.toList()); // 先删除所有子表记录 movementEncodedNamesRepository.deleteAll(encodedNames); // 最后删除主表记录 movementRepository.delete(movement); }
固定删除顺序后,所有事务都会按相同顺序获取锁,不会出现循环等待的情况。
方案二:使用批量删除替代级联删除
放弃JPA的级联删除逻辑,直接执行批量SQL操作,减少多次单条删除带来的锁竞争:
@Transactional public void deleteByMovementNumber(String movementNumber) { // 批量删除关联子表记录 movementEncodedNamesRepository.deleteByMovementMovementNumber(movementNumber); // 批量删除主表记录 movementRepository.deleteByMovementNumber(movementNumber); }
对应的Repository方法:
public interface MovementEncodedNamesRepository extends JpaRepository<MovementEncodedNames, Long> { @Modifying @Query("DELETE FROM MovementEncodedNames e WHERE e.movement.movementNumber = ?1") void deleteByMovementMovementNumber(String movementNumber); }
批量操作的锁粒度更可控,且避免了JPA级联时的多次单条删除锁竞争。
方案三:调整JPA映射配置(辅助优化)
可以将@OneToMany的fetch模式改为Fetch.EAGER(仅适合关联数据量小的场景),避免延迟加载时在事务内额外触发查询带来的锁问题;同时确认orphanRemoval = true是否必要——如果业务上不需要自动删除从集合中移除的子表记录,可以关闭该配置,减少不必要的锁操作。
内容的提问来源于stack exchange,提问作者rhbc73

