You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DB2带FK约束的Delete操作死锁问题排查与解决咨询

死锁原因分析

核心原因:无索引导致锁范围扩大 + 并发锁顺序不一致

  1. 外键无索引引发全表扫描与锁范围扩大
    由于Movement_encoded_names.movement_id未建索引,删除Movement记录时,数据库为验证外键约束(确保无子表记录引用待删除主表行),会对Movement_encoded_names执行全表扫描。这会导致数据库锁住整个数据页(即日志中的PAGEID=141),而非仅锁住与当前Movement关联的子表行——原本无关的行(ROWID=132、187)也被纳入锁范围,大幅增加锁冲突概率。

  2. 并发事务的锁获取顺序不一致形成循环等待
    多机并发删除时,不同事务的操作顺序存在差异:

    • 部分事务先尝试删除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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 00:06:10