MySQL 什么情况下会产生死锁?为什么会死锁?以及 Online DDL 解决了什么问题

前言

死锁不是"偶发事故",而是设计问题

在实际业务中,很多同学第一次接触 MySQL 死锁,往往是在日志中看到一句:

复制代码
Deadlock found when trying to get lock; try restarting transaction

然后的反应通常是:

  • "是不是 MySQL 的 bug?"
  • "加索引能不能解决?"
  • "把事务拆小一点行不行?"

事实上,死锁不是 MySQL 的 bug,而是 InnoDB 锁设计 + 业务访问模式共同作用的必然结果

本文将从一个非常真实的业务场景出发,深入剖析:

  • MySQL 在什么情况下会产生死锁
  • InnoDB 各类锁是如何协同工作的
  • 为什么 SELECT ... FOR UPDATE + INSERT 特别容易死锁
  • 幻读与 Next-Key Lock 在其中扮演了什么角色
  • Online DDL 到底解决了什么问题(以及没解决什么)

一、一个非常典型的业务场景

场景描述:资源唯一性校验

假设有一张表 resource,业务语义是:某个资源只能被创建一次

sql 复制代码
CREATE TABLE resource (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  resource_key VARCHAR(64) NOT NULL,
  UNIQUE KEY uk_resource_key (resource_key)
) ENGINE=InnoDB;

常见的业务代码逻辑是:

sql 复制代码
BEGIN;

SELECT * FROM resource 
WHERE resource_key = 'A'
FOR UPDATE;

-- 如果不存在
INSERT INTO resource(resource_key) VALUES ('A');

COMMIT;

乍一看,这段逻辑非常严谨

  • 使用事务
  • 使用 SELECT FOR UPDATE 防止并发插入
  • 有唯一索引兜底

但在高并发下,这正是死锁高发现场

二、InnoDB 锁体系总览(先有全局认知)

在分析死锁之前,我们必须先明确 InnoDB 中到底有哪些锁。

记录锁(Record Lock)

  • S 型记录锁(共享锁)

    • 多个事务可以同时持有

    • 用于普通 SELECT

  • X 型记录锁(排他锁)

    • 同一条记录只能有一个事务持有

    • 用于 UPDATE / DELETE / SELECT FOR UPDATE

记录锁只锁已经存在的记录

间隙锁(Gap Lock)

  • 锁定的是索引记录之间的"区间"
  • 不锁具体记录,只锁"范围"
  • 用来防止 幻读

例如索引中存在:

sql 复制代码
(10) ------ (20)

Gap Lock 可以锁住 (10, 20) 这个区间。

Next-Key Lock(临键锁)

InnoDB 中最重要、也是最容易导致死锁的锁

  • = 记录锁 + 间隙锁
  • 锁定区间:(prev_key, current_key]
  • 是 InnoDB RR(可重复读)隔离级别的默认行为

SELECT ... FOR UPDATE

索引范围查询 下,几乎一定会触发 Next-Key Lock。

插入意向锁(Insert Intention Lock)

  • 一种 特殊的间隙锁
  • S 型锁
  • 用于表示:

"我想在这个 gap 里插入一条记录"

重点:
插入意向锁与 Gap Lock / Next-Key Lock 是冲突的

意向锁(Intention Lock)

  • 表级锁(IS / IX)
  • 表示事务"将要"在某些记录上加锁
  • 用于提高锁冲突判断效率
  • 不直接参与死锁,但几乎所有行锁都会先加意向锁

隐式锁 & 显式锁

  • 隐式锁

    插入时,事务尚未真正加行锁,但通过事务 ID 隐含保护

  • 显式锁

    当其他事务需要访问这条记录时,隐式锁会升级为显式锁

隐式锁升级的过程,是很多死锁的导火索

元数据锁(MDL)

  • 控制表结构的并发访问
  • DDL / DML 之间的互斥
  • 是 Online DDL 要重点解决的问题(后文详述)

三、死锁是如何一步一步发生的?

下面我们用 事务 A / 事务 B 复盘整个死锁过程。

第一步:两个事务同时进入

sql 复制代码
事务 A                             事务 B
BEGIN;                             BEGIN;

第二步:SELECT FOR UPDATE

sql 复制代码
SELECT * FROM resource 
WHERE resource_key = 'A'
FOR UPDATE;

假设此时表中 还没有 resource_key = 'A' 的记录

会发生什么?

  • InnoDB 在 唯一索引 uk_resource_key
  • A 所在的索引区间加上 X 型 Next-Key Lock
  • 锁住的是一个"未来可能插入 A 的区间"

结果是:

sql 复制代码
事务 A:持有 Next-Key Lock(X)
事务 B:也尝试加同一个 Next-Key Lock(X)

注意:
Next-Key Lock 是互斥的

所以:

  • 事务 A 成功
  • 事务 B 被阻塞,等待 A 释放锁

第三步:事务 A 执行 INSERT

sql 复制代码
INSERT INTO resource(resource_key) VALUES ('A');

此时发生了一个非常关键的事情:

插入位置落在 已被 Next-Key Lock 锁住的 gap

InnoDB 会尝试:

  • 将插入操作的 隐式锁

  • 转换为 显式锁

同时需要获取一个 插入意向锁(S 型)

第四步:锁冲突出现

关键冲突点来了:

  • 插入意向锁(S)
  • 与事务 B 持有的 Next-Key Lock(X) 冲突

于是:

sql 复制代码
事务 A:等待 B 释放 Next-Key Lock
事务 B:等待 A 释放 Next-Key Lock

循环等待成立,死锁产生

InnoDB 如何处理?

  • InnoDB 会通过 死锁检测线程
  • 选择 代价较小的事务回滚
  • 抛出死锁异常

四、这类死锁的本质原因是什么?

总结一句话:

Next-Key Lock + 插入意向锁 + 并发存在性检查 = 死锁温床

更具体地说:

  1. SELECT FOR UPDATE 在 RR 下锁的是"范围",不是"记录"
  2. 间隙锁之间是兼容的
  3. 插入时需要插入意向锁
  4. 插入意向锁与 Next-Key Lock 冲突
  5. 多事务互相等待,形成环

五、幻读与 Next-Key Lock 的关系

什么是幻读?

sql 复制代码
事务 A:第一次查询没有记录
事务 B:插入一条新记录
事务 A:第二次查询发现"多了一条"

这就是幻读。

InnoDB 如何解决幻读?

  • RR 隔离级别下
  • 使用 Next-Key Lock
  • 锁住"可能出现新记录的范围"

解决幻读的代价,就是更复杂的锁冲突模型

六、Online DDL 解决了什么问题?

传统 DDL 的问题

在早期 MySQL 版本中:

sql 复制代码
ALTER TABLE resource ADD COLUMN ext VARCHAR(64);

会:

  • 持有 排他 MDL 锁

  • 阻塞所有:

  • SELECT

  • INSERT

  • UPDATE

  • DELETE

对线上业务是 灾难性的

Online DDL 的核心目标

Online DDL 的目标不是"无锁",而是:

最小化 DDL 对 DML 的影响

Online DDL 做了哪些事情?

MDL 锁拆分

  • 大部分时间使用共享 MDL
  • 只在最终切换阶段使用短暂排他锁

拷贝/重建过程异步化

  • 后台重建数据
  • 前台继续处理读写

引入 inplace / instant 算法

  • MySQL 8.0 中大量 DDL 不再重建表

Online DDL 能解决死锁吗?

不能

Online DDL 解决的是:

  • 表结构变更期间的 阻塞问题
  • MDL 导致的长时间不可用

但它:

  • 不会改变 InnoDB 行锁模型
  • 不会避免 Next-Key Lock
  • 不会消除业务逻辑导致的死锁

七、如何在业务层面规避这类死锁?

设计层面

避免:

sql 复制代码
SELECT ... FOR UPDATE
+ INSERT

改为:

  • 直接 INSERT
  • 依赖唯一索引
  • 捕获重复键异常

降低锁范围

  • 精确命中唯一索引
  • 避免范围扫描
  • 减少 Next-Key Lock 触发概率

事务控制

  • 事务尽量短
  • 不要在事务中做无关操作
  • 固定访问顺序,减少交叉等待

总结

为什么会死锁?

  • InnoDB 为了解决幻读,引入了 Next-Key Lock

  • Next-Key Lock + 插入意向锁存在天然冲突

  • 并发事务在特定业务模式下形成循环等待

Online DDL 的定位

  • 解决的是 DDL 阻塞问题

  • 不是行锁死锁的"银弹"

一句话结论

MySQL 死锁不是偶然,而是锁模型与业务访问模式的必然结果

理解锁,才能真正写出高并发安全的 SQL。

相关推荐
麦麦鸡腿堡2 小时前
MySQL表的操作指令与常用数值类型
数据库·mysql
千寻技术帮3 小时前
10379_基于SSM的校园跑腿服务平台
mysql·微信小程序·校园跑腿·ssm
surtr14 小时前
数据库基础(数据库原理和应用)
数据库·sql·mysql·oracle·database
「光与松果」4 小时前
MySQL中统计各个IP的连接数
数据库·mysql
苹果醋34 小时前
Java设计模式实战:从面向对象原则到架构设计的最佳实践
java·运维·spring boot·mysql·nginx
boy快快长大4 小时前
【MySQL】InnoDB记录存储结构
数据库·mysql
初恋叫萱萱5 小时前
基于CodeRider-Kilo和MySQL开发一款书店管理系统
数据库·mysql·产品运营
图乐aj5 小时前
MySQL 运维之日常运维篇 二
运维·mysql
qq_366336375 小时前
mysql-5.7.38-winx64.zip 启动教程(免安装版)
数据库·mysql·adb