Mysql中有哪些锁
1. MySQL 的锁在“哪一层”
MySQL 的锁可以按“生效对象”和“实现层次”理解:一部分在 Server 层(对实例、表、元数据做并发控制),另一部分在 InnoDB 引擎层(主要是事务并发控制的行锁体系)。
1.1 速查表
| 层次 | 锁类型 | 锁住什么 | 典型触发方式 | 典型使用场景 |
|---|---|---|---|---|
| Server 层 | 全局读锁(FTWRL) | 实例数据写入 | FLUSH TABLES WITH READ LOCK |
逻辑备份需要全局一致性 |
| Server 层 | 备份锁(Backup Lock) | 主要阻断 DDL | LOCK INSTANCE FOR BACKUP |
物理备份需要稳定元数据 |
| Server 层 | 表锁 | 表 | LOCK TABLES ... READ/WRITE |
MyISAM 串行化写入、强制互斥 |
| Server 层 | 元数据锁(MDL) | 表/库等对象的元数据 | 任何访问对象的 SQL 都会自动加 | 排查 DDL 被长事务卡住 |
| Server 层 | 命名锁(User-level Lock) | 逻辑资源名 | GET_LOCK() / RELEASE_LOCK() |
跨连接互斥:定时任务、迁移开关 |
| InnoDB 层 | 意向锁(IS/IX) | 表(声明意图) | 行锁前自动加 | 与表锁/MDL 做快速冲突判定 |
| InnoDB 层 | 记录锁(Record) | 某条索引记录 | UPDATE、DELETE、SELECT ... FOR UPDATE |
点查更新:按主键/唯一键修改 |
| InnoDB 层 | 间隙锁(Gap) | 索引间隙 | RR 隔离级别的范围锁定读 | 防幻读:禁止间隙插入 |
| InnoDB 层 | 临键锁(Next-Key) | 记录 + 前间隙 | RR 下范围条件更新/锁定读 | 防幻读:锁定区间 (a, b] |
| InnoDB 层 | 插入意向锁 | 间隙(插入意图) | INSERT 落在被锁间隙附近 |
提升并发插入,减少间隙互斥 |
| InnoDB 层 | 自增锁(AUTO-INC) | 自增计数器 | 含 AUTO_INCREMENT 的插入 |
批量插入分配自增值的并发控制 |
2. Server 层锁:全局、表、元数据、命名锁
2.1 全局读锁:FLUSH TABLES WITH READ LOCK(FTWRL)
FTWRL 会让实例进入“全局只读”的近似状态:会阻塞 DML(INSERT、UPDATE、DELETE)和大多数 DDL(ALTER TABLE 等),从而得到跨表一致的视图。
- 使用场景:
- 需要 全库逻辑备份一致性,并且不能依赖引擎事务能力(例如混用 MyISAM)。
- 少量运维窗口:短时间冻结写入,避免备份期间数据漂移。
- 注意点:
- 对在线业务冲击大,持锁时间越长,写入堆积越严重。
- 只用 InnoDB 做备份时,优先使用
mysqldump --single-transaction(利用 MVCC)来降低对写入的影响,避免长时间 FTWRL。
2.2 备份锁:LOCK INSTANCE FOR BACKUP
备份锁面向“备份一致性”设计,核心思路是:尽量不阻塞业务 DML,但会阻断或限制可能破坏备份一致性的 DDL / 元数据变更,使得备份过程元数据稳定。
- 版本提示:备份锁属于 MySQL 8.0 系列能力;如果是更老版本,常见替代手段是 FTWRL 或备份工具的特定方案。
- 使用场景:
- 物理备份(例如基于数据文件的备份)需要元数据稳定:避免备份过程中执行
CREATE/DROP/ALTER导致备份不可用或恢复失败。 - 与备份工具配合,让备份期间“可写但不可随意改结构”。
- 对比 FTWRL:
- FTWRL 更“粗暴”:整体阻塞写入。
- 备份锁更“精细”:优先保护元数据一致性,尽量放行业务写入。
2.3 表锁:LOCK TABLES ... READ/WRITE
表锁是显式的“整表互斥/共享”控制,适用于不支持行锁的引擎或需要强制串行化的场景。
- 使用场景:
- MyISAM:只有表级锁,写入并发依赖表锁。
- 明确要把某张表当作“临界区”使用:例如手工做数据修复/迁移,希望阻断并发写入,避免修复过程被打断。
- 常见用法:
LOCK TABLES t WRITE;
-- 临界区:此时其他连接无法读写 t
UPDATE t SET ... WHERE ...;
UNLOCK TABLES;
- 注意点:
- InnoDB 日常 OLTP 不建议依赖表锁控制并发,会显著降低吞吐;更推荐用事务 + 行锁(
SELECT ... FOR UPDATE)实现“只锁需要的行”。
2.4 元数据锁:Metadata Lock(MDL)
MDL 保护的是对象的“结构与定义”,不是行数据。只要你访问了表,就会自动持有 MDL:例如普通 SELECT 会获取 MDL 共享锁;而 DDL(ALTER TABLE)通常需要 MDL 排他锁。
- 使用场景(你不需要显式使用,但必须理解它的后果):
- 排查线上“
ALTER TABLE卡住很久”:常见原因是某个长事务或长查询一直持有 MDL 共享锁,DDL 需要等它释放。 - 规范治理:避免在高峰期做 DDL;尽量让事务短小,减少长时间持有 MDL 的机会。
- 典型现象:
- A 会话跑一个长查询:
SELECT ...(持有 MDL 共享锁,直到语句结束;若在显式事务中,可能持有更久)。 - B 会话执行
ALTER TABLE:需要 MDL 排他锁,被 A 阻塞,导致“DDL 排队”。 - 排查手段(建议开启
performance_schema):
-- 找到 MDL 等待关系(MySQL 8+ 常用视图/表)
SELECT * FROM performance_schema.metadata_locks;
2.5 命名锁:GET_LOCK()(User-level Lock)
命名锁是“应用层互斥”能力:你用一个字符串作为锁名,在 MySQL 内部实现跨连接的互斥,不直接绑定某张表或某行数据。
- 使用场景:
- 定时任务防重:同一时刻只允许一台机器执行某个 job(
GET_LOCK('job:xxx', 0))。 - 灰度/迁移开关:对某个流程做全局互斥,避免并发执行导致重复搬运、重复扣费等。
- 需要“强一致单点互斥”但不想引入额外分布式锁组件的场景(注意可用性与单点问题)。
- 常见用法:
SELECT GET_LOCK('job:rebuild_index', 10); -- 10 秒拿不到就失败
-- 临界区
SELECT RELEASE_LOCK('job:rebuild_index');
- 注意点:
- 命名锁是连接级资源:连接断开通常会自动释放;连接泄漏会带来“锁一直不释放”的风险。
- 不要用它替代 InnoDB 行锁来做数据一致性,命名锁更适合“流程互斥”。
3. InnoDB 事务锁:S/X、意向锁、行锁与间隙锁
3.1 锁模式:S、X、IS、IX 分别解决什么问题
- 共享锁(S Lock):允许并发读,但与写互斥。常用于“锁定读”以保证后续逻辑的前提不被并发写破坏。
- 排他锁(X Lock):独占写,其他事务不能再读写该资源的锁定读版本。
- 意向共享锁(IS)、意向排他锁(IX):表级“声明”,表示事务接下来要在表内某些记录上加 S / X 锁,便于与表锁快速做冲突判断。
3.2 锁定读语法:FOR UPDATE、FOR SHARE、NOWAIT、SKIP LOCKED
锁的“类型”决定了并发语义,但落地时你真正会写的是 SQL。InnoDB 里最常见的加锁入口就是锁定读:
SELECT ... FOR UPDATE:- 作用:对读到的记录加 X 类锁(并结合间隙/临键锁语义),用于“先查后改”的悲观并发控制。
- 场景:库存扣减、余额扣款、订单状态机推进等需要强互斥的更新链路。
SELECT ... FOR SHARE(MySQL 8.0+,旧语法LOCK IN SHARE MODE):- 作用:对读到的记录加 S 类锁,允许并发读但阻止并发写,用于“先查后用”的一致性前提保护。
- 场景:读取某个配置/额度后,后续逻辑依赖它不被修改(但不需要你立刻更新它)。
NOWAIT/SKIP LOCKED(常与FOR UPDATE搭配):NOWAIT:拿不到锁立刻失败,适合“宁可失败也不排队”的低延迟场景。SKIP LOCKED:跳过被锁住的行,适合“多消费者抢任务”的队列式处理(避免全体阻塞)。
3.3 意向锁(IS / IX):多粒度锁定的“快速冲突检测”
意向锁是 InnoDB 自动维护的表级锁,用来表达“我准备在这张表的某些记录上加 S/X 行锁”。它本身不会把整张表变成不可并发访问,但能让“表级锁/MDL”与“行级锁”之间快速判断是否冲突。
- 使用场景(理解层面为主):
- 当有人试图对整表加表锁时(例如
LOCK TABLES),可以通过是否存在 IS/IX 快速判定是否需要等待,而不是遍历整张表检查每一行有没有锁。 - 排查“为什么我的表锁/DDL 一直等”:很多时候不是表锁本身慢,而是表上存在大量未提交事务的行锁意图。
3.4 记录锁(Record Lock):精确锁住索引记录
记录锁锁定的是“某条索引记录”,不是“某张表”。这意味着:是否走索引,直接决定了锁的精确度与影响范围。
- 使用场景:
- 点查更新:按主键/唯一键更新一行,避免并发写覆盖。
- 先查后改:先用
SELECT ... FOR UPDATE锁住目标行,再做业务判断与更新,避免并发下的“超卖/重复扣减”。 - 示例:
START TRANSACTION;
SELECT stock FROM sku WHERE id = 100 FOR UPDATE; -- 锁住 id=100 的索引记录
UPDATE sku SET stock = stock - 1 WHERE id = 100;
COMMIT;
3.5 间隙锁(Gap Lock):锁住“区间”,阻止插入
间隙锁锁的是“索引区间的空隙”,用于防止幻读。它通常出现在 可重复读(Repeatable Read,RR) 隔离级别下的范围锁定读/更新中。
- 使用场景:
- 需要保证某个范围内“结果集合不被新增行改变”:例如做“范围内唯一性”或“按范围取号”的逻辑。
- 防止并发插入导致的幻读:事务 A 在范围条件下做
SELECT ... FOR UPDATE,事务 B 不能在该范围内插入新行。 - 示例:
-- 假设 idx_age 是 age 的普通二级索引
START TRANSACTION;
SELECT * FROM user WHERE age BETWEEN 20 AND 30 FOR UPDATE; -- 可能加 Next-Key / Gap 锁
-- 其他事务在 (20,30) 范围内插入新 age 记录可能被阻塞
COMMIT;
3.6 临键锁(Next-Key Lock):记录锁 + 间隙锁(典型为 (a, b])
临键锁是 InnoDB 在 RR 下常用的范围锁实现:既锁住已有记录,又锁住记录前的间隙,从而同时防止“更新冲突”和“范围内插入”。
- 使用场景:
- 范围更新/删除:例如
UPDATE t SET ... WHERE k > 10 AND k < 20,为了防止范围内插入造成幻读,需要锁住区间。 - “读到的范围”在事务内必须稳定:例如分页处理、批处理需要确保本批次集合不被插入打乱。
- 注意点:
- 如果查询条件 无法利用索引,InnoDB 可能需要扫描大量记录,并对扫描到的记录/间隙加锁,表现上会“像锁表一样”影响并发,但本质仍是行锁集合。
3.7 插入意向锁(Insert Intention Lock):提升并发插入
插入意向锁不是你显式加的锁,而是 InnoDB 为了让多个事务能在同一索引间隙中并发插入而引入的机制:它表达“我要往这个间隙插入”,从而避免插入之间的过度互斥。
- 使用场景:
- 高并发在同一个索引范围内插入(例如时间序列数据落在相近的索引值附近)。
- 与间隙锁/临键锁交互:当某事务持有范围锁时,其他事务对该范围内插入会被阻塞;但在没有范围锁的情况下,多插入事务之间尽量不互相阻塞。
3.8 自增锁(AUTO-INC Lock):控制 AUTO_INCREMENT 分配
带 AUTO_INCREMENT 的表在插入时需要分配单调递增值。InnoDB 使用自增锁(以及不同锁模式)来在并发下保证分配规则。
- 使用场景:
- 单行插入:通常影响较小。
- 批量插入:例如
INSERT INTO t SELECT ...或多值插入,在某些配置下会持有更久的自增相关锁,影响并发插入吞吐。 - 关键参数:
innodb_autoinc_lock_mode:决定自增分配的并发策略(不同模式在“并发性”和“连续性/可预测性”之间权衡)。- 面试落地说法:
- 如果系统对“自增值连续”没有强需求,通常更关注并发与吞吐;如果依赖自增连续做分片/业务逻辑,要明确知道并发下可能出现跳号或不可预期分配的边界。
3.9 外键约束相关的锁
InnoDB 在维护外键一致性时,会对父表/子表相关记录加锁来防止并发破坏约束。
- 使用场景:
- 插入子表行:需要验证父表对应记录存在,可能对父表记录加共享类锁,防止父记录在校验窗口被删除。
- 删除/更新父表主键:需要检查是否存在子表引用,可能导致对相关子表记录加锁,进而放大锁冲突。
- 实战建议:
- 高并发写入场景下谨慎使用外键;如果使用外键,要确保索引齐全(父表被引用列、子表外键列必须有索引),否则锁范围与扫描成本会显著放大。
4. “每个锁”的典型 SQL 触发与使用场景速记
4.1 锁与语句对照表
| 锁 | 常见触发方式 | 主要解决的问题 | 典型场景 |
|---|---|---|---|
| 全局读锁(FTWRL) | FLUSH TABLES WITH READ LOCK |
全库一致性 | 混合引擎的逻辑备份 |
| 备份锁 | LOCK INSTANCE FOR BACKUP |
备份期间元数据稳定 | 物理备份、克隆/恢复链路 |
| 表锁 | LOCK TABLES t READ/WRITE |
强制整表互斥 | MyISAM 写入、手工修复 |
| MDL | 任意访问对象的 SQL 自动加 | 结构一致性 | DDL 排队与阻塞排查 |
| 命名锁 | GET_LOCK('name', timeout) |
跨连接互斥 | 定时任务防重、流程互斥 |
| S / X 行锁 | FOR SHARE / FOR UPDATE |
读写互斥 | 超卖防护、先查后改 |
| 记录锁 | 点查命中索引记录 | 精确互斥 | 按主键更新一行 |
| 间隙锁 | RR 下范围锁定读/更新 | 禁止区间插入 | 防幻读、范围唯一 |
| 临键锁 | RR 下范围条件 + 记录存在 | 锁区间 (a, b] |
范围更新/删除 |
| 插入意向锁 | 并发 INSERT |
减少插入互斥 | 高并发插入 |
| 自增锁 | AUTO_INCREMENT 插入 |
自增分配一致性 | 批量插入并发控制 |
4.2 常见误区纠正
- “没走索引就退化成表锁”:更准确的说法是 会扫描并锁住更多记录/间隙,最终表现得“像锁表”,但锁类型仍主要是行锁集合。
- “普通
SELECT会加行锁”:InnoDB 的普通SELECT通常是 MVCC 一致性读,不加行锁;但它仍会持有 MDL,且在SERIALIZABLE隔离级别或显式锁定读时会参与锁竞争。 - “只要加
FOR UPDATE就一定只锁一行”:锁住的是“命中的索引范围”,条件不同、索引不同,锁住的范围可能从一行扩大到一个区间,甚至大量记录。
5. 影响锁范围的关键因素:什么时候会出现 Gap / Next-Key
5.1 隔离级别:RC 与 RR 的核心差异
- 读已提交:Read Committed(RC)
- 重点:通常不需要用间隙锁来防幻读,因此很多场景下锁更“窄”,并发更好。
- 代价:可重复读语义更弱,你需要用业务侧约束或更强的锁定读来保证某些一致性前提。
- 可重复读:Repeatable Read(RR,InnoDB 默认)
- 重点:为了解决幻读,范围锁定读/更新更容易引入 Gap / Next-Key,从而阻止区间插入。
- 代价:范围条件的并发冲突更常见,特别是热点范围写入。
5.2 条件与索引:决定“锁住一行”还是“锁住一段区间”
- 唯一索引等值条件(例如
WHERE pk = ?):更倾向于记录锁,影响范围小,最适合高并发点写。 - 非唯一索引等值或范围条件(例如
WHERE k = ?、WHERE k BETWEEN ? AND ?):更可能引入 Next-Key / Gap 语义来覆盖区间,从而阻止并发插入导致的幻读。 - 条件无法走索引:会扫描更多记录并逐步加锁,容易把冲突范围扩大到“接近全表”。
6. 排查与定位:谁在等锁、等的是什么锁
6.1 行锁等待:performance_schema.data_locks 与 data_lock_waits
如果业务出现“卡住但 CPU 不高”,优先确认是否在等锁(尤其是 FOR UPDATE、大范围 UPDATE/DELETE)。
SELECT * FROM performance_schema.data_lock_waits;
SELECT * FROM performance_schema.data_locks;
6.2 MDL 等待:performance_schema.metadata_locks
如果是 DDL 卡住(ALTER TABLE、CREATE INDEX),优先看 MDL,而不是只盯 InnoDB 行锁。
SELECT * FROM performance_schema.metadata_locks;
6.3 死锁现场:优先看 SHOW ENGINE INNODB STATUS
如果 MySQL 已经报错 Deadlock found when trying to get lock; try restarting transaction,最直接的第一现场通常是:
SHOW ENGINE INNODB STATUS;
- 重点关注
LATEST DETECTED DEADLOCK段落。 - 这里通常会给出:
- 事务 1、事务 2 的事务 id、线程 id、持锁情况、等待的锁。
- 每个事务当时正在执行的 最后一条 SQL。
- 哪个事务被 InnoDB 选为回滚牺牲者。
- 面试和线上排查都要注意一个边界:
- 它只保留“最近一次”死锁,不是完整历史。
- 如果死锁发生很频繁,后一次会覆盖前一次。
可以把它当成一句结论记住:死锁先看 SHOW ENGINE INNODB STATUS,锁等待再看 performance_schema。
6.4 MySQL 是如何判定死锁的
先给结论:InnoDB 判定死锁的核心方法,是在锁等待发生时构造“事务等待图”(wait-for graph),如果发现图中出现环,就认定发生了死锁。
6.4.1 什么是等待图
可以把每个事务看成一个节点:
- 事务 A 等事务 B 释放锁,就记一条边
A -> B - 事务 B 又在等事务 C,就有
B -> C - 如果最后出现
C -> A,形成闭环,就说明这几个事务互相等着对方,谁都无法继续推进
这就是死锁。
例如:
- 事务 A 持有记录
id = 1的锁,等待id = 2 - 事务 B 持有记录
id = 2的锁,等待id = 1
对应到等待图里就是:
A -> BB -> A
图里出现环,所以 InnoDB 会判定为死锁,而不是继续无限等待。
6.4.2 什么时候触发死锁检测
不是所有事务都会被后台线程“定时全库扫描”一遍。更准确的理解是:
- 当某个事务申请锁,但发现这把锁当前拿不到,需要进入等待时
- InnoDB 会沿着“谁在等谁”的链路继续往后检查
- 如果检查过程中发现又回到了当前事务自己,就说明形成了环
也就是说,死锁检测通常是在锁等待发生时按需触发的,而不是纯粹靠低频定时任务。
这也是为什么高冲突写场景里,死锁检测本身也可能带来额外 CPU 开销:因为大量事务都在频繁进入“等待并检测”的过程。
6.4.3 发现死锁后会怎么处理
InnoDB 不会让所有事务一直卡死,而是会主动选一个事务作为 victim(牺牲者) 回滚,打破这个环。
常见理解方式是:
- 谁回滚代价更小,谁更可能被选中
- 这里的“代价”通常和事务已经修改了多少行、持有多少锁、回滚成本多大有关
所以线上经常会看到:
- 两个事务都没错
- 但其中一个突然收到死锁错误并被回滚
这不是随机崩掉,而是 InnoDB 在做“最小代价解环”。
6.5 如何定位“相互冲突、导致死锁”的语句
死锁本质上不是“单条 SQL 慢”,而是 两个或多个事务形成了循环等待。定位时要回答 4 个问题:
- 谁在等待。
- 等的是哪把锁。
- 谁持有这把锁。
- 等待方和持有方各自在执行什么 SQL。
推荐按下面顺序看。
6.5.1 从死锁日志里先拿到两个事务的最后 SQL
SHOW ENGINE INNODB STATUS 中通常会直接出现类似下面的信息:
TRANSACTION ...WAITING FOR THIS LOCK TO BE GRANTEDHOLDS THE LOCK(S)query id ... updating
其中最关键的是两部分:
- 等待方正在执行的 SQL:这往往就是“被卡住的语句”。
- 持锁方最近执行的 SQL:这往往就是“先拿到锁、又去等别人锁的语句”。
如果两边分别是:
- 事务 A:
UPDATE ... WHERE id = 1 - 事务 B:
UPDATE ... WHERE id = 2
同时日志又显示:
- A 持有
id = 1对应记录锁,等待id = 2 - B 持有
id = 2对应记录锁,等待id = 1
那这两条语句就是直接冲突链路,根因通常是 访问顺序不一致。
6.5.2 再用 performance_schema 把“锁”映射回“会话和 SQL”
如果死锁刚发生完,或者你看到的是“长时间锁等待,怀疑马上会死锁”,可以把等待关系、锁对象、当前 SQL 串起来看。
SELECT
w.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx_id,
w.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx_id,
rl.OBJECT_SCHEMA AS waiting_schema,
rl.OBJECT_NAME AS waiting_table,
rl.INDEX_NAME AS waiting_index,
rl.LOCK_TYPE AS waiting_lock_type,
rl.LOCK_MODE AS waiting_lock_mode,
rl.LOCK_DATA AS waiting_lock_data,
bl.LOCK_TYPE AS blocking_lock_type,
bl.LOCK_MODE AS blocking_lock_mode,
bl.LOCK_DATA AS blocking_lock_data
FROM performance_schema.data_lock_waits w
JOIN performance_schema.data_locks rl
ON w.REQUESTING_ENGINE_LOCK_ID = rl.ENGINE_LOCK_ID
JOIN performance_schema.data_locks bl
ON w.BLOCKING_ENGINE_LOCK_ID = bl.ENGINE_LOCK_ID;
这条 SQL 解决的是“哪两个事务因为哪条记录/哪段索引范围冲突”。
然后继续把事务 id 关联到线程和 SQL:
SELECT
t.THREAD_ID,
t.PROCESSLIST_ID,
t.PROCESSLIST_USER,
t.PROCESSLIST_HOST,
t.PROCESSLIST_DB,
es.SQL_TEXT,
es.TIMER_WAIT
FROM performance_schema.threads t
JOIN performance_schema.events_statements_current es
ON t.THREAD_ID = es.THREAD_ID
WHERE t.PROCESSLIST_ID IS NOT NULL;
PROCESSLIST_ID可以对应SHOW PROCESSLIST里的会话 id。SQL_TEXT可以看到线程当前正在执行的 SQL。- 如果当前语句已经切走,可以再看
events_statements_history或events_statements_history_long。
把这两组结果结合起来,才能真正回答:
- 等待方是哪条 SQL。
- 阻塞方是哪条 SQL。
- 它们争用的是哪张表、哪个索引、哪条记录或哪个间隙。
6.6 定位顺序
线上排查可以直接按这个顺序走:
- 先确认是不是死锁报错,还是普通锁等待超时。
- 如果已经发生死锁,立刻执行
SHOW ENGINE INNODB STATUS;,保存LATEST DETECTED DEADLOCK。 - 提取两个事务各自的 SQL、等待锁、已持有锁、受影响表和索引。
- 再查
performance_schema.data_lock_waits、data_locks,确认冲突点到底是主键记录、二级索引记录,还是 Gap / Next-Key。 - 结合
performance_schema.threads、events_statements_current/history,把事务 id 映射回应用连接和具体 SQL。 - 最后回到业务代码核对事务边界、加锁顺序、索引命中情况。