mysql什么情况下会产生死锁

题目详细答案

不同顺序的锁定

当两个或多个事务以不同的顺序请求相同的资源时,容易引发死锁。

1、 事务A先锁定表1的记录,然后尝试锁定表2的记录。

2、 事务B先锁定表2的记录,然后尝试锁定表1的记录。

3、 如果事务A和事务B同时执行,就可能导致死锁。

间隙锁(Gap Lock)

在InnoDB存储引擎中,间隙锁用于防止幻读。在范围查询中,间隙锁可能会导致死锁。

1、 事务A执行SELECT * FROM table WHERE id > 10 FOR UPDATE,锁定了id大于10的所有记录及其间隙。

2、 事务B执行INSERT INTO table (id) VALUES (15),试图插入一个新记录。

3、 如果这两个事务并发执行,可能会导致死锁。

自增列的死锁

在高并发情况下,当多个事务同时插入自增列时,可能会导致死锁。

1、 事务A和事务B同时插入数据到包含自增主键的表中。

2、MySQL在分配自增值时可能会导致锁争用,从而引发死锁。

外键约束

在涉及外键约束的表中,更新或删除操作可能会导致死锁。

1、 事务A在表1中删除一条记录,该表有一个外键引用表2。

2、 事务B在表2中更新或删除与表1中记录相关的记录。

3、 如果这两个事务并发执行,可能会导致死锁。

锁升级

当MySQL从行级锁升级到表级锁时,可能会导致死锁。

1、 事务A和事务B分别锁定同一表中的不同行。

2、 如果某个操作需要将行级锁升级为表级锁,这可能会导致死锁。

混合使用不同类型的锁

在同一个事务中混合使用不同类型的锁(如读锁和写锁)时,可能会导致死锁。

1、事务A持有一个读锁,并试图获取一个写锁。

2、事务B持有一个写锁,并试图获取一个读锁。

3、如果这两个事务并发执行,可能会导致死锁。

大量并发事务

在高并发环境中,大量事务同时操作同一资源,可能会导致死锁。

1、多个事务同时对同一行数据进行更新操作。

2、事务之间相互等待资源释放,可能会导致死锁。

处理和预防死锁的方法

  1. 自动检测和回滚:InnoDB存储引擎能够自动检测死锁,并回滚其中一个事务以解除死锁。
  2. 查看死锁信息:使用命令SHOW ENGINE INNODB STATUS查看最近一次死锁的信息,以帮助诊断问题。
  3. 合理的事务设计:尽量避免长时间持有锁,确保事务尽可能短小和高效。
  4. 一致的锁定顺序:确保所有事务以相同的顺序请求资源,以减少死锁的可能性。
  5. 减少并发事务:通过优化应用程序逻辑,减少同时并发的事务数量。

MySQL 死锁:成因、场景与解决方案全解析

死锁是 MySQL 并发事务中常见的问题,本质是“两个或多个事务互相持有对方需要的锁,且都不愿释放,导致永久等待”。结合你提供的答案,我们从“死锁成因场景”“核心处理手段”“预防最佳实践”三个维度,深入拆解死锁的本质与应对方法。

一、死锁的 7 大典型成因场景

不同场景的死锁,核心都是“锁竞争的循环等待”,但触发条件和表现形式不同,以下结合实际案例详细说明:

1. 不同顺序的锁定(最常见场景)

核心逻辑

两个事务分别锁定“资源 A”和“资源 B”,之后又试图获取对方已锁定的资源,形成“循环等待”。

场景示例

假设存在 订单表(orders)库存表(stock),两个事务并发执行:

  • 事务 A
    1. UPDATE orders SET status=1 WHERE order_id=100;(锁定 orders 表 order_id=100 的行)
    2. 等待执行 UPDATE stock SET num=num-1 WHERE goods_id=200;(需要锁定 stock 表 goods_id=200 的行)
  • 事务 B
    1. UPDATE stock SET num=num-1 WHERE goods_id=200;(已锁定 stock 表 goods_id=200 的行)
    2. 等待执行 UPDATE orders SET status=1 WHERE order_id=100;(需要锁定 orders 表 order_id=100 的行)
死锁原因

事务 A 持有“orders 行锁”,等待“stock 行锁”;事务 B 持有“stock 行锁”,等待“orders 行锁”,双方互相等待,形成死锁。

2. 间隙锁导致的死锁(InnoDB 特有)

核心逻辑

InnoDB 为解决“幻读”, 使用可重复读(RR)隔离级别 ,在范围查询(如 WHERE id > 10)时会锁定“符合条件的行 + 行之间的间隙”(间隙锁)。若另一个事务试图在间隙中插入数据,会触发锁竞争,进而导致死锁。

场景示例

假设 user 表id 列有数据 [5, 10, 15],两个事务并发执行:

  • 事务 ASELECT * FROM user WHERE id > 10 FOR UPDATE;
    (锁定 id>10 的行:id=15,同时锁定间隙 (10,15)、(15,+∞),防止插入新行导致“幻读”)
  • 事务 BINSERT INTO user (id, name) VALUES (12, '张三');
    (试图在间隙 (10,15) 中插入数据,需要获取该间隙的锁,但该间隙已被事务 A 锁定,因此事务 B 等待)
死锁延伸

若此时事务 A 又试图插入 id=13 的数据(需要获取 (12,15) 间隙的锁,但事务 B 已为插入 12 申请了 (10,15) 的锁),则事务 A 等待事务 B,事务 B 等待事务 A,形成死锁。

3. 自增列的死锁(高并发插入场景)

核心逻辑

MySQL 对自增列(AUTO_INCREMENT)会使用“自增锁”(一种特殊的表级锁):事务插入数据时,会先申请自增锁获取自增值,插入完成后释放锁(MySQL 8.0 后优化为“轻量级锁”,但高并发下仍可能冲突)。高并发时,多个事务同时申请自增锁,可能因锁争用导致死锁。

场景示例

order 表order_id 是自增主键,1000 个事务同时执行:

  • 事务 AINSERT INTO order (user_id, amount) VALUES (1, 100);(申请自增锁,获取 order_id=1001)
  • 事务 BINSERT INTO order (user_id, amount) VALUES (2, 200);(申请自增锁,等待事务 A 释放)
死锁原因

MySQL 5.7 及之前,自增锁是“语句级锁”(插入语句执行完才释放),高并发时大量事务排队等待自增锁,若同时存在其他行锁竞争(如插入时触发外键检查),会进一步加剧锁等待,最终形成死锁。

若此时事务 A 的插入操作需要 修改 user 表数据(比如业务逻辑要求 “插入订单后,更新用户余额”):

  • 事务 A:插入 order 表后,继续执行 UPDATE user SET balance=balance-100 WHERE user_id=2;
    1. 需对 useruser_id=2 的行加 X 锁
    2. 若事务 B 此前已对 user_id=2 加了 S 锁(比如事务 B 的插入操作也触发了外键检查,对 user_id=2 加了 S 锁),则事务 A 进入 X 锁等待(持有自增锁和 user_id=1 的 S 锁,等待事务 B 的 S 锁)。
  • 事务 B:此时仍在等待事务 A 的自增锁,同时持有 user_id=2 的 S 锁,等待事务 A 的 S 锁(若事务 B 也需要修改 user_id=1)。
最终形成循环等待(死锁)
  • 事务 A 持有:自增锁、user_id=1 的 S 锁 → 等待事务 B 的 user_id=2 的 S 锁(或 X 锁);
  • 事务 B 持有:user_id=2 的 S 锁 → 等待事务 A 的自增锁,同时可能等待事务 A 的 user_id=1 的 S 锁;
  • 双方互相持有对方需要的锁,且都不愿释放,形成 循环等待—— 死锁触发。

4. 外键约束引发的死锁

核心逻辑

涉及外键的表,更新/删除主表数据时,InnoDB 会自动检查从表的关联数据(需加锁验证);若同时有事务操作从表的关联数据,会导致锁竞争循环。

场景示例

假设 订单表(orders,主表,order_id 为主键)订单详情表(order_item,从表,order_id 为外键关联 orders.order_id)

  • 事务 ADELETE FROM orders WHERE order_id=100;
    (删除主表数据前,需锁定从表中 order_id=100 的关联记录,防止删除期间从表数据被修改)
  • 事务 BUPDATE order_item SET num=2 WHERE order_id=100;
    (已锁定从表 order_id=100 的记录,同时会检查主表 order_id=100 是否存在,需申请主表的锁)
死锁原因

事务 A 等待从表 order_item 的锁(被事务 B 持有),事务 B 等待主表 orders 的锁(被事务 A 持有),形成循环等待。

5. 锁升级导致的死锁

核心逻辑

MySQL 会根据“锁定的行数”动态调整锁粒度:若单个事务锁定同一表的大量行(如超过 innodb_max_rows_locked_for_upgrade 阈值),InnoDB 会将“行级锁”升级为“表级锁”。若此时有其他事务持有该表的行锁,会导致锁冲突。

场景示例

user 表 有 10000 行数据,两个事务并发执行:

  • 事务 AUPDATE user SET status=1 WHERE age > 30;
    (锁定大量行,触发锁升级为表级写锁,等待释放行锁)
  • 事务 BUPDATE user SET status=2 WHERE user_id=100;
    (已锁定 user_id=100 的行,等待事务 A 释放表级锁)
死锁原因

事务 A 升级表级锁时,需等待事务 B 释放行锁;事务 B 执行行更新时,需等待事务 A 释放表级锁,形成死锁。

6. 混合使用不同类型的锁(读锁与写锁冲突)

核心逻辑

MySQL 支持“共享锁(S 锁,读锁)”和“排他锁(X 锁,写锁)”:多个事务可同时持有 S 锁,但 S 锁与 X 锁、X 锁与 X 锁互斥。若事务混合持有 S 锁和 X 锁,会导致锁等待循环。

场景示例
  • 事务 ASELECT * FROM goods WHERE goods_id=200 LOCK IN SHARE MODE;(持有 S 锁,读数据)
    → 后续试图执行 UPDATE goods SET price=100 WHERE goods_id=200;(需要获取 X 锁)
  • 事务 BUPDATE goods SET price=120 WHERE goods_id=200;(已持有 X 锁,写数据)
    → 后续试图执行 SELECT * FROM goods WHERE goods_id=200 LOCK IN SHARE MODE;(需要获取 S 锁)
死锁原因

事务 A 持有 S 锁等待 X 锁(被事务 B 持有),事务 B 持有 X 锁等待 S 锁(被事务 A 持有),互斥等待导致死锁。

7. 大量并发事务导致的死锁(高负载场景)

核心逻辑

高并发环境下(如秒杀、大促),大量事务同时操作同一行/同一表(如库存扣减、订单创建),会导致锁竞争加剧,即使单个事务逻辑简单,也可能因“锁等待链过长”形成死锁。

场景示例

stock 表goods_id=200 的库存仅剩 10 件,100 个事务同时执行:

  • 事务 1~100UPDATE stock SET num=num-1 WHERE goods_id=200;
死锁原因

所有事务都试图获取 goods_id=200 的行级 X 锁,只有 1 个事务能成功,其余 99 个事务排队等待。若此时部分事务还持有其他表的锁(如订单表锁),会形成复杂的锁等待链,最终触发死锁。

二、死锁的核心处理手段

当死锁发生时,MySQL 会通过“自动检测+手动诊断”结合的方式处理,确保系统不陷入永久等待。

1. 自动检测与回滚(InnoDB 内置机制)

InnoDB 会定期(默认每 1 秒)检查“锁等待链”,若发现循环等待(死锁),会选择“代价最小的事务”(如修改行数最少、执行时间最短的事务)进行回滚,释放锁资源,解除死锁。

  • 示例:若事务 A 修改 1 行数据,事务 B 修改 10 行数据,死锁时 InnoDB 会回滚事务 A,让事务 B 继续执行。

2. 手动查看死锁信息(诊断关键)

通过 SHOW ENGINE INNODB STATUS; 命令,可查看最近一次死锁的详细信息,包括:

  • 死锁事务的 ID、执行的 SQL 语句;
  • 事务持有和等待的锁类型(如行锁、间隙锁);
  • 死锁发生的时间、回滚的事务。
示例输出片段(关键部分):
------------------------
LATEST DETECTED DEADLOCK
------------------------
2024-05-20 14:30:00 0x7f1234567890
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 10 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 10, OS thread handle 140705123456789, query id 1234 localhost root updating
UPDATE stock SET num=num-1 WHERE goods_id=200
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 5 page no 3 n bits 72 index PRIMARY of table `test`.`stock` trx id 12345 lock_mode X locks rec but not gap waiting

*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 8 sec starting index read
mysql tables in use 1, locked 1
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 11, OS thread handle 140705123456790, query id 1235 localhost root updating
UPDATE stock SET num=num-1 WHERE goods_id=200
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 5 page no 3 n bits 72 index PRIMARY of table `test`.`stock` trx id 12346 lock_mode X locks rec but not gap

从输出中可明确:两个事务都在更新 stockgoods_id=200 的行,互相等待对方的 X 锁,导致死锁。

三、死锁的预防最佳实践(从根源减少死锁)

处理死锁的核心是“预防”,通过优化事务逻辑和锁策略,从根源减少死锁发生的概率。

1. 保证一致的锁定顺序(解决“不同顺序锁定”问题)

所有事务对多个资源(表、行)的锁定,必须遵循“统一顺序”。

  • 示例:操作 ordersstock 表时,所有事务都先锁定 orders 表,再锁定 stock 表,避免循环等待。

2. 缩短事务时长,减少锁持有时间

事务中只包含“必要的 SQL 操作”,避免在事务中执行非数据库操作(如 RPC 调用、日志打印),减少锁的持有时间。

  • 反例:事务中先执行 UPDATE,再调用第三方支付接口(耗时 5 秒),最后执行 COMMIT——期间锁一直被持有,增加死锁风险。
  • 正例:先调用支付接口,确认支付成功后,再开启事务执行 UPDATECOMMIT,事务耗时控制在毫秒级。

3. 避免范围查询,减少间隙锁(解决“间隙锁死锁”)

尽量用“精确查询”(如 WHERE id=100)替代“范围查询”(如 WHERE id > 100),避免 InnoDB 加间隙锁。

  • 若必须用范围查询,可将隔离级别从“可重复读(RR)”降为“读已提交(RC)”——RC 级别下,InnoDB 不会加间隙锁,仅锁定已存在的行,减少锁冲突。

4. 优化自增列配置(解决“自增列死锁”)

  • MySQL 8.0+:默认使用“持久化自增锁”(轻量级锁,获取自增值后立即释放,而非语句结束后释放),大幅减少锁争用;
  • 避免在高并发插入场景中,让自增表同时触发外键检查或触发器——可提前预处理数据,减少事务内的额外锁操作。

5. 减少锁升级(解决“锁升级死锁”)

  • 避免单个事务锁定大量行:若需更新大量数据,可拆分为“小批量更新”(如每次更新 100 行,分多次执行),避免触发锁升级;
  • 调整 innodb_max_rows_locked_for_upgrade 参数(默认 1000),提高锁升级的阈值(需谨慎,避免行锁过多占用内存)。

6. 合理使用锁类型,避免混合锁冲突

  • 仅在“需要共享读数据”的场景使用共享锁(LOCK IN SHARE MODE),普通查询用默认的“快照读”(不加锁);
  • 若事务中需要从“读”转为“写”,直接用排他锁(FOR UPDATE),避免先加 S 锁再申请 X 锁的冲突。

四、总结

死锁的本质是“锁竞争的循环等待”,并非 MySQL 的 bug,而是并发事务中“锁策略”与“业务逻辑”共同作用的结果。处理死锁的关键是:

  1. 诊断:通过 SHOW ENGINE INNODB STATUS 定位死锁的事务和 SQL,找到锁竞争的根源;
  2. 预防:从“锁定顺序、事务时长、锁类型、查询方式”四个维度优化,减少锁冲突的概率;
  3. 兜底:依赖 InnoDB 的自动死锁检测与回滚,确保死锁发生后系统能快速恢复,不影响整体可用性。

在实际业务中(如电商秒杀、金融交易),需结合具体场景选择优化方案,平衡“数据一致性”和“并发性能”,才能有效规避死锁问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

埃泽漫笔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值