📖 本篇简介:锁是 MySQL 并发控制的核心,也是线上事故的高发区。本篇用 17 道高频面试题串起 InnoDB 锁的全貌:行锁/表锁/间隙锁/Next-Key Lock、意向锁、MDL、自增锁、S/X 锁兼容矩阵、死锁检测与排查、两阶段锁协议、乐观锁与悲观锁选型。学完本篇,你能讲清"为什么 UPDATE 没走索引会锁全表""线上死锁怎么定位""业务里怎么用乐观锁防超卖"。前置知识:第 3、4 章事务与隔离级别;本系列示例基于 MySQL 8.0。
目录
- 1 锁机制高频考点地图
- 题目 1MySQL 的锁按粒度和性质怎么分类
- 题目 2InnoDB 行锁到底加在什么上面
- 题目 3什么是意向锁 IXIS有什么用
- 题目 4MDL 元数据锁是什么线上加表字段为什么会卡
- 题目 5AUTOINC 自增锁是什么有几种模式
- 题目 6记录锁间隙锁NextKey Lock 的区别
- 题目 7S 锁和 X 锁的兼容矩阵是什么
- 题目 8什么是死锁InnoDB 怎么检测和处理
- 题目 9死锁和锁等待有什么区别
- 题目 10怎么排查死锁SHOW ENGINE INNODB STATUS 怎么看
- 题目 11怎么避免死锁
- 题目 12UPDATE 没走索引为什么会锁全表
- 题目 13两阶段锁协议是什么
- 题目 14乐观锁和悲观锁的区别业务怎么选
- 题目 15什么时候用表锁 LOCK TABLES
- 题目 16锁等待超时怎么调 innodblockwaittimeout
- 题目 17插入意向锁Insert Intention Lock是什么
- 锁机制速查表
- 常见坑与注意事项
- 复习清单
- 练习题
1 锁机制高频考点地图
面试里"锁"类问题的常见问法:
| 难度 | 典型问题 | 本篇对应 |
|---|---|---|
| 入门 | 锁有哪些分类?行锁表锁? | 题目 1、2 |
| 基础 | 意向锁?MDL?自增锁? | 题目 3、4、5 |
| 进阶 | 死锁怎么检测?两阶段锁? | 题目 8、10、13 |
| 实战 | UPDATE 没走索引?乐观锁选型? | 题目 12、14 |
一条主线记住:InnoDB 默认行级锁,锁在索引上;没走索引就退化成全表加锁;死锁靠等待图检测,主动回滚代价小的事务。
题目 1:MySQL 的锁按粒度和性质怎么分类?
考点
- 粒度:全局锁/表锁/页锁/行锁;
- 性质:S/X/意向锁;
- InnoDB 主要行锁。
参考答案
按粒度分:
| 锁 | 粒度 | 谁用 |
|---|---|---|
| 全局锁 | 整个实例 | FTWRL,备份用 |
| 表锁 | 整张表 | MyISAM、LOCK TABLES、MDL |
| 页锁 | 一页(约几百行) | BDB,InnoDB 不用 |
| 行锁 | 一行/一行 + 间隙 | InnoDB 默认 |
按性质分:共享锁 S、排他锁 X、意向锁 IS/IX、插入意向锁、自增锁等。
- InnoDB 的优势就是行锁粒度小、并发高;MyISAM 只有表锁,写并发差;
- 面试常问"InnoDB 行锁比表锁好在哪"——好并发,但没走索引会退化成表级锁(题目 12)。
-- 全局锁:整库只读,备份用
FLUSH TABLES WITH READ LOCK;
-- 业务读写全停,备份完
UNLOCK TABLES;
-- 生产备份推荐 mysqldump --single-transaction,不阻塞业务
追问
- 问:页锁为什么不流行?答:粒度介于表和行之间,并发不如行锁、管理复杂,基本被淘汰。
- 问:MyISAM 为什么快?答:读不阻塞读,但写一来全堵,适合写少读多的报表场景,现代业务基本都换 InnoDB。
题目 2:InnoDB 行锁到底加在什么上面?
考点
- 行锁加在索引记录上;
- 没索引 = 全表扫描 = 全表加锁;
- 聚簇索引 vs 二级索引。
参考答案
- InnoDB 行锁是加在索引记录上的,不是加在"物理行"上;
- UPDATE/DELETE 的 WHERE 条件用了哪个索引,就在那个索引上加锁;如果是二级索引,二级索引和聚簇索引都要加锁(先锁二级索引记录,再回表锁聚簇索引记录);
- WHERE 条件没走任何索引:扫描全表,给每一行都加锁 = 实际表锁,并发瞬间归零;
- 这就是"上线前必须 EXPLAIN 看 type"的原因之一。
-- 没走索引 = 锁全表
CREATE TABLE t(id INT PRIMARY KEY, name VARCHAR(50), age INT);
UPDATE t SET age = 20 WHERE name = 'tom'; -- name 没索引 → 全表扫 → 每行加 X 锁
-- 正确:
ALTER TABLE t ADD KEY idx_name(name);
UPDATE t SET age = 20 WHERE name = 'tom'; -- 只锁 name='tom' 的行
追问
- 问:走了二级索引,回表时聚簇索引也加锁吗?答:加,二级索引记录上 X 锁 + 回表后的聚簇索引记录上 X 锁。
- 问:SELECT 普通查询加行锁吗?答:不加,快照读走 MVCC;只有 FOR UPDATE/UPDATE/DELETE 才加。
题目 3:什么是意向锁 IX/IS?有什么用?
考点
- 意向锁是表级锁;
- 表明"我在表里加了行锁";
- 加快表锁判定。
参考答案
- 意向锁(Intention Lock)是表级锁,分 IS(意向共享)和 IX(意向排他);
- 作用:当你要给整张表加表锁时,不用逐行检查有没有行锁——只要看表上有没有 IX/IS 意向锁就知道;
- 典型场景:
- 事务要给某行加 X 锁,先在表上加 IX;
- 另一个事务要
LOCK TABLES t WRITE加表级 X 锁,一看表上有 IX,就知道"有人在行上写",阻塞; - 意向锁之间互相兼容(IX 和 IX 不冲突),只和表级 S/X 锁冲突。
-- 意向锁自动加,不用手动写
BEGIN;
UPDATE t SET age=1 WHERE id=1; -- 自动:表级 IX + 行级 X
-- 此时另一个会话 LOCK TABLES t WRITE 会被 IX 阻塞
追问
- 问:IX 和 IX 冲突吗?答:不冲突,多个事务可以同时在不同行上加 X 锁。
- 问:意向锁会阻塞 DML 吗?答:不会,DML 之间 IX 兼容;只阻塞表级锁操作。
题目 4:MDL 元数据锁是什么?线上加表字段为什么会卡?
考点
- MDL 保护表结构;
- DDL 要拿 MDL 写锁;
- 长事务阻塞 DDL,DDL 反过来阻塞新查询。
参考答案
- MDL(Metadata Lock):访问一张表时自动加,保护表结构不被并发 DDL 改;
- 读操作加 MDL 读锁,写操作加 MDL 读锁,DDL(ALTER/TABLE)加 MDL 写锁;
- 线上经典事故:
1. 事务 A 长时间持有表 t 的 MDL 读锁(没提交);
2. 事务 B 执行 ALTER TABLE t ADD COLUMN ...,要等 MDL 写锁,被 A 阻塞;
3. 之后所有新的 SELECT/UPDATE 都要拿 MDL 读锁,排在 B 后面——全堵死;
- 这就是"线上 ALTER 加字段把库打挂"的经典根因。
-- 8.0 查看 MDL 锁等待(performance_schema)
SELECT * FROM performance_schema.metadata_locks
WHERE object_name = 't';
-- 处理:KILL 掉持有 MDL 的长事务,ALTER 才能继续
-- 8.0 支持 ALTER TABLE ... NOWAIT / WAIT N 避免无限等
ALTER TABLE t ADD COLUMN c INT, ALGORITHM=INPLACE, LOCK=NONE, WAIT 10;
追问
- 问:8.0 的 ONLINE DDL 不阻塞读写吗?答:普通 DML 不阻塞,但MDL 写锁在 DDL 开始和结束瞬间仍要短暂拿,长事务会让这个等待变长。
- 问:怎么避免?答:DDL 前先查长事务;用 pt-online-schema-change / gh-ost 影子表方案。
题目 5:AUTO-INC 自增锁是什么?有几种模式?
考点
- 自增列分配 ID 的锁;
- innodb_autoinc_lock_mode;
- 8.0 模式 2 性能好。
参考答案
- 给
AUTO_INCREMENT列分配 ID 时要加自增锁,避免并发插重复 ID; innodb_autoinc_lock_mode三种模式:
1. 0(传统):语句级表锁,整条 INSERT 结束才释放,批量插入性能差;
2. 1(连续,5.7 默认):简单 INSERT 用轻量 mutex,批量 INSERT 仍表锁到语句结束;
3. 2(交错,8.0 默认):所有插入都用 mutex,不锁表,性能最好;
- 模式 2 下自增 ID 可能出现"不连续、回跳"(并发插入交错分配),业务别依赖 ID 连续。
SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode'; -- 8.0 默认 2
-- 批量 INSERT ... SELECT 在模式 2 下,主从用 ROW binlog 才安全
追问
- 问:自增 ID 会回跳吗?答:会,事务回滚后已分配的 ID 不复用;模式 2 下并发交错也会跳号。
- 问:分布式 ID 怎么选?答:高并发分布式用雪花算法/号段模式,本地自增只在单库内用。
题目 6:记录锁、间隙锁、Next-Key Lock 的区别?
考点
- Record Lock 锁单行;
- GAP Lock 锁间隙;
- Next-Key = 两者合一。
参考答案
| 锁类型 | 锁的对象 | 作用 |
|---|---|---|
| Record Lock | 单条索引记录 | 阻止别人改/删该行 |
| GAP Lock | 索引记录之间的开区间 | 阻止别人在间隙插入 |
| Next-Key Lock | 左开右闭区间 (prev, cur] | Record + GAP,防幻读 |
- Record Lock:
WHERE id=10唯一索引命中,锁 id=10 这一行; - GAP Lock:
WHERE id>10 AND id<20查不到行时,锁 (10,20) 间隙,阻止插入; - Next-Key Lock:范围查询默认加,左开右闭;
- RC 级别基本只有 Record Lock,RR 才用 Next-Key Lock 防幻读。
-- 索引记录:10, 20, 30
SELECT * FROM t WHERE id >= 20 FOR UPDATE;
-- 加的 Next-Key Lock:(10,20]、(20,30]、(30,+∞)
-- 插入 25、35 都会阻塞
追问
- 问:唯一索引等值命中退化成什么?答:退化成 Record Lock(不加间隙)。
- 问:唯一索引等值查不到呢?答:加 GAP Lock 在该值所在间隙。
题目 7:S 锁和 X 锁的兼容矩阵是什么?
考点
- S/S 兼容;S/X、X/X 互斥;
- 意向锁矩阵;
- 锁等待的根源。
参考答案
行级锁兼容矩阵:
| 已持有 \ 请求 | S | X |
|---|---|---|
| S | ✅ 兼容 | ❌ 阻塞 |
| X | ❌ 阻塞 | ❌ 阻塞 |
- S 锁:共享,读锁;多个事务可以同时读同一行;
- X 锁:排他,写锁;拿到 X 后别人既不能读(加锁读)也不能写;
- 意向锁兼容矩阵:IS/IX/IX/IS 互相兼容,只和表级 S/X 互斥。
-- S 锁演示:会话 A 加 S,会话 B 加 S 成功,加 X 阻塞
-- A:
BEGIN;
SELECT * FROM t WHERE id=1 LOCK IN SHARE MODE; -- S
-- B:
SELECT * FROM t WHERE id=1 LOCK IN SHARE MODE; -- ✅ 成功
SELECT * FROM t WHERE id=1 FOR UPDATE; -- ❌ 阻塞(要 X)
追问
- 问:普通 SELECT 加 S 锁吗?答:不加,快照读走 MVCC,无锁;只有 LOCK IN SHARE MODE / FOR SHARE 才加 S。
- 问:UPDATE 加什么锁?答:X 锁。
题目 8:什么是死锁?InnoDB 怎么检测和处理?
考点
- 循环等待;
- wait-for graph;
- 主动回滚 undo 量小的事务。
参考答案
- 死锁:两个或多个事务互相持有对方需要的锁,循环等待,谁也动不了;
- InnoDB 主动死锁检测(
innodb_deadlock_detect=ON):每次加锁等待时构造"等待图",发现环就立即回滚其中一个事务(选 undo log 量小、代价低的),让另一个继续; - 与锁等待的区别:死锁是循环等待,立即检测到;锁等待是单向等待,靠超时解除。
-- 死锁经典时序
-- 会话 A:
BEGIN; UPDATE t SET b=1 WHERE id=1; -- 锁 id=1
-- 会话 B:
BEGIN; UPDATE t SET a=1 WHERE id=2; -- 锁 id=2
-- 会话 A:
UPDATE t SET b=2 WHERE id=2; -- 等 B 的 id=2
-- 会话 B:
UPDATE t SET a=2 WHERE id=1; -- 等 A 的 id=1 → 死锁,A/B 之一被回滚
追问
- 问:死锁检测有性能问题吗?答:高并发下每次加锁都要扫描等待图,开销 O(n);超热点场景可关检测 + 调小超时,但要谨慎。
- 问:死锁报错长什么样?答:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction。
题目 9:死锁和锁等待有什么区别?
考点
- 循环 vs 单向;
- 主动检测 vs 超时;
- 报错码不同。
参考答案
| 对比 | 死锁 | 锁等待 |
|---|---|---|
| 等待方向 | 循环互相等 | 单向等 |
| 解除方式 | InnoDB 主动检测回滚一方 | 等对方提交或 innodb_lock_wait_timeout 超时 |
| 报错码 | 1213 (Deadlock found) | 1205 (Lock wait timeout exceeded) |
| 响应速度 | 立即 | 最多等 50 秒(默认) |
- 死锁是"事故",要复盘代码;锁等待是"排队",超时后业务可重试。
追问
- 问:锁等待超时后事务结束了吗?答:报错后当前语句回滚,但事务没结束,需要显式 ROLLBACK。
- 问:1205 和 1213 业务上怎么处理?答:都要重试,但 1213(死锁)要排查为什么会循环等。
题目 10:怎么排查死锁?SHOW ENGINE INNODB STATUS 怎么看?
考点
- LATEST DETECTED DEADLOCK 段;
- 事务持有的锁和等待的锁;
- 8.0 用 performance_schema。
参考答案
SHOW ENGINE INNODB STATUS\G
-- 重点看 LATEST DETECTED DEADLOCK 段:
-- 1. 两个事务各自执行的 SQL
-- 2. 各自持有的锁(HOLDS THE LOCK)
-- 3. 各自等待的锁(WAITING FOR THIS LOCK)
-- 4. 哪个事务被回滚(VICTIM)
-- 8.0 也可以查历史死锁日志
SELECT * FROM performance_schema.data_locks;
-- 开启死锁日志打印到 error log:
SET GLOBAL innodb_print_all_deadlocks = ON;
- 排查思路:看两个事务的 SQL,找"加锁顺序不一致"的地方——把加锁顺序统一即可消除。
追问
- 问:
innodb_print_all_deadlocks开着有副作用吗?答:死锁频繁时 error log 会暴涨,高并发场景按需开。 - 问:pt-deadlock-logger 是干嘛的?答:Percona 工具,定期抓取死锁日志存表,方便事后分析。
题目 11:怎么避免死锁?
考点
- 统一加锁顺序;
- 缩短事务;
- 降低隔离级别;
- 索引走到位。
参考答案
- 常见手段:
1. 统一加锁顺序:多个事务按相同顺序访问行(比如都按 id 升序锁),避免循环等待;
2. 缩短事务:事务越短,持有锁时间越短,死锁概率越低;
3. 降低隔离级别:RC 没有间隙锁,死锁大幅减少;
4. 走索引:UPDATE/DELETE 走索引,锁范围小;
5. 大事务拆小:批量删除分批做,每批提交一次;
6. 业务重试:捕获 1213 错误,幂等重试。
-- 反例:两个事务加锁顺序相反 → 死锁
-- 正例:都按 id 升序锁
BEGIN;
SELECT * FROM t WHERE id IN (1,2,3) ORDER BY id FOR UPDATE; -- 顺序固定
-- 业务逻辑
COMMIT;
追问
- 问:批量 UPDATE 怎么写不死锁?答:按主键范围分批 + 排序,每批小事务提交。
- 问:唯一索引冲突会导致死锁吗?答:会,两个事务同时插同一唯一键,互相等对方的间隙锁,典型死锁场景。
题目 12:UPDATE 没走索引为什么会锁全表?
考点
- 全表扫描 = 每行加锁;
- RR 下连间隙都锁;
- 线上事故高发。
参考答案
- InnoDB 行锁加在索引记录上;UPDATE 的 WHERE 没走索引时,优化器全表扫描,对扫描到的每一行都加 X 锁;
- RR 级别下还会加 Next-Key Lock,等于把表"锁住",其他事务的插入、更新全堵;
- 这是线上"一个 UPDATE 把库打挂"的最常见原因。
-- 事故现场
UPDATE order SET status = 'PAID' WHERE out_trade_no = 'xxx'; -- out_trade_no 没索引
-- 全表扫描 + 每行 X 锁 → 业务所有写被阻塞
-- 修复:
ALTER TABLE order ADD KEY idx_out_trade_no(out_trade_no);
UPDATE order SET status = 'PAID' WHERE out_trade_no = 'xxx'; -- 只锁一行
追问
- 问:SELECT 没走索引也锁表吗?答:普通 SELECT 是快照读不加锁;UPDATE/DELETE/SELECT FOR UPDATE 才锁。
- 问:怎么提前发现?答:上线前 EXPLAIN,type 不是 range/ref/const 的 UPDATE 一律打回。
题目 13:两阶段锁协议是什么?
考点
- 扩展阶段加锁;
- 收缩阶段只解锁;
- InnoDB 基本遵循。
参考答案
- 两阶段锁协议(2PL):事务分两阶段:
1. 扩展阶段:可以申请任何锁,不能释放;
2. 收缩阶段:可以释放锁,不能再申请新锁;
- InnoDB 虽然不是严格 2PL(死锁检测会提前回滚),但锁持有到事务结束才释放这一点符合 2PL;
- 意义:保证可串行化调度;代价是锁持有时间长,并发低。
-- 锁在 COMMIT/ROLLBACK 才释放
BEGIN;
UPDATE t SET a=1 WHERE id=1; -- 加 X 锁
UPDATE t SET a=2 WHERE id=2; -- 再加 X 锁
-- 中间 id=1 的锁还没释放
COMMIT; -- 所有锁一起释放
追问
- 问:能不能语句结束就放锁?答:InnoDB 不行,必须到事务结束;这是长事务危害大的根因。
- 问:乐观锁违背 2PL 吗?答:乐观锁不加 DB 锁,靠版本号校验,属于"无锁"方案,不在 2PL 框架内。
题目 14:乐观锁和悲观锁的区别?业务怎么选?
考点
- 悲观锁 = 数据库行锁;
- 乐观锁 = 版本号/CAS;
- 冲突频率决定选型。
参考答案
| 对比 | 悲观锁 | 乐观锁 |
|---|---|---|
| 实现 | SELECT FOR UPDATE | version 字段 + CAS UPDATE |
| 冲突时 | 阻塞等待 | 更新失败重试 |
| 适合 | 冲突频繁、短事务 | 冲突少、高并发读 |
| 性能 | 低(排队) | 高(无锁) |
- 悲观锁:先加锁再改,
SELECT ... FOR UPDATE; - 乐观锁:读时记下版本号,更新时
UPDATE ... SET version=version+1 WHERE id=? AND version=?,影响行数为 0 说明冲突,重试。
-- 乐观锁典型写法
ALTER TABLE account ADD COLUMN version INT NOT NULL DEFAULT 0;
-- 读
SELECT balance, version FROM account WHERE id=1; -- balance=100, version=3
-- 改
UPDATE account SET balance=balance-10, version=version+1
WHERE id=1 AND version=3; -- affected_rows=1 成功;=0 冲突,重试
追问
- 问:库存扣减用悲观锁还是乐观锁?答:热点商品秒杀用乐观锁 + 失败重试;低频订单用悲观锁更简单。
- 问:乐观锁 ABA 问题?答:数据库 version 单调递增没有 ABA;Java 的 AtomicStampedReference 才要管 ABA。
题目 15:什么时候用表锁 LOCK TABLES?
考点
- LOCK TABLES 显式表锁;
- 与 InnoDB 行锁对比;
- 特殊场景。
参考答案
LOCK TABLES t READ/WRITE是 Server 层的显式表锁,会绕过 InnoDB 行锁直接锁整表;- 生产很少用,典型场景:
1. MyISAM 表的写操作(本来就表锁);
2. 批量数据迁移,要独占一张表;
3. 某些元数据一致性操作;
- InnoDB 业务表一般不用 LOCK TABLES,行锁更细。
-- 表锁演示
LOCK TABLES t WRITE; -- 独占,其他会话读写都阻塞
-- 操作
UNLOCK TABLES;
追问
- 问:FLUSH TABLES WITH READ LOCK 和 LOCK TABLES READ 区别?答:FTWRL 锁所有库所有表(备份用);LOCK TABLES 锁指定表。
- 问:5.6 之前 LOCK TABLES 有什么坑?答:它会隐式提交当前事务,慎用。
题目 16:锁等待超时怎么调 innodb_lock_wait_timeout?
考点
- 默认 50 秒;
- 生产建议 5~10 秒;
- 与死锁检测分工。
参考答案
innodb_lock_wait_timeout:行锁等待超时,默认 50 秒;- 生产建议调到 5~10 秒,避免连接被长时间挂住拖垮连接池;
- 死锁检测(
innodb_deadlock_detect=ON)负责循环等待的即时检测;这个超时负责单向等待; - 超时后报错 1205,业务要捕获并重试。
SET GLOBAL innodb_lock_wait_timeout = 5; -- 全局
SET SESSION innodb_lock_wait_timeout = 5; -- 当前会话
追问
- 问:调太小会不会误伤正常业务?答:长 SQL 单条执行时间本身就长,但锁等待是"等别人提交",不是自己执行慢,5 秒足够;超过 5 秒说明对方事务太长,该治理。
- 问:死锁检测关掉后靠什么兜底?答:靠这个超时,超时自动报错,业务重试。
题目 17:插入意向锁(Insert Intention Lock)是什么?
考点
- INSERT 时在间隙上加的意向锁;
- 与 GAP 锁的关系;
- 并发插入阻塞。
参考答案
- 插入意向锁是一种特殊的 GAP 锁:INSERT 时在目标位置的间隙上加,表明"我打算在这个间隙插入";
- 插入意向锁之间互相兼容(多个事务可以同时在同一间隙准备插入不同行);
- 但如果间隙被别的事务加了普通 GAP 锁,插入意向锁要等待——这就是 RR 下并发插入互相阻塞的原因。
-- 间隙锁阻塞插入意向锁
-- 会话 A:
SELECT * FROM t WHERE id BETWEEN 10 AND 20 FOR UPDATE; -- 加 (10,20) Next-Key
-- 会话 B:
INSERT INTO t VALUES(15); -- 插入意向锁要等 A 的 GAP,阻塞
追问
- 问:RC 下插入意向锁还阻塞吗?答:RC 没有普通 GAP 锁,所以插入意向锁不阻塞,并发高。
- 问:唯一索引冲突为什么死锁?答:两个事务都想插入同一唯一键,互相等对方的插入意向锁/间隙锁。
锁机制速查表
| 锁 | 粒度 | 谁加 | 作用 |
|---|---|---|---|
| 记录锁 X/S | 单行索引记录 | UPDATE/DELETE/FOR UPDATE | 阻止同行并发写 |
| GAP 锁 | 索引间隙 | RR 范围查询 | 阻止间隙插入 |
| Next-Key Lock | 左开右闭区间 | RR 范围当前读 | 防幻读 |
| 意向锁 IX/IS | 表级 | 自动 | 加速表锁判定 |
| MDL | 表元数据 | 自动 | 防 DDL/DML 并发改结构 |
| AUTO-INC 锁 | 自增计数器 | INSERT 时 | 防重复自增 ID |
| 插入意向锁 | 间隙 | INSERT 时 | 标记插入意图 |
| 报错码 | 含义 | 处理 |
|---|---|---|
| 1213 | 死锁 | 重试 + 复盘加锁顺序 |
| 1205 | 锁等待超时 | 重试 + 查长事务 |
常见坑与注意事项
1. UPDATE 没 WHERE 或没走索引:全表加 X 锁,线上事故之王。
2. 以为加索引就万事大吉:WHERE 里函数/隐式类型转换会让索引失效,照样全表锁。
3. 事务里加 RPC/HTTP:锁持有时间随外部接口拉长,连锁死。
4. DDL 不查长事务:MDL 排队把业务打挂,8.0 用 LOCK=NONE, WAIT N。
5. RR 下大范围 SELECT FOR UPDATE:Next-Key 锁一大片,批量任务改 RC 或分批。
6. 乐观锁忘了 WHERE version=?:退化成无条件 UPDATE,并发照样超卖。
7. 以为死锁检测开着就不用管:高热点表死锁检测本身就是瓶颈,要拆业务。
复习清单
| 题目 | 核心结论 | 易错点 |
|---|---|---|
| 锁分类 | 粒度 + 性质两个维度 | 漏了 MDL/意向锁 |
| 行锁加哪 | 索引记录上 | 说"加在物理行" |
| 意向锁 | 表级,加速表锁判定 | 以为它锁行 |
| MDL | DDL 排队堵业务 | 忘了长事务是根因 |
| Next-Key | Record+GAP 左开右闭 | 区间方向说反 |
| 死锁检测 | wait-for graph,回滚小事务 | 答成"等超时" |
| 死锁 vs 锁等待 | 循环 vs 单向,1213 vs 1205 | 报错码混淆 |
| UPDATE 没索引 | 全表加锁 | 以为 SELECT 也锁 |
| 两阶段锁 | 锁到事务结束才放 | 以为语句结束就放 |
| 乐观锁 | version + CAS | 忘了重试 |
练习题
练 1:会话 A 执行 UPDATE t SET name='x' WHERE phone='138xxxx',phone 列没有索引。RR 级别下,会话 B 同时 INSERT INTO t ... 会怎样?
<details>
<summary>参考答案</summary>
A 全表扫描,对每一行加 X 锁 + Next-Key Lock(RR),等于锁全表;B 的 INSERT 要拿插入意向锁,被间隙锁阻塞,直到 A 提交。这就是"没索引的 UPDATE = 表锁"。修复:给 phone 加索引。
</details>
练 2:业务要求"扣库存时不能超卖",表 sku(sku_id, stock, version)。写出乐观锁扣减 SQL,并说明 affected_rows=0 怎么办。
<details>
<summary>参考答案</summary>
-- 先读 stock 和 version
SELECT stock, version FROM sku WHERE sku_id=100;
-- 扣减,CAS
UPDATE sku SET stock=stock-1, version=version+1
WHERE sku_id=100 AND version=:version;
-- affected_rows=1:成功
-- affected_rows=0:说明别人改过,重读 version 再重试(最多 3 次),仍失败则提示系统繁忙
</details>
练 3:线上 ALTER TABLE t ADD COLUMN c INT 一直卡住,之后所有 SELECT 也卡住。最可能的原因和处理步骤?
<details>
<summary>参考答案</summary>
根因:表 t 上有个长事务持有 MDL 读锁,ALTER 要拿 MDL 写锁排在后面;新的 SELECT 也要 MDL 读锁,排在 ALTER 后面,全堵。处理:
1. SELECT * FROM information_schema.INNODB_TRX 找长事务;
2. KILL 长事务连接;
3. ALTER 拿到 MDL 写锁继续;
4. 事后排查为什么会有长事务,DDL 改用 LOCK=NONE, WAIT N 或 gh-ost。
</details>
下一篇预告:第 6 章 MVCC原理面试精讲
文章回复
0 条公开回复