首页/发现新知
视界 · 深度阅读

数据库死锁的成因与代码规避方案详解

死锁是由数据库中的竞争的并发锁引起的,通常是在多步骤事务中,这篇文章主要介绍了数据库死锁的成因与代码规避方案的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

目录
  • 一、什么是数据库死锁
  • 二、死锁的典型成因
  • 三、复现一个死锁场景
  • 四、代码层面的规避方案
    • 4.1 统一加锁顺序
    • 4.2 缩短事务并控制锁粒度
    • 4.3 失败重试与降级
  • 五、排查与监控建议

    一、什么是数据库死锁

    死锁是指两个或多个事务在执行过程中,因争夺锁资源而相互等待,导致所有事务都无法继续推进的状态。数据库通常通过等待图检测死锁,一旦发现环路,就会主动回滚其中一个事务(牺牲者),让其余事务继续执行。因此死锁并不是“数据库坏了”,而是并发访问下的一种必然可能,关键在于我们能否在代码层面降低发生概率。

    死锁的四个必要条件可以简单概括为:互斥、持有并等待、不可剥夺、循环等待。数据库无法消除互斥,但应用代码可以通过统一加锁顺序来打破“循环等待”,这是最有效的规避手段。

    二、死锁的典型成因

    • 加锁顺序不一致:事务 A 先锁记录 1 再锁记录 2,事务 B 先锁记录 2 再锁记录 1,形成环路。
    • 间隙锁与范围更新:在可重复读隔离级别下,范围条件更新会加间隙锁,不同事务的插入位置交叉时容易互相阻塞。
    • 长事务持有锁过久:事务中夹杂远程调用、文件 IO 等耗时操作,锁持有时间被放大。
    • 索引缺失导致锁范围扩大:没有命中索引的更新会扫描并锁定大量行,显著提高冲突概率。

    三、复现一个死锁场景

    下面用两张账户表模拟转账,两个事务以相反顺序更新账户,这是最经典的死锁模型。

    -- 事务 A
    BEGIN;
    UPDATE account SET balance = balance - 100 WHERE id = 1;
    -- 此处暂停,等待事务 B 执行
    UPDATE account SET balance = balance + 100 WHERE id = 2;
    COMMIT;
    
    -- 事务 B(与 A 并发执行)
    BEGIN;
    UPDATE account SET balance = balance - 100 WHERE id = 2;
    UPDATE account SET balance = balance + 100 WHERE id = 1;
    COMMIT;

    执行后数据库会返回类似 Deadlock found when trying to get lock; try restarting transaction 的错误,说明检测到环路并回滚了其中一个事务。

    四、代码层面的规避方案

    4.1 统一加锁顺序

    转账时始终按账户 ID 升序加锁,让所有事务的加锁路径一致,从根本上消除循环等待。

    public void transfer(long fromId, long toId, BigDecimal amount) {
        long first = Math.min(fromId, toId);
        long second = Math.max(fromId, toId);
        // 固定顺序加锁,避免交叉等待
        accountMapper.lockById(first);
        accountMapper.lockById(second);
        accountMapper.decrease(fromId, amount);
        accountMapper.increase(toId, amount);
    }

    4.2 缩短事务并控制锁粒度

    把远程调用、消息发送等非数据库操作移出事务边界,事务内只保留必要的 SQL,同时确保更新条件命中索引。

    @Transactional(timeout = 3)
    public void settleOrder(Long orderId) {
        // 事务内只做数据库操作
        orderMapper.updateStatus(orderId, "PAID");
        stockMapper.deductByOrderId(orderId);
        // 通知逻辑放到事务提交后执行
    }
    -- 为更新条件建立索引,避免全表扫描扩大锁范围
    ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);

    4.3 失败重试与降级

    死锁被数据库回滚后,业务侧应识别该异常并进行有限次重试,而不是直接抛给用户。

    public void executeWithRetry(Runnable action, int maxRetry) {
        for (int i = 0; i < maxRetry; i++) {
            try {
                action.run();
                return;
            } catch (DeadlockLoserDataAccessException e) {
                if (i == maxRetry - 1) {
                    throw e;
                }
                // 退避后重试,降低再次冲突概率
                sleepQuietly(50L * (i + 1));
            }
        }
    }

    五、排查与监控建议

    线上出现死锁时,优先查看数据库的死锁日志(如 MySQL 的 SHOW ENGINE INNODB STATUS),定位具体 SQL 与加锁顺序。再结合慢查询日志确认是否存在长事务或缺失索引。建议在应用层对死锁异常做埋点统计,当某条链路死锁频率上升时及时告警。

    总结来说,死锁无法彻底消除,但通过统一加锁顺序、缩短事务、补齐索引和合理重试,可以把发生概率压到很低,并让系统在偶发死锁时具备自愈能力。

    到此这篇关于数据库死锁的成因与代码规避方案的文章就介绍到这了,更多相关数据库死锁成因与规避内容请搜索爱读视界以前的文章或继续浏览下面的相关文章希望大家以后多多支持爱读视界!

    您可能感兴趣的文章:

    • mysql 数据库死锁原因及解决办法
    • MySQL数据库的一次死锁实例分析
    • 讲解Oracle数据库中结束死锁进程的一般方法
    • SQL Server数据库的死锁详细说明
    • SQL Server数据库死锁处理超详细攻略
    • 关于MySQL数据库死锁的案例和解决方案
    读到这里,发现更多一点。探索更多内容 ↗