前言
MySQL锁是并发控制的核心,很多死锁、超时、更新阻塞、幻读问题根源都来自锁。很多开发者分不清:表锁、行锁、意向锁、MDL锁、Gap锁、临键锁这些概念。
注意:MyISAM只支持表级锁;现在业务几乎全部使用InnoDB引擎,本文主要围绕 InnoDB 讲解,把所有锁分类、作用、场景、踩坑一次性讲清楚。
一、锁的大分类维度
可以从4个角度划分锁:
- 粒度维度:全局锁、表级锁、行级锁(记录锁)
- 兼容性维度:共享锁(S)、排他锁(X)
- 意向锁:意向共享IS、意向排他IX,表级别辅助锁
- InnoDB特殊锁:MDL元数据锁、Gap间隙锁、Next‑Key临键锁、插入意向锁
二、全局锁(Global Read Lock)
作用范围:整个MySQL实例所有库所有表
语法:
sql
FLUSH TABLES WITH READ LOCK;
- 加锁之后,所有表只能读,DML、DDL全部阻塞;
- 典型场景:全库逻辑备份(mysqldump --single‑transaction 可以避开全局锁)
- 风险:业务库不要随便执行,一旦加锁,所有写入全部卡住,线上灾难。
- 释放:
UNLOCK TABLES;或者会话断开自动释放。
生产建议:备份优先使用
--single‑transaction快照读,避免FTWRL全局锁。
三、表级锁(Table‑level lock)
作用范围:整张数据表,分为两种:表读锁、表写锁
1)表读锁(共享锁)
sql
LOCK TABLES t1 READ;
- 当前会话:可读,不可写;
- 其他会话:可读,写操作阻塞。
2)表写锁(排他锁)
sql
LOCK TABLES t1 WRITE;
- 当前会话:可读可写;
- 其他会话:读、写全部阻塞。
MyISAM引擎默认使用表锁;InnoDB尽量不要手动使用
LOCK TABLES,InnoDB有行锁,手动表锁会破坏InnoDB事务特性。
3)MDL元数据锁(Metadata Lock,元数据锁,非常容易踩坑)
MDL锁是InnoDB 5.6之后,访问表的时候自动获取,不需要手动加锁,DDL操作会拿MDL排他锁。
- MDL共享锁:执行select / insert / update / delete DML自动拿MDL‑S;多个会话可以同时持有。
- MDL排他锁:alter table、drop table、rename等DDL拿MDL‑X;排他锁和所有MDL‑S互斥。
经典线上故障场景
- 会话A执行一条慢查询SELECT,持有MDL‑S,SQL执行很慢没有结束;
- DBA执行alter table修改表结构,申请MDL‑X,被阻塞;
- 后续所有新的DML请求,都需要申请MDL‑S,会被等待中的MDL‑X堵住;
- 整个表全部卡死,大量连接堆积,数据库雪崩。
MDL锁在事务提交之后才释放,不是SQL执行完就释放 。
坑:长事务持有MDL‑S,会直接阻塞DDL,进而堵死整个表业务。
四、意向锁 Intention Lock(IS / IX)
意向锁是InnoDB表级锁,不会阻塞普通DML,是用来协调表锁和行锁之间冲突检测,用户不能手动操作,InnoDB内部自动维护。
- IS 意向共享锁:事务打算给表中部分行加共享S行锁,先给表加上IS意向锁。
- IX 意向排他锁:事务打算给表中部分行加排他X行锁,先给表加上IX意向锁。
意向锁兼容性矩阵:
| X表锁 | S表锁 | IX意向锁 | IS意向锁 | |
|---|---|---|---|---|
| X表锁 | 冲突 | 冲突 | 冲突 | 冲突 |
| S表锁 | 冲突 | 兼容 | 冲突 | 兼容 |
| IX意向锁 | 冲突 | 冲突 | 兼容 | 兼容 |
| IS意向锁 | 冲突 | 兼容 | 兼容 | 兼容 |
通俗理解:意向锁作用:当有人想加整张表的表锁的时候,通过意向锁快速判断表里有没有行锁存在。
五、行级锁 Record Lock(记录锁,只锁行)
InnoDB行锁是索引实现!!如果更新条件不走索引,行锁会升级为表锁,重大坑点。
两种基础行锁:
- S 共享锁(读锁)
select ... lock in share mode;
- 自己可以读;其他事务也可以加S锁读;但是不能加X锁修改该行。
- X 排他锁(写锁)
select ... for update;
- 获取行排他锁;其他事务不能读也不能写,等待锁释放。
兼容性:
| S行锁 | X行锁 | |
|---|---|---|
| S行锁 | 兼容 | 互斥 |
| X行锁 | 互斥 | 互斥 |
重要提醒:
update / delete 会自动给匹配行加上X排他行锁,不需要手动for update;
where条件没有索引,InnoDB无法定位行,会锁住全表,等价表锁效果。
六、InnoDB RR隔离级别下的三种特殊锁(解决幻读核心)
InnoDB 在 REPEATABLE‑READ(RR)可重复读隔离级别,为了解决幻读,引入 Gap锁、临键锁、插入意向锁。
1. Gap Lock 间隙锁
锁的是索引记录之间的间隙,不是锁行,防止其他事务在间隙插入新数据,以此解决幻读。
只在RR隔离级别生效;RC隔离级别没有间隙锁。
举例:索引值 10,20,30
更新 where id=20;间隙会锁住 (10,20)、(20,30) 这个区间,别的事务不能往这个间隙insert。
间隙锁之间互相兼容:多个事务可以持有同一个间隙锁;间隙锁只阻塞插入,不阻塞修改已有记录。
2. Next‑Key Lock 临键锁(行锁+间隙锁组合)
InnoDB RR下默认行锁算法 = 临键锁
临键锁 = Record记录锁 + Gap间隙锁,左开右闭区间。
- 命中确切索引记录:临键锁退化成单纯记录锁(只锁那一行)
- 没有命中任何记录:保留间隙锁,阻止插入,避免幻读。
幻读:同一个事务内,同样select,第二次查询多出了别人新插入的数据。RR靠临键锁解决幻读。
3. Insert Intention Lock 插入意向锁
属于Gap间隙锁的一种,Insert语句插入数据的时候获取。
多个事务向同一个间隙插入不同记录,互相不会阻塞,提升并发插入性能。
七、快照读 & 当前读(锁和MVCC关系)
-
快照读(普通select):不加锁 ,走MVCC多版本,读取历史快照,不会产生锁冲突。
SELECT * FROM t where id=1; -
当前读:加锁读取,读取最新数据
select ... for update/select ... lock in share mode/ update / delete / insert执行当前读,才会触发行锁、临键锁、间隙锁。
很多同学疑惑:为什么普通select不会阻塞?普通select是快照读,不上锁。
八、所有锁汇总一览表
| 锁名称 | 锁粒度 | 手动控制 | 主要作用 |
|---|---|---|---|
| 全局锁 | 整个实例 | FLUSH TABLES WITH READ LOCK | 全库只读,备份场景 |
| 表读锁S | 整张表 | LOCK TABLES READ | 整张表只读 |
| 表写锁X | 整张表 | LOCK TABLES WRITE | 整张表独占读写 |
| MDL元数据锁 | 表 | 自动获取 | 保护表元数据,DDL/DML互斥 |
| IS意向共享锁 | 表 | 自动 | 辅助检测表锁与行锁冲突 |
| IX意向排他锁 | 表 | 自动 | 辅助检测表锁与行锁冲突 |
| Record S共享行锁 | 索引行 | LOCK IN SHARE MODE | 行共享读锁 |
| Record X排他行锁 | 索引行 | FOR UPDATE、DML | 行写锁,修改行 |
| Gap间隙锁 | 索引间隙 | 自动(RR) | 阻止间隙插入,防幻读 |
| Next‑Key临键锁 | 行+间隙 | 自动(RR) | RR默认行锁算法,解决幻读 |
| 插入意向锁 | 索引间隙 | 自动(insert) | 优化并发插入 |
九、线上高频锁踩坑总结
坑1:where条件无索引,行锁退化为全表锁
sql
update user set name='xxx' where phone='138xxxx';
phone字段没有索引,InnoDB无法定位行,扫描全表给每一行上X锁,整个表更新全部阻塞。
行锁是索引实现,没有索引就没有行锁。
坑2:长事务导致锁不释放
事务执行完不commit,行锁、MDL锁一直占有,其他会话阻塞,产生大量锁等待、死锁。
规范:业务事务尽量小,不要在事务里面做网络IO、外部http调用。
坑3:RR隔离级别间隙锁带来意外阻塞
RR级别下更新一个不存在的数据,会加Gap间隙锁,导致别的会话insert被卡住。
如果业务可以接受幻读,可考虑RC隔离级别,RC没有间隙锁,并发更高。
坑4:MDL锁雪崩
慢SQL持有MDL‑S,接着执行alter table DDL,会堵死整张表所有DML。
上线DDL避开业务高峰,先确认没有长事务慢查询。
坑5:分不清快照读与当前读
普通select不上锁;for update、update、delete属于当前读,会上行锁。
坑6:意向锁手动无法操作,不要去纠结怎么加意向锁
意向锁是InnoDB内部机制,开发者不需要写SQL操作。
十、如何排查锁问题
show engine innodb status;查看最近死锁日志information_schema.innodb_trx当前运行事务information_schema.innodb_locks当前锁信息performance_schema监控MDL锁等待- 慢查询日志,找出长事务、慢DML
十一、锁相关开发规范
- InnoDB尽量使用行锁,优先保证update/delete where条件命中索引;
- 事务尽可能短小,避免大事务、长事务;
- DDL变更避开业务高峰,排查是否存在运行中的长事务;
- 理解RC和RR锁行为差异:RC没有间隙锁,不会防幻读;RR带间隙锁解决幻读,但会带来额外插入阻塞;
- 普通查询优先快照读,不要随便滥用
select ... for update; - 尽量少手动使用
LOCK TABLES表锁,会破坏InnoDB事务特性。
总结
- MySQL锁粒度从大到小:全局锁 > 表锁 > 行锁;InnoDB优先使用行锁,行锁依赖索引。
- MDL元数据锁,DDL阻塞事故头号元凶,事务结束才释放。
- 意向锁IS/IX只是表级别辅助检测,用户不能手动控制。
- RR隔离级别才有Gap间隙锁、Next‑Key临键锁,目的解决幻读;RC没有间隙锁,并发更高。
- 普通select快照读不加锁;for update、update、delete属于当前读会上锁。
- 行锁失效变表锁最常见原因:更新条件不走索引。
- 锁不释放大多来自长事务,写业务一定要控制事务大小。
记住:InnoDB行锁锁住的不是物理记录,锁住的是索引。没有索引,行锁不复存在。