MySQL Next-Key Lock 锁表事故全拆解(从现象到根治)

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 ordersUPDATE 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

相关推荐
剩下了什么1 天前
MySQL JSON_SET() 函数
数据库·mysql·json
山峰哥1 天前
数据库工程与SQL调优——从索引策略到查询优化的深度实践
数据库·sql·性能优化·编辑器
较劲男子汉1 天前
CANN Runtime零拷贝传输技术源码实战 彻底打通Host与Device的数据传输壁垒
运维·服务器·数据库·cann
java搬砖工-苤-初心不变1 天前
MySQL 主从复制配置完全指南:从原理到实践
数据库·mysql
WangYaolove13141 天前
基于python的在线水果销售系统(源码+文档)
python·mysql·django·毕业设计·源码
山岚的运维笔记1 天前
SQL Server笔记 -- 第18章:Views
数据库·笔记·sql·microsoft·sqlserver
roman_日积跬步-终至千里1 天前
【LangGraph4j】LangGraph4j 核心概念与图编排原理
java·服务器·数据库
汇智信科1 天前
打破信息孤岛,重构企业效率:汇智信科企业信息系统一体化运营平台
数据库·重构
野犬寒鸦1 天前
从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用
java·服务器·数据库·后端·学习·算法
霖霖总总1 天前
[小技巧66]当自增主键耗尽:MySQL 主键溢出问题深度解析与雪花算法替代方案
mysql·算法