MySQL Next-Key Lock 锁表事故全拆解(从现象到根治)
- [🚨 线上事故背景](#🚨 线上事故背景)
- [💥 出现了什么问题?](#💥 出现了什么问题?)
- [🔥 问题 SQL(事故源头)](#🔥 问题 SQL(事故源头))
- [🧨 看似无害,实际致命](#🧨 看似无害,实际致命)
- [🧠 锁是怎么加的?(重点)](#🧠 锁是怎么加的?(重点))
-
- [🔒 实际锁的范围](#🔒 实际锁的范围)
- [❌ 锁表事故是怎么发生的?](#❌ 锁表事故是怎么发生的?)
-
-
- [事务 A(定时任务)](#事务 A(定时任务))
- [事务 B(正常业务)](#事务 B(正常业务))
- [事务 C](#事务 C)
-
- [🧩 为什么 LIMIT 1 没救你?](#🧩 为什么 LIMIT 1 没救你?)
- [🔍 线上如何确认?(实战)](#🔍 线上如何确认?(实战))
- [🧨 根因总结(事故定性)](#🧨 根因总结(事故定性))
- [✅ 解决方案](#✅ 解决方案)
-
- [方案 1️⃣:改为唯一索引](#方案 1️⃣:改为唯一索引)
- [方案 2️⃣:降级隔离级别为 RC](#方案 2️⃣:降级隔离级别为 RC)
- [方案 3️⃣:两段式处理(最常用)](#方案 3️⃣:两段式处理(最常用))
- [方案 4️⃣:加 SKIP LOCKED(MySQL 8+)](#方案 4️⃣:加 SKIP LOCKED(MySQL 8+))
- [🧠 一句话总结](#🧠 一句话总结)
🚨 线上事故背景
表结构(简化)
sql
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
status INT,
create_time DATETIME,
KEY idx_status (status)
) ENGINE=InnoDB;
- 表数据量:3000 万
- 隔离级别:RR(默认)
- 业务特点:高并发下单 + 状态更新
💥 出现了什么问题?
现象
- 大量请求卡死
- QPS 掉到个位数
- 数据库 CPU 不高,但连接数飙升
INSERT orders、UPDATE orders全部阻塞
🔥 问题 SQL(事故源头)
sql
-- 定时任务,每 5 分钟跑一次
START TRANSACTION;
SELECT id
FROM orders
WHERE status = 0
ORDER BY create_time
LIMIT 1
FOR UPDATE;
-- 后续逻辑...
🧨 看似无害,实际致命
很多人第一反应:
"就锁一条 status=0 的订单啊?"
但实际上不是。
🧠 锁是怎么加的?(重点)
关键事实
status是 非唯一索引- 隔离级别是 RR
- 使用了 FOR UPDATE(当前读)
👉 必然触发 Next-Key Lock
🔒 实际锁的范围
假设 idx_status 中:
status 索引分布:
0 | 0 | 0 | 0 | 1 | 2 | 3 ...
InnoDB 会加锁:
(-∞, 0] ← 整个 status=0 区间 + 前间隙
📌 即使 LIMIT 1,锁也不会变小。
❌ 锁表事故是怎么发生的?
事务 A(定时任务)
- 锁住 所有 status=0 的记录
- 还锁住 status=0 前的 gap
事务 B(正常业务)
sql
INSERT INTO orders (status, ...)
VALUES (0, ...);
🚫 被 gap lock 阻塞
事务 C
sql
UPDATE orders SET status=1 WHERE id=xxx;
🚫 被 record lock 阻塞
📌 最终效果:
所有"未支付订单"相关操作全被卡死
👉 看起来就像"整张表被锁"
🧩 为什么 LIMIT 1 没救你?
原因
- InnoDB 的锁是 按索引范围加
- 不是按返回行数
LIMIT只影响结果集,不影响锁范围
🔍 线上如何确认?(实战)
sql
SELECT
object_name,
lock_type,
lock_mode,
lock_data
FROM performance_schema.data_locks;
你会看到类似:
INDEX idx_status
X,GAP
lock_data = 0
👉 Next-Key Lock 实锤
🧨 根因总结(事故定性)
| 项目 | 结论 |
|---|---|
| 根因 | 非唯一索引 + RR + FOR UPDATE |
| 本质 | Next-Key Lock 锁范围过大 |
| 表象 | 大量事务阻塞,看似锁表 |
| 误区 | 以为 LIMIT 会缩小锁 |
非唯一 + RR + 当前读 = 临键锁;
LIMIT 不减锁;
锁表八成是索引问题。
✅ 解决方案
方案 1️⃣:改为唯一索引
sql
-- 设计为"只允许一个待处理订单"
UNIQUE KEY uk_status (status)
👉 等值查询退化为 Record Lock
方案 2️⃣:降级隔离级别为 RC
sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- RC 下 没有 Next-Key Lock
- 只加行锁
⚠️ 注意并发一致性变化
方案 3️⃣:两段式处理(最常用)
sql
-- 先快照读
SELECT id FROM orders WHERE status=0 LIMIT 1;
-- 再按主键锁
SELECT * FROM orders WHERE id=? FOR UPDATE;
👉 锁粒度最小
方案 4️⃣:加 SKIP LOCKED(MySQL 8+)
sql
SELECT id
FROM orders
WHERE status=0
ORDER BY create_time
LIMIT 1
FOR UPDATE SKIP LOCKED;
👉 专治"任务队列型"业务
🧠 一句话总结
Next-Key Lock 不是 bug,而是 InnoDB 的设计;
真正的事故,来自"不知道它会锁多大"。
若有转载,请标明出处:https://blog.csdn.net/CharlesYuangc/article/details/156309780