Theory · MySQL · InnoDB
从实例到索引记录 —— 全局锁 / MDL / 意向锁 / 行锁四件套 / 死锁推演,加锁的对象永远是"索引"
全局(FTWRL / 备份锁)→ 表级(表锁、MDL、意向锁 IS/IX、AUTO-INC)→ 行级(Record / Gap / Next-Key / Insert Intention)
基本单位是 next-key(前开后闭);唯一等值命中退化为记录锁;等值未命中退化为 gap——三条规则推遍所有案例
长事务卡死 DDL 的 MDL 雪崩、gap 互相等待与乱序更新两类死锁逐帧推演,配套 SHOW ENGINE INNODB STATUS 排查链
Why Locking Matters
-- accounts 表:id=1 的账户余额 100 元 -- 同一毫秒来了两个扣款请求,各扣 80 元 -- T1(先读后写) BEGIN; SELECT balance FROM accounts WHERE id=1; -- 读到 100 UPDATE accounts SET balance = 100-80 WHERE id=1; COMMIT; -- 余额 = 20 -- T2(与 T1 完全相同的代码,同时执行) BEGIN; SELECT balance FROM accounts WHERE id=1; -- 也读到 100(T1 还没提交) UPDATE accounts SET balance = 100-80 WHERE id=1; COMMIT; -- 余额还是 = 20 -- 结果:扣了两次 80,余额只剩 20 —— 凭空多出 80 元
数据库的默认行为是让每个事务看到一致的数据快照,但不会主动帮你排队。两个事务同时读走同一个 100,各自算各自的,最后写回的那一版就把对方的结果覆盖掉了——跟两个人同时编辑同一份文档、后者保存覆盖前者是一回事。
它把"同时进行"变成"排队进行":谁先动手谁先锁住这行,后来者必须等它提交完、看到新值才能继续。代价是吞吐下降、可能等待、极端情况互相堵死(死锁)。所以锁的全部学问,就是在"正确性"和"并发度"之间找平衡点。
① 锁的粒度:锁一整张表最安全但没人能干活;锁一行最精细,可没有索引时 InnoDB 会把全表每一行都锁上(第 10 页);
② 锁的范围:除了真实存在的行,InnoDB 还会锁"不存在的缝隙"(间隙锁)来防幻读,代价是凭空多出一类死锁(第 13 页);
③ 锁的时机:锁要一直握到事务提交才放(2PL),一个忘提交的长事务能把整张表卡死(第 12 页)。
Prerequisites & Glossary
| 术语 | 一句话理解(先记住这个,细节后面展开) |
|---|---|
| 事务 | 一组"要么全做完、要么全不做"的数据库操作,以 COMMIT 结束才算生效 |
| 共享锁 S | 读锁:我读的时候不许别人改,但别人也能读。写法 SELECT … FOR SHARE |
| 排他锁 X | 写锁:我改的时候别人既不能读(加锁读)也不能改。UPDATE 自动加它 |
| 当前读 | 读最新的、已提交的数据并加锁,如 FOR UPDATE/UPDATE/DELETE |
| 快照读 | 普通的 SELECT:读事务开始时的一致性快照,不加锁 |
| 行锁 | 锁住某一行(精确说是它的索引记录)——粒度最细,并发度最高 |
| 间隙锁 Gap | 锁住两条记录之间不存在的缝隙,唯一目的是不许往里插新行 |
| 临键锁 Next-Key | 行锁 + 它前面那段间隙,区间前开后闭——RR 下加锁的基本单位 |
| MDL 元数据锁 | 锁表结构而不是锁数据:事务用着这张表,就不许别人 ALTER/DROP |
| 死锁 | 两个事务各握着对方要的锁,互相等且谁也不放手——只能靠一方被回滚打破 |
mvcc.html → 快照读 / 当前读的机制、next-key 与 gap 的定义、幻读怎么被挡住
transaction-basics.html → 隔离级别、2PL(为什么锁到提交才放)、长事务治理
OS · 死锁 → 等待图检测、四条件与四策略的通用机制(不限数据库)
为了不纠结名词,后面统一按"谁想对哪个对象做什么操作,被谁挡住了"来分析。你只需要盯住一件事:锁到底圈住了多大范围的数据。
Lock Hierarchy
开场那 80 元的窟窿,靠锁住 id=1 这一行就补上了——"锁哪些行、锁多大范围"正是这张图要回答的。
Global Locks
server 层全局只读:阻塞一切写与 DDL,同时关闭表并同步 binlog 位点——逻辑备份取一致性位点(--source-data / --master-data)的传统手段。代价:主库执行期间写入全部排队,"主库备份 = 变相停写"。
实例级备份锁,需要 BACKUP_ADMIN 权限。手册:"permits DML during an online backup while preventing operations that could result in an inconsistent snapshot"——允许 DML 继续,只阻止建/改/删文件、REPAIR/TRUNCATE/OPTIMIZE 与账号管理;多会话可同时持有;8.0.28 起 PLUGINS 期间禁 PURGE BINARY LOGS。
| 维度 | FTWRL | LOCK INSTANCE FOR BACKUP |
|---|---|---|
| 引入版本 | 古老(5.x 即有) | 8.0 新增(§15.3.5) |
| 阻塞什么 | 全部写 + DDL(全库只读) | 仅"会造成备份不一致"的操作(文件级) |
| DML | 阻塞 | 放行——在线备份不卡业务 |
| 典型组合 | 老版 mysqldump 取位点 | 8.0 mysqldump:backup lock 定文件一致性 + FLUSH TABLES … WITH READ LOCK 短暂取 binlog 位点,写阻塞窗口缩到极短 |
| 从库只读 | 备库统一只读用 read_only / super_read_only,不是加锁问题——别混为一谈 | |
Table & Intention Locks · §17.7.1
事务 T1 锁了某一行(X 行锁);事务 T2 想执行 LOCK TABLES t WRITE——若无表级标记,T2 必须逐行检查整表有无行锁。意向锁方案:T1 拿行 X 锁之前先拿表级 IX,T2 看一眼表级就知道"有行锁存在",直接等待。
① "Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table."② "Intention locks do not block anything except full table requests (for example, LOCK TABLES ... WRITE)."——只与全表请求互斥,IS/IX 彼此之间、与行级 S/X 之间互不冲突。
| 请求 ↓ / 已持有 → | IS | IX | S(表) | X(表) | 含义 |
|---|---|---|---|---|---|
| IS(行 S 前置) | ✓ | ✓ | ✓ | ✗ | 读意向互相不打扰 |
| IX(行 X 前置) | ✓ | ✓ | ✗ | ✗ | 写意向只怕"全表级"写 |
| S(全表读,LOCK TABLES READ) | ✓ | ✗ | ✓ | ✗ | 全表读容不下任何"写意向" |
| X(全表写,LOCK TABLES WRITE) | ✗ | ✗ | ✗ | ✗ | 全表写谁都容不下 |
Metadata Locks · §10.11.4
手册:"To ensure transaction serializability, the server must not permit one session to perform a DDL statement on a table that is used in an uncompleted ... transaction in another session."——server 在事务用表时取 MDL,推迟到事务结束才释放;MDL 保证结构在事务生命周期内稳定(否则同一个 SELECT 前后读的可能已经不是同一张表)。
读读兼容、读写互斥:增删改查 = MDL 读(共享),ALTER/DROP = MDL 写(排他)。多个读锁并存无碍;一个写锁请求进来,不仅要等存量读锁放完,还会挡住后续新的读锁请求——这就是雪崩的机制根源(第 12 页推演)。
| 要点 | 内容 |
|---|---|
| 覆盖范围 | 不只表:库、表、存储过程、触发器、表空间等都有 MDL;DML 与 SELECT 也会持有 MDL 读锁 |
| 持有时长 | 事务结束(commit / rollback)才释放——不是语句结束。autocommit 下语句即事务,所以"长事务"才成为风险源 |
| 观察手段 | performance_schema.metadata_locks 表(谁持有、谁在等);手册:"useful for seeing which sessions hold locks, are blocked waiting for locks" |
| 等待上限 | lock_wait_timeout(MDL 等待超时,默认 31536000 秒 ≈ 1 年——等于不设防,必须靠流程而非默认值兜底) |
| 死锁检测 | 手册:语句逐个获取 MDL 并在其中做死锁检测;但"持有+排队"结构决定了长事务仍能把 DDL 与后续读写一起卡住 |
Row Locks · §17.7.1
| 锁 | 定义(手册口径) | 关键性质 |
|---|---|---|
| Record 记录锁 | 锁在索引记录上的 S / X 锁——同一行走不同索引,锁的就是不同索引记录 | 唯一索引等值命中时"退化"出现的就是它 |
| Gap 间隙锁 | 锁索引记录之间的开区间;"purely inhibitive"——唯一目的就是阻止别人往间隙里插入 | S/X 不分、可共存;RR 才有,RC 基本禁用(第 11 页) |
| Next-Key 临键锁 | "a combination of a record lock on the index record and a gap lock on the gap before the index record"= 记录锁 + 前面的 gap,区间前开后闭 (上一值,本值] | RR 加锁的基本单位;supremum 伪记录兜住 +∞(mvcc 已给定义) |
| Insert Intention 插入意向 | "a type of gap lock set by INSERT operations prior to row insertion"——插入前先声明"我要插进这个间隙" | 同 gap 不同位置互不阻塞;撞上别人的 gap 锁才等待 |
mode 0 传统表级 AUTO-INC 锁(到语句结束);mode 1 连续(简单插入走轻量互斥,批量插入才上表锁);mode 2 交错(完全不用表级锁,最快,值单调但不保证连续)。8.0 起默认 2——手册明说原因:默认复制格式从 statement 变 row,不再需要 mode 1 的连续性保证;statement 复制仍需 0/1。
四件套是同一次扫描的不同形态:默认形态是 next-key;命中唯一等值退化为 record;等值未命中退化为 gap;INSERT 进场先挂 insert intention。记住"形态退化"视角,第 9 页的三条规则就不需要死记。
Locking Rules 1/2 · 等值
Locking Rules 2/2 · 范围
| 案例(idx_k:10·15·20·25) | 加锁结果 | 推演要点 |
|---|---|---|
WHERE k>=16 AND k<=24(范围) | next-key (15,20] + next-key (20,25] | 沿途全部 next-key;右侧第一个不满足的值 25 也被 next-key 锁住——gap (20,25) 挡住插 21~24,25 本身再挂记录锁 |
WHERE k>20(开右界) | (20,25] + (25, supremum] | 扫到索引末端时由 supremum 伪记录兜住 +∞——正无穷那一段是"最大的间隙"(mvcc 已述) |
UPDATE t SET … WHERE name='x'(无索引) | 聚簇索引全表扫描 → 扫过的每条记录都加 next-key → 等价锁全表 | "锁加在索引记录上"的推论:没有可用索引,锁的就是全表的记录;业务高频事故源 |
① gap/next-key 基本禁用 → 只锁记录本身;② 半一致读:UPDATE 扫描遇被锁行,先返回最新已提交版本判 WHERE,不匹配就放行不等锁——无索引更新的灾难被大幅缓解(代价:幻读 + 仅 row binlog,transaction-basics)。手册:半一致读"greatly reduces the probability of deadlocks"。
① UPDATE/DELETE 的 WHERE 列必须有索引(EXPLAIN 验证 type 不为 ALL,见 index-btree);② RR 下查"是否存在"用唯一索引等值,退化成记录锁最轻;③ 8.0 对 LIMIT 等场景会提前停止加锁,复杂语句用 performance_schema.data_locks 实测锁范围,别背结论。
Gap Lock Properties · §17.7.1
手册:"Gap locks in InnoDB are 'purely inhibitive', which means that their only purpose is to prevent other transactions from inserting to the gap."——gap 不阻止读、不阻止 gap 之间共享,只阻止一件事:往里插。
手册:"There is no difference between shared and exclusive gap locks. They do not conflict with each other."——两个事务对同一 gap 各拿一份"gap 锁"完全合法;冲突发生在 gap vs 插入意向那一对。
手册:改为 READ COMMITTED 后,搜索与索引扫描不再加 gap,"used only for foreign-key constraint checking and duplicate-key checking"——外键检查与唯一键查重仍会加(mvcc 隔离级别页同口径)。
| 冲突关系 | 结果 |
|---|---|
| gap 锁 vs gap 锁(无论谁先谁后) | 共存——为第 13 页"互相等待 gap"死锁埋下伏笔:都拿得住 gap,插入时才互相卡 |
| gap 锁 vs 插入意向锁 | 阻塞——INSERT 先声明意向,发现 gap 被占则等待 |
| 插入意向锁 vs 插入意向锁(不同位置) | 共存——同一间隙插不同 key 不互等(并发插入的友好性来源) |
| 唯一索引等值命中 vs gap | 不需要 gap——退化为记录锁;这就是"查存在性用唯一条件"最轻的原因 |
MDL Pitfall & Online DDL · §10.11.4 / §17.12
-- 雪崩四步(时间自上而下) T1 BEGIN; SELECT * FROM t WHERE id=1; -- ① 持 MDL 读(事务未提交 → 一直持有) T2 ALTER TABLE t ADD COLUMN c INT; -- ② 等 MDL 写(排在 T1 后面) T3 SELECT * FROM t; -- ③ 也要等!MDL 写请求"插队"挡住一切新读 T4… 连接池被 ②③ 耗尽 → 该表读写全停 → 上游超时雪崩 -- 解法:变更前先杀长事务(INNODB_TRX / processlist),错峰执行 DDL -- lock_wait_timeout 默认 1 年 → 必须显式设小或靠流程兜底
| 操作(8.0) | ALGORITHM | 重建表 | 并发 DML |
|---|---|---|---|
| ADD COLUMN | INSTANT(8.0.12 起默认;仅元数据) | 否 | 允许 |
| DROP COLUMN | INSTANT(8.0.29 起默认;此前 INPLACE) | 否 | 允许 |
| ADD / DROP 二级索引 | INPLACE | 否 | 允许(见 index-btree) |
| MODIFY COLUMN 改类型 | COPY(唯一选择) | 是 | 不允许——锁写,大表慎用 |
Deadlock Case 1 · §17.7.5
Deadlock Case 2 & Diagnosis · §17.7.5
| 帧 | T1(先 A 后 B) | T2(先 B 后 A) | 状态 |
|---|---|---|---|
| t1 | UPDATE accounts SET … WHERE id=1 | — | T1 持 id=1 行 X 锁 |
| t2 | — | UPDATE accounts SET … WHERE id=2 | T2 持 id=2 行 X 锁 |
| t3 | UPDATE … WHERE id=2 → 等 T2 | — | T1 等 T2 释放 id=2 |
| t4 | — | UPDATE … WHERE id=1 → 等 T1 | 等待环 → 死锁,回滚 undo 少的一方 |
SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 段:两条事务各自持有的锁与等待的锁、回滚了谁。手册:它只保留最近一次死锁现场。
手册:"enable innodb_print_all_deadlocks to print information about all deadlocks to the mysqld error log"——把每一次死锁都打进错误日志,配合日志采集统计死锁模式。
performance_schema.data_locks / data_lock_waits + sys.innodb_lock_waits 视图:直接回答"谁持有什么、谁在等谁"(8.0 取代了旧的 INNODB_LOCKS/INNODB_LOCK_WAITS)。
innodb_deadlock_detect=on(默认开,主动检测立即回滚受害者)vs innodb_lock_wait_timeout=50(秒,等锁超时只回滚当前语句)。热点行高并发下有"关检测、靠超时或应用排队"的取舍——检测本身有 O(n) 开销。Deadlock Prevention · §17.7.5
① 用事务而非 LOCK TABLES;② 让 insert/update 型事务尽量小、不长期开着;③ 多表/多行更新时所有事务按相同顺序访问(配套 SELECT … FOR UPDATE);④ 给 FOR UPDATE 与 UPDATE … WHERE 的列建索引——没索引会扫全表锁全表(第 10 页)。
手册:"The possibility of deadlocks is not affected by the isolation level, because the isolation level changes the behavior of read operations, while deadlocks occur because of write operations."——隔离级别不影响死锁概率(但 RC 禁 gap 后,"gap 型"死锁确实消失,这是锁形态变化而非级别魔力,答题时要把两层都说到)。
| 工程手段 | 做法与适用 |
|---|---|
| 应用层重试 | 捕获死锁错误(ER_LOCK_DEADLOCK 1213)重试整个事务——死锁不是错误而是"重试信号",幂等是前提 |
| 热点行削峰 | 秒杀扣库存:单行并发更新排队 → 队列化/合并扣减/分段库存(stock 拆 N 行),避免"检测风暴" |
| 统一加锁顺序 + 小事务 | 代码规范层面固化访问顺序(如按 id 升序);事务里不排队不做 RPC——锁窗口=事务时长(transaction-basics · 2PL) |
Pessimistic vs Optimistic
| 维度 | 悲观锁(数据库机制) | 乐观锁(业务协议) |
|---|---|---|
| 实现 | SELECT … FOR UPDATE(当前读加 X 锁)或直接 UPDATE … WHERE 条件 借行锁串行化 | 版本号:UPDATE t SET stock=stock-1, version=version+1 WHERE id=? AND version=?,affected rows=0 即冲突重试 |
| 冲突处理 | 阻塞等待(或 NOWAIT / SKIP LOCKED 立即返回) | 提交时发现冲突 → 应用层重试 |
| 适用场景 | 冲突高、临界区短、必须同步互斥(账户扣减、库存防超卖) | 冲突低、可重试、读多写少(内容编辑、状态机) |
| 代价 | 锁窗口 = 事务时长(2PL);热点行吞吐骤降;连接被占用 | 无 DB 锁,但冲突率高时重试风暴 + DB 徒劳写入 |
扣库存别先查后写:UPDATE stock SET n=n-1 WHERE id=? AND n>0——一条语句在行锁内完成"检查+扣减",天然防超卖;n=0 影响行数为 0 即售罄。这比 FOR UPDATE + 业务判断少一次持锁往返。
InnoDB 的 MVCC 是引擎内部的"读不加锁"(mvcc);业务乐观锁是应用层的"写前校验"。前者由数据库自动完成、对业务透明,后者要自己在表里加 version 字段——面试时明确"不是一回事,只是思想同源"。
Cheat Sheet · 1/2
| 全局 | FTWRL(全库只读)|8.0 LOCK INSTANCE FOR BACKUP(放行 DML,只挡文件级不一致操作) |
| 表级 | 表锁 LOCK TABLES|MDL(锁结构,事务结束才放)|意向锁 IS/IX|AUTO-INC |
| 行级 | Record/Gap/Next-Key/Insert Intention —— 永远加在索引记录上 |
| 意向锁口诀 | 只"登记意图",IS/IX 互相兼容、与行锁不冲突,唯一对手是 LOCK TABLES |
| 基本单位 | next-key(前开后闭:记录锁 + 它前面的 gap) |
| 退化 ① | 唯一索引等值命中 → 退化为记录锁(只锁这一个点) |
| 退化 ② | 等值未命中 → 退化为 gap(锁缝隙,右界那条记录本身不锁) |
| 普通索引等值 | next-key (前值, 值] + 右侧 gap (值, 下一值]——还要防下一个同值插进来 |
| 范围扫描 | 扫到哪锁到哪,右边第一个不满足的值也会被锁;扫到末端由 supremum 兜住 +∞ |
| 无索引 UPDATE | 聚簇全表扫描 → 每行都加 next-key → 等价锁全表(生产事故头号来源) |
| 纯抑制性 | 只防插入,不防读、不防别人也持同一 gap |
| S/X 不分 | 共享排他 gap 锁没区别、可共存;冲突对是「gap vs 插入意向」 |
| RC 下禁用 | 例外:外键检查与唯一键查重仍会加 gap |
Cheat Sheet · 2/2
| gap 互等 | 两事务各持同一 gap(可共存)→ 各自 INSERT → 插入意向互撞对方 gap → 成环。谁都没做错 |
| 乱序更新 | 两事务以相反顺序更新两行 → 各持一把等另一把 → 成环(转账类业务原型) |
| 谁被回滚 | InnoDB 选代价小(undo 量少)的一方回滚,客户端收到 1213 |
| 看最近一次 | SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK |
| 看全量 | innodb_print_all_deadlocks=on → 打进 error log 做模式统计 |
| 看实时 | 8.0:performance_schema.data_locks / data_lock_waits + sys.innodb_lock_waits |
| 检测 vs 超时 | innodb_deadlock_detect:发现环回滚整个事务;innodb_lock_wait_timeout(默认 50s):只回滚当前语句 |
| 应用侧 | 捕获 1213 → 整事务重试(前提是幂等);死锁是重试信号不是故障 |
| MDL 雪崩 | 长事务持 MDL 读 → DDL 等 MDL 写 → 连后续新读也一起挡住 → 连接池耗尽。lock_wait_timeout 默认 1 年,等于不设防 |
| Online DDL | "online" = 执行期允许并发 DML,不等于不加 MDL;ADD COLUMN=INSTANT、ADD INDEX=INPLACE、改类型=COPY 且锁写 |
| 悲观 vs 乐观 | 悲观 = DB 机制(FOR UPDATE/原子 UPDATE);乐观 = 业务协议(自己加 version 字段)——与 MVCC 不是一回事 |
| 防超卖 | 一条语句:UPDATE stock SET n=n-1 WHERE id=? AND n>0,affected=0 即售罄。先 SELECT 再 UPDATE 必超卖 |
Interview QA · 1/2
先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的直接翻回第 17 / 18 页速查表。
全局:FTWRL、8.0 LOCK INSTANCE FOR BACKUP;表级:表锁、MDL、意向锁 IS/IX、AUTO-INC 锁;行级:Record / Gap / Next-Key / Insert Intention。行锁加在索引记录上。
多粒度协议的登记簿:拿行 S/X 前先在表级挂 IS/IX,让全表操作不用逐行扫锁。IS/IX 互相兼容、与行级锁不冲突,唯一对手是 LOCK TABLES 这类全表请求(手册原话)。
都不是——手册:共享与排他 gap 锁没有区别、互不冲突,唯一目的是阻止插入(purely inhibitive)。冲突对是"gap vs 插入意向":INSERT 声明意向时撞上 gap 才等待;插入意向之间不同位置共存。
唯一命中:退化记录锁;唯一未命中:next-key 退化为 gap (前值, 右界),右界记录不受锁;普通索引等值:next-key (前值, 值] + gap (值, 下一值)——防重复插入。基本单位 next-key 前开后闭,两个退化是全部口诀。
RR:聚簇全表扫描 → 每条记录 next-key → 等价锁全表。RC:gap 禁用只锁记录 + 半一致读(不匹配行提前放行)大幅缓解——手册称半一致读大幅减少死锁。代价:幻读、仅 row binlog。
MDL 保证事务期间表结构稳定:DML 持读锁、DDL 要写锁,写锁还会挡住后续一切读请求。长事务持有读锁不放 → DDL 排队 → 全表读写跟着排队 → 连接池耗尽。lock_wait_timeout 默认一年,必须显式设小。
不是。online 指 INPLACE/INSTANT 执行期间允许并发 DML;起止仍需短暂 MDL 排他,长事务照样能卡住它。ALGORITHM 记四行:ADD COLUMN INSTANT、ADD INDEX INPLACE、DROP INDEX INPLACE、改类型 COPY 锁写。
三种模式:0 传统表级 AUTO-INC 锁、1 连续(批量插入才表锁)、2 交错无表锁。8.0 起默认 2——官方理由:默认复制从 statement 变 row,不再需要连续性;statement 复制仍需 0/1(§17.6.1.6 原话)。
Interview QA · 2/2 · 先自答再对照
① 同一 gap 两个事务互持 gap(可共存)后互插,插入意向互相撞 gap → 环。② 两事务以相反顺序更新两行,各持一把 X 锁等另一把 → 环。共同点:持有并等待 + 顺序不一致。
SHOW ENGINE INNODB STATUS 看 LATEST DETECTED DEADLOCK(仅最近一次:双方持锁/等锁/回滚了谁);innodb_print_all_deadlocks 全量打进错误日志做统计;8.0 用 performance_schema.data_locks + sys.innodb_lock_waits 实时看谁等谁。
手册四条:事务化、小事务、同序访问、加锁列建索引。隔离级别不影响死锁概率(死锁因写、级别改读)——但 RC 禁 gap 后"gap 型"死锁消失,是锁形态变化。再补应用层 1213 重试与热点行削峰。
innodb_deadlock_detect(默认 on):发现环立即回滚代价小的事务;innodb_lock_wait_timeout(默认 50 秒):等锁超时只回滚当前语句、事务仍在。超高并发热点行有"关检测靠超时/应用排队"的取舍——检测有代价。
RC 下搜索与扫描不加 gap(外键/唯一键查重除外),"锁不存在的东西"这类死锁源头消失;UPDATE 半一致读让不匹配行提前放行,持锁窗口更短。代价:幻读仍在、仅 row binlog(binlog 约束见 transaction-basics)。
NOWAIT:拿不到锁立即报错 3572——用于快速失败防堆积;SKIP LOCKED:跳过被锁行继续取——任务队列轮询的标配(多 worker 互不阻塞消费)。两者仅作用行级锁,statement 复制不安全。
表加 version:UPDATE … SET v=v+1 WHERE id=? AND v=?,affected=0 即冲突。处理:有限次重试(幂等前提)、退避后再试、或改悲观/队列。冲突率高时重试风暴——乐观锁适用冲突低的场景,选型别硬套。
一条语句:UPDATE stock SET n=n-1 WHERE id=? AND n>0——行锁内完成检查与扣减,affected=0 即售罄,天然防超卖。热点再上分段库存/合并扣减/异步落库;先 SELECT 再 UPDATE 的写法在并发下必超卖。
Related & References
mvcc.html · next-key / gap / supremum 定义与幻读阻断的机制视角
transaction-basics.html · 2PL(锁到 commit)、隐式提交与 MDL 事故链入口
index-btree.html · "锁在索引上"的结构基础;Online DDL 与索引变更
log-redo-undo-binlog.html · 死锁回滚的 undo 代价、RC 仅 row binlog 的日志约束
replication.html · AUTO-INC mode 与 binlog 格式的复制因果
层级 → 全局 / 表级(MDL·意向)/ 行级四件套
规则 → next-key 基本单位 + 两个退化 + supremum 兜底
事故 → 长事务 × MDL × DDL = 全表雪崩
死锁 → 两类环(gap 互等 / 乱序更新)→ 三件套排查
OS 对照 → OS · 死锁(等待图检测与恢复/预防策略的通用机制)
选型 → 冲突高悲观原子 UPDATE / 冲突低乐观 version
References
| dev.mysql.com/doc/refman/8.0/en/innodb-locking.html | §17.7.1:record/gap/next-key/insert intention 定义、"purely inhibitive"、S/X gap 无区别、意向锁用途、RC 例外(外键/重复键检查) |
| dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html | §17.7.2.4:锁到 commit/rollback 才释放、FOR SHARE vs FOR UPDATE、NOWAIT / SKIP LOCKED |
| dev.mysql.com/doc/refman/8.0/en/innodb-deadlocks.html | §17.7.5:死锁定义与 victim、SHOW ENGINE INNODB STATUS、innodb_print_all_deadlocks、减少死锁清单、隔离级别不影响死锁结论 |
| dev.mysql.com/doc/refman/8.0/en/metadata-locking.html | §10.11.4:MDL 推迟到事务结束、防 DDL 撕裂、performance_schema.metadata_locks、逐个取锁并做死锁检测 |
| dev.mysql.com/doc/refman/8.0/en/innodb-auto-increment-handling.html | §17.6.1.6:innodb_autoinc_lock_mode 0/1/2,8.0 起默认 2 及 row 复制因果 |
| dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl.html · …-operations.html | §17.12 / §17.12.1:INSTANT/INPLACE/COPY 支持矩阵、ADD COLUMN INSTANT 自 8.0.12、DROP COLUMN INSTANT 自 8.0.29、64 row versions、"halts immediately" |
| dev.mysql.com/doc/refman/8.0/en/lock-instance-for-backup.html | §15.3.5:实例级备份锁、BACKUP_ADMIN、允许 DML 阻止文件级不一致操作、8.0.28 PURGE BINARY LOGS 限制 |
| dev.mysql.com/doc/refman/8.0/en/performance-schema-data-locks-table.html · …-data-lock-waits-table.html | 8.0 行锁实时视图(取代 INNODB_LOCKS / INNODB_LOCK_WAITS)与 sys.innodb_lock_waits |
| dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html | innodb_lock_wait_timeout(默认 50s)、lock_wait_timeout(默认 31536000s)、innodb_deadlock_detect |
| book:丁奇《MySQL 实战 45 讲》锁规则篇 | "next-key 基本单位 + 唯一命中退化记录锁 + 等值未命中退化 gap"的通行归纳口径(本 deck 第 9/10 页) |
https:// 后面即达官方页面。本 deck 全部按 MySQL 8.0 撰写,5.7 在 INSTANT DDL / AUTO-INC mode / data_locks 上行为不同。