背景

生产环境突然出现用户登录不了的问题,通过排除 mysql 数据库出现死锁,通过查询日志

查看 MySQL InnoDB 引擎内部运行状态的诊断报告

show engine INNODB STATUS;

将日志发送给 chatGPT 分析死锁问题。

在高并发系统中,MySQL 死锁是非常常见的问题。本文结合真实 InnoDB 状态日志,从死锁现象、产生原因到解决方案进行系统性分析。


一、什么是死锁?

死锁(Deadlock)指多个事务互相等待对方释放资源,形成循环等待,最终导致事务无法继续执行。

典型模型:

事务A 等待 事务B
事务B 等待 事务A

MySQL InnoDB 检测到死锁后,会自动回滚其中一个事务来解除死锁。



二、案例背景

本次死锁出现在:

collaboration 表

核心 SQL:

INSERT INTO collaboration (...)
VALUES (...)

主键格式类似:

HZ202603060027
HZ202603060029

属于:

✅ 业务递增字符串主键
❌ 非数值自增主键



三、死锁日志核心信息

从 InnoDB 状态可以看到:

① 两个事务同时执行插入

事务1:

TRANSACTION 768989781
INSERT INTO collaboration

事务2:

TRANSACTION 768990060
INSERT INTO collaboration

② 两个事务都持有 S 锁

日志中出现:

lock mode S

说明:

  • S锁是共享锁

  • 多个事务可以同时持有

但问题是:

👉 两个事务都在等待升级为 X 锁。



四、死锁形成的核心原因

⭐ 根本原因:热点页竞争

本案例中:

所有 INSERT 都发生在 B+Tree 最右侧页

日志中出现:

supremum

supremum 是 InnoDB 索引页的最大边界虚拟记录。


B+Tree 结构示意

[记录1]
[记录2]
[记录3]
[SUPREMUM]

新数据会不断插入:

SUPREMUM 左侧

如果高并发写入:

多个事务会同时:

  • 定位插入位置

  • 申请锁

  • 尝试升级为写锁

从而形成锁竞争。



五、为什么 INSERT 也会产生死锁?

很多人误以为:

INSERT 不会死锁

这是错误的。

InnoDB 插入时通常涉及:

Record Lock
Gap Lock
Next-Key Lock
Insert Intention Lock

在高并发场景下:

多个事务可能:

同时持有 S 锁
同时尝试升级 X 锁

形成循环等待。



六、为什么自增 ID 可以降低死锁概率?

自增 ID 能有效减少死锁,核心原因不是“也往最后插入”,而是:

⭐ 数据库统一分配 ID

MySQL 通过自增锁机制:

先发号
再写入

避免多个事务:

同时竞争同一个插入位置

⭐ 更重要的是:分散索引页写入

自增主键会:

  • 减少热点页竞争

  • 降低 gap lock 争抢

  • 提高并发写入性能



七、为什么业务递增字符串主键更容易死锁?

例如:

HZ + 日期 + 序列号

存在几个问题:

① 业务系统自己生成 ID

多个应用实例可能:

同时生成相近 ID

然后一起写数据库。


② 字符串排序按字典序

MySQL 索引排序:

按字符比较
而不是时间逻辑

导致:

所有数据集中在索引树右侧

形成热点页。



八、解决死锁的最佳实践(重要 ⭐⭐⭐⭐⭐)

✅ 方案一(最推荐)

改用:

BIGINT AUTO_INCREMENT

同时:

业务ID单独存储

这是大多数互联网系统标准方案。



✅ 方案二:分布式ID

使用:

  • Snowflake ID

  • 类雪花算法

特点:

趋势递增
但不完全连续

能减少锁冲突。



✅ 方案三:短事务原则

避免:

一个事务中包含:
查询 + 业务逻辑 + 写入

应该拆成:

查询
业务计算
写入(短事务)

九、总结(最重要)

本案例死锁的本质是:

高并发 INSERT
+
递增字符串主键
+
B+Tree 索引热点页竞争

最终导致:

事务间循环等待

最有效的解决方案是:

改用 BIGINT 自增主键
或 Snowflake 分布式 ID


十、一句话记住死锁本质

MySQL 死锁不是因为锁太多,而是因为多个事务同时争抢同一个数据结构热点位置。



Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐