
锁机制与死锁防御事务保证了单个事务内的一致性但并发时多个事务抢同一行怎么办答案是锁。锁的粒度、范围、兼容性决定了系统的并发能力和死锁概率。一、3 个引导问题现象 → 范围 → 原理Q1会话1WHERE product_id5 FOR UPDATE未提交会话2UPDATE product_id5会怎样核心结论会话2一定会阻塞。FOR UPDATE是当前读对命中行加X 排它锁X 与 X 互斥与隔离级别无关。RR 与 RC 的区别不是锁不锁而是锁多大范围有没有 Gap Lock。即使是 RCFOR UPDATE命中行的 X 锁仍然存在。快照读不加锁。如果会话2 改成普通SELECT瞬间返回。Q2会话1WHERE product_id 5 FOR UPDATE。会话2INSERT product_id6会阻塞吗INSERT product_id4呢核心结论INSERT 6阻塞INSERT 4放行。锁范围 BTree 扫描范围。5从 product_id5 的右侧开始扫组合区间 (5,∞)。(3,5] 不在扫描路径上product_id4 完全不被锁。如果是BETWEEN 8 AND 10扫到 10 之后还要再扫下一条不匹配行15才停会溢出锁右侧 Next-Key。Q3SELECT ... FOR UPDATE当前读不走 MVCC怎么防幻读核心结论RR 下通过Next-Key LockRecord Gap在 WHERE 覆盖的全部间隙 行上加锁Gap Lock 只互斥 INSERT → 新行物理上插不进来 → 幻读不可能。混读幻读不是 ANSI 幻读先快照读SELECT5 行→ 别人插入 → 再当前读FOR UPDATE6 行这是「读模式错乱」不是幻读ANSI 幻读定义是两次相同类型查询之间差异。Gap Lock 之间不互斥反常识。两个事务都可以在同一段 Gap 上立 Gap Lock互不冲突。只有 INSERT 想进来时才真冲突。二、核心原理 6 句快照读不加锁 / 当前读加锁。普通SELECT 快照读MVCC ReadViewSELECT ... FOR UPDATE / LOCK IN SHARE MODE / UPDATE / DELETE 当前读先读最新再加锁。加锁单位 Next-Key Lock前间隙 Gap 记录本身 Record前开后闭(prev, current]组合形式LOCK_MODEX无后缀。唯一索引PK/UK 等值 命中 → 降级为纯 Record LockLOCK_MODEX,REC_NOT_GAP。底层原因UNIQUE 约束替 Gap Lock 完成了防幻读InnoDB 直接跳过 Gap。普通非唯一索引等值不降级命中行 Next-KeyX右侧第一条不匹配行退化为纯 Gap LockX,GAP防止插入到已扫描区域的右边界。无索引全表扫typeALL→ 锁死整张表。InnoDB 对扫描路径上每条记录都加 Next-Key从第一行到 supremum 伪记录组合区间(-∞, ∞)。二级索引FOR UPDATE 两把锁先锁二级索引行 1 把回表时再锁对应聚簇 PRIMARY 行 1 把聚簇行锁总是REC_NOT_GAP因为主键唯一定位不需要间隙保护。三、锁分类速查分类锁类型触发方式互斥关系行级锁共享锁 SSELECT ... LOCK IN SHARE MODES-S 兼容S-X 互斥排他锁 XSELECT ... FOR UPDATE/UPDATE/DELETEX-X 互斥X-S 互斥表级锁意向锁 IS/IXInnoDB 自动添加表级意向锁之间兼容MDL 元数据锁ALTER TABLE等 DDLServer 层保护表结构意向锁作用告知表内某行已加行锁避免 DDL 全表扫描判断。四、Next-Key Lock 加锁规则RR 速查表核心公式临键锁 记录锁 间隙锁区间左开右闭(prev, current]。两个基本原则加锁基本单位是 Next-Key Lock前开后闭查找过程中访问到的对象才会加锁查询场景加锁类型锁区间唯一索引等值命中退化为记录锁仅锁命中记录唯一索引等值未命中退化为间隙锁未命中值所在间隙非唯一索引等值命中Next-Key Lock 向右遍历记录前间隙可能扩展到后不同值前间隙非唯一索引等值未命中退化为间隙锁未命中值所在间隙范围查询Next-Key Lock访问到的记录及前间隙可能延伸到满足条件后第一个间隙无索引/索引失效Next-Key Lock 全表整表范围(-∞, ∞)为什么必须锁区间而非只锁相同值若只防相同值事务A 范围查询后事务B 插入区间内不同值即可逃脱导致 A 再次查询结果集多行 → 幻读。必须物理锁死整个扫描路径区间。插入意向锁INSERT在 RR 下加的是轻量级间隙锁目标间隙未被临键锁占用→ 立即获得插入成功目标间隙已被临键锁占用→ 阻塞等待直至释放不同间隙的插入完全兼容插 id6 和 id12 互不干扰五、RC vs RR 的锁差异维度RCRR间隙锁关闭几乎不加 Gap Lock开启Next-Key Lock幻读风险可能幻读快照读无幻读当前读靠锁防并发性能更好更差死锁概率较低较高GAP 锁范围大六、RR 幻读的完整真相纯快照读RR 无幻读。ReadView 冻结新插入行trx_id max_trx_id不可见。纯当前读RR 用 Next-Key Lock 防幻读锁住范围阻止插入。⚠️ 混合场景类幻读事务A 普通SELECT快照读看到旧行 5 条事务B 插入新行并提交事务A 执行UPDATE/SELECT FOR UPDATE当前读读到新行 6 条严格说这是快照读结果与当前读结果的差异不是两次快照读不一致更进一步A 对新行 UPDATE 后后续普通 SELECT 该行也可能变可见因 UPDATE 把新行纳入事务上下文带creator_trx_id→ 对自己可见面试建议回答MySQL InnoDB 的 RR 在大多数业务场景防住了幻读普通 SELECT 走 MVCC 快照读复用 ReadViewSELECT FOR UPDATE/UPDATE/DELETE 走 Next-Key Lock。但在快照读和当前读混合使用的场景下可能出现业务层面的类幻读现象因此不能说 RR 在所有语义层面完全等同于 Serializable。七、data_locks 4 字段速记SELECT*FROMperformance_schema.data_locksWHEREOBJECT_NAMEyour_table;字段核心含义定位动作LOCK_TYPETABLE意向锁可忽略RECORD行/间隙锁真正要查的过滤RECORDLOCK_MODE✦① 有REC_NOT_GAP→ 纯行锁② 有GAP→ 纯间隙③ 无后缀 → Next-Key 合体一眼认清锁类型LOCK_STATUSGRANTED已持有WAITING阻塞入口找 WAITING 行的 LOCK_DATA → 找同值 GRANTED 行 → 其ENGINE_TRANSACTION_ID源头LOCK_DATAPK (主键)二级 (索引列, 主键)supremum 正无穷间隙锁 WAITING 的锚点是间隙的右边界不是左边界八、死锁剖析与排查8.1 死锁构造时间轴 T1-T4T1 A锁 3 T2 B锁 5 T3 A等 5hang T4 B等 3秒级死锁触发结果WE ROLL BACK TRANSACTION (n)。InnoDB 选undo log 条目最少修改量最小的事务回滚。锁依赖循环图事务A (TRX 2312) 事务B (TRX 2313) ┌──────────────────────┐ ┌──────────────────────┐ │ HOLDS: product_id3 │──►│ WAITS: product_id3 │ │ WAITS: product_id5 ◄│ │ HOLDS: product_id5 │ └──────────────────────┘ └──────────────────────┘ ◄──────── 循环等待 Circular Wait ────────► ★ 死锁 ★8.2 排查工具SHOWENGINEINNODBSTATUS\G关注LATEST DETECTED DEADLOCK段TRANSACTION 1WAITING FOR THIS LOCK TO BE GRANTED在等什么锁TRANSACTION 2HOLDS THE LOCK(S)持有什么锁WE ROLL BACK TRANSACTION (X)回滚了哪个事务lock_mode X locks gap before rec insert intention waiting等待插入意向的间隙锁生产人工定位 6 步① 抓日志 → ② 找 LATEST 段 → ③ 列两事务 HOLDS/WAITING hex 翻十进制 → ④ 画依赖图找环 → ⑤ 映射 SQL 到应用代码 → ⑥ 按统一加锁顺序修复。BIGINT hex 翻十进制技巧InnoDB STATUS 中原始 hex 首字节为0x80符号位翻转去掉0x80后 7 字节直接转如8000000000000005→ 5。8.3 RC 下典型死锁S 锁升级 X 锁三个事务并发插入id1事务A 插入成功持有id1的X 锁事务B、C 插入冲突被阻塞各自挂S 锁事务A 回滚释放 X 锁B、C 同时被唤醒均尝试将 S 锁升级为 X 锁→ 互斥 → 死锁8.4 RR 下增强死锁临键 S 锁 间隙互斥步骤 2 中 B、C 挂的是临键 S 锁锁记录 前间隙步骤 4 不仅 S 升 X 互斥间隙锁范围重叠 → 死锁概率更高波及邻近区间插入九、TMS 库存扣减 5 层防死锁方案口诀排序切循环 小事务切时间 唯一索引切范围 参数切雪崩 重试兜底。第① 层排序 禁止前端乱序切循环 ★★★Transactional(timeout3,rollbackForException.class)publicPreDeductResultdoSortedDeduct(ListLongrawPids,MapLong,DeductItemdeductMap){// 无论传什么强行升序排序1 行代码根治 90% 顺序死锁ListLongsortedPidsnewArrayList(rawPids);Collections.sort(sortedPids);// 先一次性 FOR UPDATE 把所有行锁住按排序后的顺序加锁ListInventoryinventoriesinventoryMapper.selectForUpdateByPids(sortedPids);// 内存中校验库存和版本号for(Inventoryinv:inventories){DeductItemitemdeductMap.get(inv.getPid());if(inv.getStock()item.getQty())thrownewStockException(库存不足);inv.setStock(inv.getStock()-item.getQty());inv.setVersion(inv.getVersion()1);}// 批量更新此时锁已被当前事务占住updated 必然为 1for(Inventoryinv:inventories){inventoryMapper.updateById(inv);}returnPreDeductResult.success(sortedPids,deductMap);}关键点如果id3被其他事务锁住当前事务会卡在第一个 SELECT绝不会提前去请求id5的锁 → 彻底杜绝持有 5、等待 3的循环等待。第② 层REQUIRES_NEW 小事务切持有时间 ★★★ServicepublicclassInventorySmallTxService{Transactional(propagationPropagation.REQUIRES_NEW,timeout2)publicPreDeductResultpreDeductRequireNew(ListLongsortedPids,MapLong,DeductItemdeductMap){// 最佳实践一条 SQL 批量更新MySQL 支持 CASE WHEN// 加锁、释放只在一条 SQL 内完成事务时间 1次网络IO 1次锁请求intupdatedinventoryMapper.batchDeductWithCase(sortedPids,deductMap);if(updated!sortedPids.size())thrownewStockException(库存不足);returnPreDeductResult.success(sortedPids,deductMap);}}用独立事务强行截断长链路让核心扣减飞一样地提交释放锁把慢操作扔给外面没锁的事务去慢慢磨。使用前提不能套在分布式全局事务Seata/XA里不能让循环嵌套 REQUIRES_NEW 耗尽连接池业务允许最终一致性。第③ 层扣减 SQL 强校验切锁范围 ★★★!-- 上线前 EXPLAIN 必须keyuk_product_id, rows1 --updateiddeductWithVersionUPDATE inventory SET remaining remaining - #{qty}, version version 1 WHERE product_id #{productId} AND version #{version}/update上线前必须 EXPLAIN 确认key 是 UK/PK 且 rows1。 Inventory 表必须有uk_product_id唯一索引否则WHERE product_id可能退化为全表扫描 → Next-Key Lock 锁全表。第④ 层参数兜底切雪崩spring:datasource:hikari:connection-init-sql:SET SESSION innodb_lock_wait_timeout 5;[mysqld] innodb_lock_wait_timeout 5 ; 默认50s→5s快速失败 innodb_print_all_deadlocks ON ; 死锁自动写 error.log innodb_status_output_locks ON ; INNODB STATUS 输出详细锁第⑤ 层Spring Retry 死锁自动重试兜底Retryable(retryFor{DeadlockLoserDataAccessException.class,CannotAcquireLockException.class},maxAttempts3,backoffBackoff(delay100,multiplier2,maxDelay2000))Transactional(propagationPropagation.REQUIRES_NEW,rollbackForException.class)publicPreDeductResultdeductWithRetry(ListLongsortedPids,MapLong,DeductItemdeductMap){returnsvc.doSortedDeduct(sortedPids,deductMap);}RecoverpublicPreDeductResultrecover(DeadlockLoserDataAccessExceptione,ListLongsortedPids,MapLong,DeductItemm){returnPreDeductResult.fail(库存扣减重试 3 次仍死锁请稍后重试。冲突商品sortedPids);}5 层防御效果对比层手段死锁下降复杂度推荐度①统一升序加锁顺序100% 消除顺序死锁0.05 人日★★★★★②REQUIRES_NEW 小事务扣快放~3600×0.5 人日★★★★☆③扣减 SQL 强校验唯一索引rows1~100×1 人日★★★★☆④innodb_lock_wait_timeout 50→5s~10×0配置★★★☆☆⑤Spring Retry 死锁重试 3 次~3×0.5 人日★★★☆☆十、其他锁工具NOWAIT 与 SKIP LOCKED-- NOWAIT加锁失败立即报错不排队快速失败场景SELECT*FROMuserWHEREid1FORUPDATENOWAIT;-- SKIP LOCKED跳过已被锁定的行任务队列抢单场景SELECT*FROMtask_queueWHEREstatuspendingLIMIT10FORUPDATESKIP LOCKED;核心速查表锁类型速记LOCK_MODE含义XNext-Key Lock行 前间隙X,REC_NOT_GAP纯记录锁唯一索引等值命中降级X,GAP纯间隙锁唯一索引等值未命中 / 非唯一右侧边界S共享 Next-Key LockS,REC_NOT_GAP共享记录锁S,GAP共享间隙锁X,INSERT_INTENTION插入意向锁等待 Gap 释放加锁规则口诀唯一等值命中 → 降级行锁唯一等值未命中 → 降级间隙锁非唯一等值 → Next-Key 右边界 Gap范围查询 → Next-Key 一路向右无索引 → 全表锁死二级索引 FOR UPDATE → 二级锁 聚簇行锁两把。死锁防御口诀跨表按表名顺序同表按主键升序不同索引用主键更新。