【计算基础|MySQL14】MySQL 锁(上):种类与加锁规则
前两篇用实验把事务隔离和锁现象跑通了。这篇回到原理,回答一个面试必问、生产必踩的问题:MySQL 到底有哪些锁?一条 SQL 发出去,InnoDB 凭什么决定锁哪里、锁多大范围?
很多人学锁是乱的:一会儿行锁一会儿表锁,一会儿间隙锁一会儿意向锁,名词堆了一堆却串不起来。这篇先用一张分类树把所有锁摆上桌,再按"全局锁→表级锁→行级锁"三层往下拆,最后给 7 条加锁规则。看完你应该能看着一条 UPDATE SQL,在脑子里说出它会加哪些锁、锁哪些记录。
环境:本文原理基于 MySQL 8.0 InnoDB,RR 隔离级别(默认)。涉及
data_locks的结论来自上一篇实战复现。
一、先把锁的分类树画出来
MySQL 的锁不是一个平面概念,而是从多个维度切出来的。一张图看全:
#mermaid-svg-T7OSadYoMhUBOnhH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-T7OSadYoMhUBOnhH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-T7OSadYoMhUBOnhH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-T7OSadYoMhUBOnhH .error-icon{fill:#552222;}#mermaid-svg-T7OSadYoMhUBOnhH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-T7OSadYoMhUBOnhH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-T7OSadYoMhUBOnhH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-T7OSadYoMhUBOnhH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-T7OSadYoMhUBOnhH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-T7OSadYoMhUBOnhH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-T7OSadYoMhUBOnhH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-T7OSadYoMhUBOnhH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-T7OSadYoMhUBOnhH .marker.cross{stroke:#333333;}#mermaid-svg-T7OSadYoMhUBOnhH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-T7OSadYoMhUBOnhH p{margin:0;}#mermaid-svg-T7OSadYoMhUBOnhH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-T7OSadYoMhUBOnhH .cluster-label text{fill:#333;}#mermaid-svg-T7OSadYoMhUBOnhH .cluster-label span{color:#333;}#mermaid-svg-T7OSadYoMhUBOnhH .cluster-label span p{background-color:transparent;}#mermaid-svg-T7OSadYoMhUBOnhH .label text,#mermaid-svg-T7OSadYoMhUBOnhH span{fill:#333;color:#333;}#mermaid-svg-T7OSadYoMhUBOnhH .node rect,#mermaid-svg-T7OSadYoMhUBOnhH .node circle,#mermaid-svg-T7OSadYoMhUBOnhH .node ellipse,#mermaid-svg-T7OSadYoMhUBOnhH .node polygon,#mermaid-svg-T7OSadYoMhUBOnhH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-T7OSadYoMhUBOnhH .rough-node .label text,#mermaid-svg-T7OSadYoMhUBOnhH .node .label text,#mermaid-svg-T7OSadYoMhUBOnhH .image-shape .label,#mermaid-svg-T7OSadYoMhUBOnhH .icon-shape .label{text-anchor:middle;}#mermaid-svg-T7OSadYoMhUBOnhH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-T7OSadYoMhUBOnhH .rough-node .label,#mermaid-svg-T7OSadYoMhUBOnhH .node .label,#mermaid-svg-T7OSadYoMhUBOnhH .image-shape .label,#mermaid-svg-T7OSadYoMhUBOnhH .icon-shape .label{text-align:center;}#mermaid-svg-T7OSadYoMhUBOnhH .node.clickable{cursor:pointer;}#mermaid-svg-T7OSadYoMhUBOnhH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-T7OSadYoMhUBOnhH .arrowheadPath{fill:#333333;}#mermaid-svg-T7OSadYoMhUBOnhH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-T7OSadYoMhUBOnhH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-T7OSadYoMhUBOnhH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T7OSadYoMhUBOnhH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-T7OSadYoMhUBOnhH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T7OSadYoMhUBOnhH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-T7OSadYoMhUBOnhH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-T7OSadYoMhUBOnhH .cluster text{fill:#333;}#mermaid-svg-T7OSadYoMhUBOnhH .cluster span{color:#333;}#mermaid-svg-T7OSadYoMhUBOnhH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-T7OSadYoMhUBOnhH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-T7OSadYoMhUBOnhH rect.text{fill:none;stroke-width:0;}#mermaid-svg-T7OSadYoMhUBOnhH .icon-shape,#mermaid-svg-T7OSadYoMhUBOnhH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T7OSadYoMhUBOnhH .icon-shape p,#mermaid-svg-T7OSadYoMhUBOnhH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-T7OSadYoMhUBOnhH .icon-shape .label rect,#mermaid-svg-T7OSadYoMhUBOnhH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T7OSadYoMhUBOnhH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-T7OSadYoMhUBOnhH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-T7OSadYoMhUBOnhH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MySQL 锁
按粒度
按模式
按态度
按 InnoDB 实现
全局锁 FTWRL
表级锁
行级锁
共享锁 S / 读锁
排他锁 X / 写锁
悲观锁: 先加锁再操作
乐观锁: 版本号/CAS
LOCK TABLES 显式表锁
MDL 元数据锁
意向锁 IS / IX
AUTO-INC 自增锁
记录锁 Record Lock
间隙锁 Gap Lock
Next-Key Lock
插入意向锁
🔴 这张图是本篇的骨架。下面按粒度从粗到细讲。
二、全局锁:FLUSH TABLES WITH READ LOCK
结论 :全局锁就是把整个数据库实例设成只读,几乎只用于全库逻辑备份。
sql
FLUSH TABLES WITH READ LOCK; -- 加全局读锁
-- 此时所有库的所有写操作(INSERT/UPDATE/DDL)全部阻塞
UNLOCK TABLES; -- 释放
| 项 | 说明 |
|---|---|
| 作用范围 | 整个实例,所有库所有表 |
| 谁阻塞谁 | 加锁后,所有会话的写操作、DDL 全部阻塞,只能读 |
| 典型用途 | 全库逻辑备份(MyISAM 不支持事务,只能靠它保证备份一致性) |
⚠️ 避坑 :现在用 InnoDB,备份不需要全局锁。mysqldump --single-transaction 靠事务一致性快照(RR 下的 ReadView)就能拿到一致性备份,全程不加全局锁、不阻塞写 。只有 MyISAM 这种不支持事务的引擎才不得不 FTWRL。线上千万别手贱敲 FTWRL,否则业务全挂。
三、表级锁
3.1 显式表锁:LOCK TABLES
sql
LOCK TABLES t READ; -- 加表读锁,其他会话能读不能写
LOCK TABLES t WRITE; -- 加表写锁,其他会话读写都阻塞
UNLOCK TABLES;
🔴 InnoDB 一般不用这个。它和 InnoDB 的行锁模型冲突(InnoDB 自己管行锁,你再手动锁表等于把并发废掉)。知道有这东西就行,生产基本见不到。
3.2 MDL 元数据锁(重点坑)
结论 :访问一张表时 InnoDB/Server 层自动加 MDL,读操作加 MDL 读锁(S),DDL 加 MDL 写锁(X);MDL 锁要到事务提交才释放。这是线上"一个 ALTER 把库打满"的元凶。
机制:
会话C(ALTER) 会话B(新查询) MySQL 会话A(长查询) 会话C(ALTER) 会话B(新查询) MySQL 会话A(长查询) #mermaid-svg-6nsLjdvhAFi327if{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6nsLjdvhAFi327if .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6nsLjdvhAFi327if .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6nsLjdvhAFi327if .error-icon{fill:#552222;}#mermaid-svg-6nsLjdvhAFi327if .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6nsLjdvhAFi327if .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6nsLjdvhAFi327if .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6nsLjdvhAFi327if .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6nsLjdvhAFi327if .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6nsLjdvhAFi327if .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6nsLjdvhAFi327if .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6nsLjdvhAFi327if .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6nsLjdvhAFi327if .marker.cross{stroke:#333333;}#mermaid-svg-6nsLjdvhAFi327if svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6nsLjdvhAFi327if p{margin:0;}#mermaid-svg-6nsLjdvhAFi327if .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6nsLjdvhAFi327if text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-6nsLjdvhAFi327if .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6nsLjdvhAFi327if .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-6nsLjdvhAFi327if .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-6nsLjdvhAFi327if .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-6nsLjdvhAFi327if #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-6nsLjdvhAFi327if .sequenceNumber{fill:white;}#mermaid-svg-6nsLjdvhAFi327if #sequencenumber{fill:#333;}#mermaid-svg-6nsLjdvhAFi327if #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-6nsLjdvhAFi327if .messageText{fill:#333;stroke:none;}#mermaid-svg-6nsLjdvhAFi327if .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6nsLjdvhAFi327if .labelText,#mermaid-svg-6nsLjdvhAFi327if .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-6nsLjdvhAFi327if .loopText,#mermaid-svg-6nsLjdvhAFi327if .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-6nsLjdvhAFi327if .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6nsLjdvhAFi327if .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-6nsLjdvhAFi327if .noteText,#mermaid-svg-6nsLjdvhAFi327if .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-6nsLjdvhAFi327if .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6nsLjdvhAFi327if .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6nsLjdvhAFi327if .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6nsLjdvhAFi327if .actorPopupMenu{position:absolute;}#mermaid-svg-6nsLjdvhAFi327if .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-6nsLjdvhAFi327if .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6nsLjdvhAFi327if .actor-man circle,#mermaid-svg-6nsLjdvhAFi327if line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-6nsLjdvhAFi327if :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} A 事务一直没提交,MDL S 不释放 X 与 S 互斥,C 排队等 A 新 S 和排队的 X 也互斥!B 被堵住 SELECT ... FROM t (拿 MDL 读锁S)ALTER TABLE t (要 MDL 写锁X)SELECT ... FROM t (也要 MDL 读锁S)后面所有新查询全部堵死,连接迅速打满
🔴 致命点:MDL 读锁之间是兼容的,但一旦中间来了一个要 MDL 写锁的 ALTER,后面所有新来的 MDL 读锁都得排队等那个写锁------因为写锁优先级高,不能让新读锁插队。于是"一个慢查询 + 一个 ALTER" = 整个表的业务瘫痪。
怎么解决:
- ALTER 之前先查有没有长事务:
SELECT * FROM information_schema.innodb_trx; - 大表 DDL 别用原生 ALTER,用
pt-online-schema-change或gh-ost,它们通过影子表+行级增量同步,不长期持有 MDL 写锁。 - MySQL 8.0 支持
LOCK WAIT N/NOWAIT:ALTER TABLE t NOWAIT ADD COLUMN ...,拿不到 MDL 写锁就立刻报错,不排队堵业务。
3.3 意向锁:IS / IX
结论 :意向锁是表级锁,声明"我待会要在这张表的某些行上加 S/X 锁"。它的唯一作用是让表锁能快速判断要不要和行锁冲突,不用逐行检查。
#mermaid-svg-ZsVGxeMSkdAb0RBp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZsVGxeMSkdAb0RBp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZsVGxeMSkdAb0RBp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZsVGxeMSkdAb0RBp .error-icon{fill:#552222;}#mermaid-svg-ZsVGxeMSkdAb0RBp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZsVGxeMSkdAb0RBp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZsVGxeMSkdAb0RBp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZsVGxeMSkdAb0RBp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZsVGxeMSkdAb0RBp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZsVGxeMSkdAb0RBp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZsVGxeMSkdAb0RBp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZsVGxeMSkdAb0RBp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZsVGxeMSkdAb0RBp .marker.cross{stroke:#333333;}#mermaid-svg-ZsVGxeMSkdAb0RBp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZsVGxeMSkdAb0RBp p{margin:0;}#mermaid-svg-ZsVGxeMSkdAb0RBp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZsVGxeMSkdAb0RBp .cluster-label text{fill:#333;}#mermaid-svg-ZsVGxeMSkdAb0RBp .cluster-label span{color:#333;}#mermaid-svg-ZsVGxeMSkdAb0RBp .cluster-label span p{background-color:transparent;}#mermaid-svg-ZsVGxeMSkdAb0RBp .label text,#mermaid-svg-ZsVGxeMSkdAb0RBp span{fill:#333;color:#333;}#mermaid-svg-ZsVGxeMSkdAb0RBp .node rect,#mermaid-svg-ZsVGxeMSkdAb0RBp .node circle,#mermaid-svg-ZsVGxeMSkdAb0RBp .node ellipse,#mermaid-svg-ZsVGxeMSkdAb0RBp .node polygon,#mermaid-svg-ZsVGxeMSkdAb0RBp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZsVGxeMSkdAb0RBp .rough-node .label text,#mermaid-svg-ZsVGxeMSkdAb0RBp .node .label text,#mermaid-svg-ZsVGxeMSkdAb0RBp .image-shape .label,#mermaid-svg-ZsVGxeMSkdAb0RBp .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZsVGxeMSkdAb0RBp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZsVGxeMSkdAb0RBp .rough-node .label,#mermaid-svg-ZsVGxeMSkdAb0RBp .node .label,#mermaid-svg-ZsVGxeMSkdAb0RBp .image-shape .label,#mermaid-svg-ZsVGxeMSkdAb0RBp .icon-shape .label{text-align:center;}#mermaid-svg-ZsVGxeMSkdAb0RBp .node.clickable{cursor:pointer;}#mermaid-svg-ZsVGxeMSkdAb0RBp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZsVGxeMSkdAb0RBp .arrowheadPath{fill:#333333;}#mermaid-svg-ZsVGxeMSkdAb0RBp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZsVGxeMSkdAb0RBp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZsVGxeMSkdAb0RBp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZsVGxeMSkdAb0RBp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZsVGxeMSkdAb0RBp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZsVGxeMSkdAb0RBp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZsVGxeMSkdAb0RBp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZsVGxeMSkdAb0RBp .cluster text{fill:#333;}#mermaid-svg-ZsVGxeMSkdAb0RBp .cluster span{color:#333;}#mermaid-svg-ZsVGxeMSkdAb0RBp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZsVGxeMSkdAb0RBp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZsVGxeMSkdAb0RBp rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZsVGxeMSkdAb0RBp .icon-shape,#mermaid-svg-ZsVGxeMSkdAb0RBp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZsVGxeMSkdAb0RBp .icon-shape p,#mermaid-svg-ZsVGxeMSkdAb0RBp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZsVGxeMSkdAb0RBp .icon-shape .label rect,#mermaid-svg-ZsVGxeMSkdAb0RBp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZsVGxeMSkdAb0RBp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZsVGxeMSkdAb0RBp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZsVGxeMSkdAb0RBp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 意向锁兼容矩阵
兼容
兼容
兼容
兼容
互斥
互斥
意向共享 IS
IS
意向排他 IX
IX
IS
表写锁
表读锁
关键性质(背下来):
| 组合 | 是否兼容 |
|---|---|
| IS 和 IS | ✅ 兼容 |
| IS 和 IX | ✅ 兼容 |
| IX 和 IX | ✅ 兼容(所以两个事务各锁不同行不打架) |
| IS 和 表写锁(X) | ❌ 互斥 |
| IX 和 表读锁(S) / 表写锁(X) | ❌ 互斥 |
🔴 面试怎么答 :意向锁之间互不阻塞,所以高并发下行锁(自动带 IX)不会互相堵;它存在的意义是------当有人想 LOCK TABLES t WRITE 加表写锁时,MySQL 扫一眼表上有没有 IX,有就知道"有事务正在锁行,表锁加不上",而不用把每一行的锁都翻出来查。
3.4 AUTO-INC 自增锁
给自增主键插数据时,InnoDB 在表上加一个特殊的表级锁,保证 AUTO_INCREMENT 不重号。由参数 innodb_autoinc_lock_mode 控制:
| 值 | 模式 | 行为 | 主从(statement)安全 |
|---|---|---|---|
| 0 | 传统 | 整个 INSERT 语句执行完才释放表锁 | 安全 |
| 1 | 连续(MySQL 5默认) | 简单 INSERT 一次分配连续值,批量插入提前知道行数时连续 | 安全 |
| 2 | 交错(MySQL 8默认) | 插入时并发分配,自增可能不连续 | 不安全 |
⚠️ MySQL 8 默认 innodb_autoinc_lock_mode=2,配合基于行的 binlog(binlog_format=ROW)是安全的;如果还在用 statement 格式,自增值在主从上可能不一致。这个参数知道就行,一般不用动。
四、行级锁(InnoDB 的精华)
4.1 记录锁 Record Lock
锁索引上的单条记录。分 S 和 X 两种。
🔴 铁律 :行锁加在索引上。如果 WHERE 条件没用到索引,InnoDB 只能全表扫描,给扫到的每一行都加锁------等于锁全表(上一篇实战 4 已复现)。
4.2 间隙锁 Gap Lock
锁索引记录之间的开区间,目的是防止别的事务往间隙里插入新记录(防幻读)。
#mermaid-svg-ZNiHEx5ciUs4POyB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZNiHEx5ciUs4POyB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZNiHEx5ciUs4POyB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZNiHEx5ciUs4POyB .error-icon{fill:#552222;}#mermaid-svg-ZNiHEx5ciUs4POyB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZNiHEx5ciUs4POyB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZNiHEx5ciUs4POyB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZNiHEx5ciUs4POyB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZNiHEx5ciUs4POyB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZNiHEx5ciUs4POyB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZNiHEx5ciUs4POyB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZNiHEx5ciUs4POyB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZNiHEx5ciUs4POyB .marker.cross{stroke:#333333;}#mermaid-svg-ZNiHEx5ciUs4POyB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZNiHEx5ciUs4POyB p{margin:0;}#mermaid-svg-ZNiHEx5ciUs4POyB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZNiHEx5ciUs4POyB .cluster-label text{fill:#333;}#mermaid-svg-ZNiHEx5ciUs4POyB .cluster-label span{color:#333;}#mermaid-svg-ZNiHEx5ciUs4POyB .cluster-label span p{background-color:transparent;}#mermaid-svg-ZNiHEx5ciUs4POyB .label text,#mermaid-svg-ZNiHEx5ciUs4POyB span{fill:#333;color:#333;}#mermaid-svg-ZNiHEx5ciUs4POyB .node rect,#mermaid-svg-ZNiHEx5ciUs4POyB .node circle,#mermaid-svg-ZNiHEx5ciUs4POyB .node ellipse,#mermaid-svg-ZNiHEx5ciUs4POyB .node polygon,#mermaid-svg-ZNiHEx5ciUs4POyB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZNiHEx5ciUs4POyB .rough-node .label text,#mermaid-svg-ZNiHEx5ciUs4POyB .node .label text,#mermaid-svg-ZNiHEx5ciUs4POyB .image-shape .label,#mermaid-svg-ZNiHEx5ciUs4POyB .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZNiHEx5ciUs4POyB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZNiHEx5ciUs4POyB .rough-node .label,#mermaid-svg-ZNiHEx5ciUs4POyB .node .label,#mermaid-svg-ZNiHEx5ciUs4POyB .image-shape .label,#mermaid-svg-ZNiHEx5ciUs4POyB .icon-shape .label{text-align:center;}#mermaid-svg-ZNiHEx5ciUs4POyB .node.clickable{cursor:pointer;}#mermaid-svg-ZNiHEx5ciUs4POyB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZNiHEx5ciUs4POyB .arrowheadPath{fill:#333333;}#mermaid-svg-ZNiHEx5ciUs4POyB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZNiHEx5ciUs4POyB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZNiHEx5ciUs4POyB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZNiHEx5ciUs4POyB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZNiHEx5ciUs4POyB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZNiHEx5ciUs4POyB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZNiHEx5ciUs4POyB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZNiHEx5ciUs4POyB .cluster text{fill:#333;}#mermaid-svg-ZNiHEx5ciUs4POyB .cluster span{color:#333;}#mermaid-svg-ZNiHEx5ciUs4POyB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZNiHEx5ciUs4POyB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZNiHEx5ciUs4POyB rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZNiHEx5ciUs4POyB .icon-shape,#mermaid-svg-ZNiHEx5ciUs4POyB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZNiHEx5ciUs4POyB .icon-shape p,#mermaid-svg-ZNiHEx5ciUs4POyB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZNiHEx5ciUs4POyB .icon-shape .label rect,#mermaid-svg-ZNiHEx5ciUs4POyB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZNiHEx5ciUs4POyB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZNiHEx5ciUs4POyB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZNiHEx5ciUs4POyB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 索引记录上的间隙
(-∞,10]
(10,20]
(20,30]
(30,+∞)
关键性质:
- 只有 RR 隔离级别有间隙锁(RC 没有,RC 下外键/唯一键检查除外)。
- 间隙锁之间不互斥 :A 锁了
(20,30),B 也能锁(20,30),谁都不拦谁。 - 间隙锁只和"往间隙里 INSERT"的插入意向锁互斥。
4.3 Next-Key Lock
记录锁 + 间隙锁 = Next-Key Lock,锁的是左开右闭区间 (左边记录, 右边记录]。这是 InnoDB 行锁的默认算法。
假设索引上有记录 10、20、30,那么 Next-Key Lock 把整个索引切成:
text
(-∞, 10] (10, 20] (20, 30] (30, +∞)
为什么需要它?纯记录锁防不住幻读------你锁了 id=10,别人插个 id=15 你看不见。Next-Key Lock 把 10 和 20 之间的间隙也锁了,别人插不进来。
4.4 插入意向锁 Insert Intention Lock
INSERT 之前,在目标间隙上加的一种特殊间隙锁,表示"我打算在这插一条"。它和间隙锁互斥(间隙锁挡住插入),但插入意向锁之间互相兼容(都想往同一间隙插不同位置,不冲突)。上一篇实战 6 的死锁就是它引起的。
五、加锁基本规则(MySQL 8 InnoDB RR)
这是本篇最硬核的部分。看着一条 SQL,按下表判断它加什么锁:
| # | 场景 | 加什么锁 |
|---|---|---|
| 1 | 任何行锁 | 都加在索引上;没索引 → 全表所有记录都加锁(表锁效果) |
| 2 | 唯一索引等值查询,命中记录 | 退化为记录锁 X,REC_NOT_GAP,不加间隙 |
| 3 | 唯一索引等值查询,未命中 | 加间隙锁 X,GAP,锁住该值所在间隙 |
| 4 | 普通索引等值查询,命中 | Next-Key Lock + 对下一条记录再加间隙锁(普通索引可重复,要防幻读) |
| 5 | 普通索引等值查询,未命中 | 加间隙锁 |
| 6 | 范围查询 | Next-Key Lock,扫描到不满足条件的第一个值为止(包括界外第一个值的间隙) |
| 7 | LIMIT | 减少扫描行数,从而减少加锁范围 |
🔴 规则 2 是高频考点:唯一索引等值命中,Next-Key Lock 退化成记录锁 ------因为唯一索引保证不会再有第二条同样的记录,间隙没必要锁。这就是为什么上一篇实战 1 里 WHERE id=20 FOR UPDATE 是 X,REC_NOT_GAP 而不是 X。
死锁检测
sql
-- 默认开
SHOW VARIABLES LIKE 'innodb_deadlock_detect'; -- ON
开启后 InnoDB 用**等待图(wait-for graph)**实时检测有没有环,发现死锁立刻回滚 undo 量最小的事务。高并发下等待图本身有开销,极端场景可以关(但要靠 innodb_lock_wait_timeout 兜底),一般别动。
六、锁兼容矩阵(背这张)
| S 读锁 | X 写锁 | IS | IX | |
|---|---|---|---|---|
| S | ✅ | ❌ | ✅ | ❌ |
| X | ❌ | ❌ | ❌ | ❌ |
| IS | ✅ | ❌ | ✅ | ✅ |
| IX | ❌ | ❌ | ✅ | ✅ |
读法:行锁层面 S 和 S 兼容、S/X 互斥、X/X 互斥;意向锁 IS/IX 之间全兼容,只有和真正的表 S/X 锁才互斥。
七、悲观锁 vs 乐观锁
| 对比 | 悲观锁 | 乐观锁 |
|---|---|---|
| 思路 | 假设一定会冲突,先加锁再操作 | 假设不冲突,操作时检查版本,冲突了再重试 |
| 实现 | SELECT ... FOR UPDATE |
UPDATE t SET val=?, version=version+1 WHERE id=? AND version=? |
| 适合 | 写多读少、冲突激烈(秒杀扣库存) | 读多写少、冲突少(配置更新) |
| 代价 | 锁等待、死锁风险 | 更新失败要重试,ABA 问题要处理 |
乐观锁本质就是数据库里的 CAS(Compare-And-Swap):更新时带上版本号条件, affected rows=0 就说明被人改过了,重试。
八、面试高频(怎么考、怎么答)
- MySQL 锁分哪几类? 按粒度(全局/表/行)、按模式(S/X)、按态度(悲观/乐观)、按 InnoDB 实现(记录/间隙/Next-Key/意向/插入意向/AUTO-INC)。
- MDL 是什么坑? 长事务持有 MDL 读锁不释放,一个 ALTER 要 MDL 写锁排队,后续所有新查询都被堵死。解决:看长事务、用 pt-osc/gh-ost、
ALTER NOWAIT。 - 间隙锁和 Next-Key Lock 区别? 间隙锁锁开区间防插入;Next-Key Lock = 记录锁 + 间隙锁,锁左开右闭区间,是 RR 默认行锁算法。
- 意向锁解决什么问题? 不用逐行检查,快速判断表锁能不能加;意向锁之间不互斥。
- 加锁规则核心? 锁加在索引上;唯一索引等值命中退化为记录锁、未命中加间隙锁;范围查询加 Next-Key Lock 扫到不满足的第一个值。
- RC 和 RR 加锁区别? RC 没有间隙锁(并发高、当前读可能幻读),RR 有间隙锁(防幻读但容易死锁)。
- 怎么看当前加了什么锁?
performance_schema.data_locks(MySQL 8)、data_lock_waits、SHOW ENGINE INNODB STATUS。
总结:你真正需要记住的 6 件事
- 锁按粒度分三层:全局锁(FTWRL,备份用,InnoDB 用不到)、表级锁(MDL/意向锁/AUTO-INC)、行级锁(记录/间隙/Next-Key/插入意向)。
- MDL 是最隐蔽的坑:长查询 + ALTER = 新查询全堵死;生产大表 DDL 用 pt-osc/gh-ost。
- 意向锁 IS/IX 是表级"预告",互相兼容,只和真正的表 S/X 锁互斥,作用是快速判断能否加表锁。
- 行锁加在索引上:唯一索引等值命中→记录锁;未命中→间隙锁;范围查询→Next-Key Lock;没索引→锁全表。
- 间隙锁只有 RR 有,它之间不互斥、只挡插入,是"不同行也死锁"的根源。
- 悲观锁
FOR UPDATE适合写多冲突多;乐观锁版本号 CAS 适合读多写少。
验证清单
- 能画出 MySQL 锁的分类树
- 能说清 MDL 为什么会把库打满,以及三种解决办法
- 能默写意向锁兼容矩阵(IS/IX 互相兼容,IX 和 S 互斥)
- 能看着一条
WHERE id=20 FOR UPDATE说出它加X,REC_NOT_GAP - 能说清为什么唯一索引等值命中不加间隙锁
- 能解释 RC 和 RR 在间隙锁上的区别
- 知道用
data_locks看 LOCK_MODE 四种值
参考资源
- MySQL 8.0 官方文档:InnoDB 锁模式 https://dev.mysql.com/doc/refman/8.0/en/innodb-lock-modes.html
- MySQL 8.0:InnoDB 表级锁(意向锁/AUTO-INC/MDL) https://dev.mysql.com/doc/refman/8.0/en/innodb-table-locking.html
- MySQL 8.0:InnoDB 记录锁/间隙锁/Next-Key Lock https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html
- MySQL 8.0:innodb_autoinc_lock_mode https://dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html#sysvar_innodb_autoinc_lock_mode
下一篇《MySQL 锁(下):锁全表与死锁》:专门啃两个硬骨头------UPDATE 没索引为什么会锁全表(用 Mermaid 画全表扫描加锁过程)、间隙锁到底怎么防幻读;然后把死锁讲透:四个必要条件、五个常见死锁场景、等待图检测、以及七条预防策略。