十分想念顺店杂可。。。

MySQL 死锁是并发场景中常见的问题,本质是两个或多个事务相互持有对方需要的锁,且都在等待对方释放锁,形成循环等待。解决死锁需从预防检测处理三个层面入手,以下是具体方案:

一、死锁的检测方法

首先需要确认死锁发生的具体场景,常用工具和命令:

  1. 查看 InnoDB 死锁日志
    执行 SHOW ENGINE INNODB STATUS;,在输出结果的LATEST DETECTED DEADLOCK部分可查看最近一次死锁的详细信息,包括:

    • 参与死锁的事务 ID、SQL 语句
    • 事务持有和等待的锁类型(行锁、表锁等)
    • 锁定的资源(行记录、索引等)
  2. 开启死锁日志记录
    my.cnf中配置,持久化记录死锁日志:

    innodb_print_all_deadlocks = 1  # 记录所有死锁(默认只记录最近一次)
    log_error = /var/log/mysql/error.log  # 日志存储路径
    

二、死锁产生的常见原因

  1. 事务操作顺序不一致
    多个事务操作相同表 / 行时,操作顺序不同会导致循环等待。
    例:事务 A 先更新表 1 再更新表 2,事务 B 先更新表 2 再更新表 1,可能形成死锁。

  2. 锁粒度不合理

    • 未使用索引导致行锁升级为表锁(InnoDB 行锁基于索引,无索引时会锁定全表)。
    • 范围查询(如WHERE id > 100)产生间隙锁(Gap Lock),扩大锁定范围。
  3. 事务持有锁时间过长
    事务中包含大量操作(如复杂计算、远程调用),导致锁长期不释放,增加冲突概率。

  4. 隔离级别不当
    较高的隔离级别(如REPEATABLE READ)会产生更多锁(如间隙锁),死锁概率更高。

三、解决死锁的核心方案

1. 统一事务操作顺序(最有效)

确保所有事务操作相同资源(表、行)时,严格遵循相同的顺序,避免循环等待。
例:所有事务必须先操作表 A,再操作表 B,最后操作表 C。

-- 错误示例(顺序相反导致死锁)
-- 事务A
UPDATE table1 SET col=1 WHERE id=1;  -- 持有table1的锁
UPDATE table2 SET col=1 WHERE id=1;  -- 等待table2的锁(被事务B持有)

-- 事务B
UPDATE table2 SET col=1 WHERE id=1;  -- 持有table2的锁
UPDATE table1 SET col=1 WHERE id=1;  -- 等待table1的锁(被事务A持有)

-- 正确示例(统一顺序)
-- 事务A和事务B都先操作table1,再操作table2
UPDATE table1 SET col=1 WHERE id=1;
UPDATE table2 SET col=1 WHERE id=1;
2. 减小事务范围,缩短锁持有时间
  • 事务只包含必要的 SQL 操作,避免无关逻辑(如计算、日志打印)。
  • 拆分大事务为多个小事务,减少单次锁定的资源量。
-- 优化前(大事务,锁持有久)
BEGIN;
UPDATE order SET status=1 WHERE id=100;  -- 锁定订单
SELECT * FROM user WHERE id=1;  -- 无关查询,延长锁持有时间
UPDATE log SET content='xxx' WHERE order_id=100;  -- 非核心操作
COMMIT;

-- 优化后(小事务,仅保留核心操作)
BEGIN;
UPDATE order SET status=1 WHERE id=100;  -- 核心操作,快速提交
COMMIT;

-- 非核心操作单独处理(无需锁定订单)
BEGIN;
UPDATE log SET content='xxx' WHERE order_id=100;
COMMIT;
3. 优化索引和查询,减少锁冲突
  • 确保查询使用索引:避免无索引导致的全表锁(InnoDB 行锁依赖索引)。
  • 避免锁定不必要的行:使用精确的WHERE条件,减少锁定范围;避免SELECT ... FOR UPDATE锁定过多行。
  • 慎用范围查询:范围查询(如id > 100)会产生间隙锁,可改用精确查询或降低隔离级别。

-- 错误示例(无索引导致表锁)
UPDATE user SET name='xxx' WHERE phone='13800138000';  -- phone无索引,锁定全表

-- 正确示例(添加索引,仅锁单行)
ALTER TABLE user ADD INDEX idx_phone(phone);  -- 为phone添加索引
UPDATE user SET name='xxx' WHERE phone='13800138000';  -- 仅锁定符合条件的行
4. 调整事务隔离级别

InnoDB 默认隔离级别为REPEATABLE READ,该级别会产生间隙锁(防止幻读),死锁概率较高。若业务允许,可降低至READ COMMITTED

  • 减少间隙锁的使用(仅在外键约束和唯一性检查时保留)。
  • 释放锁更快(非匹配行的锁会提前释放)。
-- 临时修改当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 永久修改(my.cnf)
transaction-isolation = READ-COMMITTED
5. 应用层重试机制

死锁发生后,MySQL 会自动回滚其中一个事务(“牺牲品”),应用程序可捕获1213错误码(死锁错误),重试事务:

// Java示例:捕获死锁错误并重试
int maxRetries = 3;
int retryCount = 0;
while (retryCount < maxRetries) {
    try {
        // 执行事务逻辑
        executeTransaction();
        break;
    } catch (SQLException e) {
        // 1213是MySQL死锁错误码
        if (e.getErrorCode() == 1213) {
            retryCount++;
            // 短暂休眠后重试(避免立即重试再次冲突)
            Thread.sleep(100 * retryCount);
        } else {
            // 处理其他错误
            throw e;
        }
    }
}

6. 其他辅助手段
  • 使用表锁代替行锁:在高并发且表数据量小的场景,可主动使用LOCK TABLES强制表锁(需谨慎,会降低并发)。
  • 监控死锁趋势:通过performance_schema或第三方工具(如 Prometheus)监控死锁频率,及时发现异常。

总结

解决 MySQL 死锁的核心原则是:减少锁冲突概率 + 快速处理不可避免的冲突。通过统一操作顺序、优化事务和索引、调整隔离级别等预防措施,结合日志监控和重试机制,可有效降低死锁对业务的影响。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐