MySQL锁机制全解析:共享锁、排他锁、意向锁、行锁、表锁
引言
线上数据库最常见的几类故障,几乎都和锁有关:
- 接口大面积超时,但 SQL 并不慢,
SHOW PROCESSLIST里一排Waiting for table metadata lock - 批量更新接口偶发死锁,日志里抛出
Deadlock found when trying to get lock - 一张小表执行
ALTER TABLE卡了半小时,所有读写全部阻塞
这些问题的根源,是开发阶段对 MySQL 锁机制缺乏系统认知——分不清什么时候加行锁、什么时候退化成表锁、共享锁和排他锁怎么互斥、意向锁到底解决什么问题。
本文把 MySQL(InnoDB)锁体系按 锁粒度 → 锁模式 → 兼容性 → 实际触发场景 → 死锁分析 的顺序拆开讲透,配大量可复现示例,适合当锁问题的排查手册。
一、为什么需要锁:并发带来的三类问题
没有锁控制,多个事务并发操作同一份数据会出现:
问题 | 现象 | 例子 |
|---|---|---|
脏读 | 读到别的事务未提交的数据 | 事务 A 改了余额还没提交,事务 B 读到了 |
不可重复读 | 同一事务内两次读同一行,结果不同 | 事务 A 读余额为 100,事务 B 扣了 50 提交,事务 A 再读变成 50 |
幻读 | 同一事务内两次范围查询,行数变了 | 事务 A 查 |
MySQL 通过 事务隔离级别 + 锁机制 组合解决这些问题。InnoDB 默认隔离级别是 REPEATABLE READ,在这个级别下,靠行锁 + 间隙锁解决幻读。
二、MySQL 锁的整体分类框架
先建立全局视角,后面逐个展开:
MySQL 锁体系(InnoDB) ├── 按粒度分 │ ├── 表级锁 │ │ ├── 表锁(手动 LOCK TABLES) │ │ ├── 元数据锁 MDL(自动,DDL 相关) │ │ └── 意向锁 IS / IX(自动,协调表锁和行锁) │ └── 行级锁 │ ├── 记录锁 Record Lock(锁具体行) │ ├── 间隙锁 Gap Lock(锁范围间隙) │ └── 临键锁 Next-Key Lock(记录锁 + 间隙锁,默认) ├── 按模式分 │ ├── 共享锁 S(读锁) │ └── 排他锁 X(写锁) └── 特殊锁 ├── 自增锁 AUTO-INC └── 谓词锁(空间索引,极少用)
三、表级锁:表锁与元数据锁(MDL)
3.1 表锁(手动控制)
MyISAM 时代的主力锁,InnoDB 下不推荐使用。
-- 加读锁(共享) LOCK TABLES users READ; -- 加写锁(排他) LOCK TABLES users WRITE; -- 解锁(释放当前会话所有表锁) UNLOCK TABLES;
特点:
- 加读锁:本会话和其他会话都能读,本会话不能写,其他会话写会阻塞
- 加写锁:本会话可读写,其他会话读写全部阻塞
- 粒度粗,并发低,InnoDB 一般只在
ALTER TABLE等 DDL 时隐式使用
3.2 元数据锁 MDL(Metadata Lock)
自动加的,不需要你写任何命令。
-- 事务 A(未提交) BEGIN; SELECT * FROM users WHERE id = 1; -- 加 MDL 读锁 -- 事务 B(同时执行) ALTER TABLE users ADD COLUMN age INT; -- 需要 MDL 写锁,被阻塞! -- 事务 C(同时执行) SELECT * FROM users WHERE id = 2; -- 需要 MDL 读锁,排在 B 后面,也被阻塞!
这就是线上最常见的"加个字段,整个表卡死"的原因。
MDL 规则:
- 读操作加 MDL 读锁,写操作加 MDL 写锁
- 读锁之间不互斥,读写锁、写写锁互斥
- 事务提交才释放 MDL 锁
生产教训:长事务 + DDL = 全表阻塞。执行 DDL 前先查 SHOW PROCESSLIST 和 information_schema.innodb_trx 确认没有长事务。
四、行级锁:记录锁、间隙锁、临键锁
InnoDB 的核心锁机制,只锁需要的行(或间隙),并发度高。
4.1 记录锁(Record Lock)
锁索引上的具体记录。
-- 对 id=5 这一行加记录锁 SELECT * FROM users WHERE id = 5 FOR UPDATE;
其他事务不能修改 id=5 这一行,但可以操作其他行。
4.2 间隙锁(Gap Lock)
锁住两条记录之间的"间隙",防止其他事务往里面插入数据。
-- 假设表中 id 有 1, 5, 10 -- 下面语句锁住 (5, 10) 这个间隙 SELECT * FROM users WHERE id BETWEEN 6 AND 9 FOR UPDATE;
效果:其他事务执行 INSERT INTO users (id) VALUES (7) 会被阻塞。
目的:防止幻读。 事务 A 范围查询后,事务 B 不能往这个范围里插新行。
4.3 临键锁(Next-Key Lock)
InnoDB 默认行锁算法 = 记录锁 + 间隙锁。
锁住:当前记录 + 前面到上一条记录之间的间隙。
索引值: 1 5 10 20 临键锁范围: id=5 的锁覆盖 (1, 5] -- 包含 5 本身 id=10 的锁覆盖 (5, 10]
-- 默认 RR 隔离级别下 SELECT * FROM users WHERE id > 5 AND id < 15 FOR UPDATE;
锁住 (5, 10] 和 (10, 15) 范围,其他事务不能插入 id=7、12,也不能修改 id=10。
RC 隔离级别(READ COMMITTED)下没有间隙锁,只锁命中的行。所以 RC 下可能出现幻读,但并发度更高。很多互联网公司用 RC + 业务层兜底来换取性能。
五、共享锁(S)与排他锁(X):读写互斥的核心规则
5.1 基本概念
锁类型 | 别名 | 用途 | 加锁方式 |
|---|---|---|---|
共享锁 S | 读锁 | 事务读数据,允许其他事务也读 |
|
排他锁 X | 写锁 | 事务修改数据,其他事务不能读也不能写 |
|
5.2 互斥规则
S锁 X锁 S锁 兼容 互斥 X锁 互斥 互斥
简单记:只要有一方是 X 锁,就互斥。
5.3 实战示例
-- 事务 A BEGIN; SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; -- 加 S 锁 -- 事务 B(同一时刻) SELECT * FROM users WHERE id = 1; -- ✅ 普通读走快照,不阻塞 SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; -- ✅ 加 S 锁,兼容 SELECT * FROM users WHERE id = 1 FOR UPDATE; -- ❌ 要加 X 锁,被阻塞 UPDATE users SET name='x' WHERE id = 1; -- ❌ 要加 X 锁,被阻塞
-- 事务 A BEGIN; SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 加 X 锁 -- 事务 B SELECT * FROM users WHERE id = 1; -- ✅ 普通读走 MVCC 快照,不阻塞 SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; -- ❌ 要加 S 锁,被阻塞 UPDATE users SET name='x' WHERE id = 1; -- ❌ 要加 X 锁,被阻塞
注意:普通 SELECT(不加锁)在 RR 和 RC 隔离级别下走 MVCC 快照读,不加任何锁,所以不会被阻塞。这是 InnoDB 高并发的关键设计。
六、意向锁(IS / IX):为什么需要它、怎么工作
6.1 问题:表锁和行锁怎么共存?
假设没有意向锁:
- 事务 A 对
users表的第 5 行加了行级 X 锁 - 事务 B 要对
users表加表级写锁
事务 B 怎么知道能不能加?它必须逐行扫描整个表检查有没有行锁——代价不可接受。
6.2 意向锁的作用
意向锁是表级锁,由 InnoDB 自动加,不需要手动干预。
- 事务 A 想对某行加 S 锁 → InnoDB 自动先对表加 意向共享锁 IS
- 事务 A 想对某行加 X 锁 → InnoDB 自动先对表加 意向排他锁 IX
这样事务 B 来加表锁时,只需要检查表上有没有 IX / IS 就行,不用逐行扫描。
6.3 实际效果
-- 事务 A BEGIN; UPDATE users SET name='x' WHERE id = 1; -- 自动加 IX 锁 + 行级 X 锁 -- 事务 B LOCK TABLES users WRITE; -- 需要表级 X 锁,和 IX 互斥,被阻塞
意向锁不阻塞行级操作,只和表级锁互斥。
七、锁的兼容性矩阵(一张表记牢)
已有 \ 请求 | IS | IX | S(表锁) | X(表锁) |
|---|---|---|---|---|
IS | ✅ 兼容 | ✅ 兼容 | ✅ 兼容 | ❌ 互斥 |
IX | ✅ 兼容 | ✅ 兼容 | ❌ 互斥 | ❌ 互斥 |
S(表锁) | ✅ 兼容 | ❌ 互斥 | ✅ 兼容 | ❌ 互斥 |
X(表锁) | ❌ 互斥 | ❌ 互斥 | ❌ 互斥 | ❌ 互斥 |
行级锁之间用临键锁规则判断,不走这张表。
八、锁的触发条件:不同 SQL 到底加了什么锁
这是生产排查最关键的部分。
8.1 前提条件
行锁只在走索引时生效。 没走索引 = 锁全表(退化成表级锁)。
8.2 各 SQL 加锁汇总(RR 隔离级别)
SQL 类型 | 索引情况 | 加锁行为 |
|---|---|---|
| 任意 | 不加锁(MVCC 快照读) |
| 走唯一索引命中单行 | 行级 S 锁(记录锁) |
同上 | 走普通索引/范围 | 临键锁(Next-Key Lock) |
| 走唯一索引命中单行 | 行级 X 锁(记录锁) |
同上 | 走普通索引/范围 | 临键锁 |
| 走唯一索引命中单行 | 行级 X 锁 |
同上 | 走普通索引/范围 | 临键锁 |
| 插入位置 | 行级 X 锁 + 插入意向间隙锁 |
8.3 退化成表锁的典型场景
-- name 没有索引 UPDATE users SET age = 20 WHERE name = '张三';
InnoDB 只能全表扫描逐行检查,每行都加 X 锁,效果等价于锁全表。其他事务对所有行的修改都被阻塞。
排查口诀:行锁没走索引 = 表锁。
九、死锁:产生条件、复现与排查
9.1 死锁的四个必要条件
- 互斥:资源不能共享
- 持有并等待:持有锁 A,等待锁 B
- 不可剥夺:锁只能主动释放
- 循环等待:事务 A 等 B,事务 B 等 A
9.2 经典死锁复现
-- 表结构 CREATE TABLE accounts ( id INT PRIMARY KEY, balance DECIMAL(10,2) ); -- 数据: id=1 balance=100, id=2 balance=200 -- 事务 A BEGIN; UPDATE accounts SET balance = balance - 10 WHERE id = 1; -- 锁 id=1 -- 事务 B(同时) BEGIN; UPDATE accounts SET balance = balance + 10 WHERE id = 2; -- 锁 id=2 -- 事务 A(继续) UPDATE accounts SET balance = balance + 10 WHERE id = 2; -- 等事务 B 释放 id=2 -- 事务 B(继续) UPDATE accounts SET balance = balance - 10 WHERE id = 1; -- 等事务 A 释放 id=1 -- 💥 死锁!MySQL 检测到后回滚其中一个事务
9.3 死锁排查
-- 查看最近一次死锁信息 SHOW ENGINE INNODB STATUS\G
输出中找 LATEST DETECTED DEADLOCK 段:
*** (1) TRANSACTION: UPDATE accounts SET balance = balance + 10 WHERE id = 2 *** (1) HOLDS THE LOCK(S): RECORD LOCKS space id 123 page no 3 ... *** (2) TRANSACTION: UPDATE accounts SET balance = balance - 10 WHERE id = 1 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 123 page no 3 ...
9.4 死锁预防策略
- 固定访问顺序:所有事务按相同顺序访问表和行(如永远先锁 id 小的)
- 缩短事务:事务里不做 RPC 调用、文件 IO
- 降低隔离级别:RC 级别无间隙锁,死锁概率大幅下降
- 加索引:避免全表扫描锁
- 重试机制:应用层捕获死锁异常,自动重试
十、锁问题的排查工具与命令
10.1 查看当前锁等待
-- MySQL 8.0+ SELECT * FROM performance_schema.data_lock_waits; SELECT * FROM performance_schema.data_locks; -- 老版本 SELECT * FROM information_schema.innodb_locks; SELECT * FROM information_schema.innodb_lock_waits;
10.2 查看阻塞的线程
SHOW FULL PROCESSLIST; -- 关注 State 列: -- Waiting for table metadata lock → MDL 等待 -- Waiting for lock → 行锁等待
10.3 查看长事务
SELECT * FROM information_schema.innodb_trx ORDER BY trx_started ASC;
10.4 查看 MDL 锁
SELECT * FROM performance_schema.metadata_locks;
10.5 杀掉阻塞会话
KILL <thread_id>;
十一、生产环境锁优化实践
11.1 索引是锁优化的第一要务
-- ❌ 无索引,锁全表 UPDATE orders SET status = 'cancelled' WHERE user_phone = '13800138000'; -- ✅ 加索引,只锁命中的行 ALTER TABLE orders ADD INDEX idx_phone (user_phone);
11.2 控制事务粒度
// ❌ 事务里调外部接口
@Transactional
public void placeOrder() {
updateInventory(); // 持有行锁
callPaymentAPI(); // 网络调用耗时 3 秒,锁一直不释放
updateOrderStatus();
}
// ✅ 拆分
public void placeOrder() {
updateInventory(); // 短事务
callPaymentAPI(); // 无锁
updateOrderStatus(); // 短事务
}11.3 避免热点行竞争
-- ❌ 高并发下秒杀同一行 UPDATE inventory SET stock = stock - 1 WHERE item_id = 1; -- ✅ 拆分成多行,分散竞争 -- 提前创建 item_id=1 的 10 行库存记录,随机选一行扣减 UPDATE inventory SET stock = stock - 1 WHERE item_id = 1 AND stock > 0 LIMIT 1;
11.4 合理选择隔离级别
-- 查看当前隔离级别 SELECT @@transaction_isolation; -- 设置为 READ COMMITTED(无间隙锁,并发高) SET session transaction isolation level READ COMMITTED;
互联网业务常用 RC:避免间隙锁带来的锁等待,幻读问题由业务层兜底(如唯一索引约束)。
十二、速查总结表
概念 | 一句话 | 关键记忆点 |
|---|---|---|
表锁 | 锁整张表 | 粒度粗,并发低 |
行锁 | 锁索引上的行 | 必须走索引,否则退化表锁 |
S 锁 | 读锁,共享 | 和 S 兼容,和 X 互斥 |
X 锁 | 写锁,排他 | 和谁都互斥 |
IS / IX | 表级意向锁 | 自动加,协调表锁和行锁 |
记录锁 | 锁具体行 | 唯一索引等值命中 |
间隙锁 | 锁范围不锁行 | 防插入,防幻读 |
临键锁 | 记录 + 间隙 | InnoDB 默认算法 |
MDL | 元数据锁 | DDL 自动加,长事务是天敌 |
死锁 | 循环等待 | 固定访问顺序 + 短事务 |
总结
MySQL 锁机制的设计目标很明确:在保证数据一致性的前提下,尽可能提高并发度。
核心逻辑链:
- 表锁粗、行锁细 → 行锁必须走索引
- S 和 X 互斥 → 读写分离靠 MVCC 快照读绕过
- 意向锁 → 让表锁不用逐行检查就能判断能不能加
- 临键锁 = 记录锁 + 间隙锁 → RR 级别防幻读,代价是锁范围更大
- 死锁 → 固定访问顺序 + 短事务 + 重试
线上锁问题的排查路径:
接口超时 → SHOW PROCESSLIST 看 State → 是 Waiting for table metadata lock?→ 查长事务 / DDL → 是 Waiting for lock?→ 查 data_lock_waits 定位阻塞源 → 死锁?→ SHOW ENGINE INNODB STATUS 看详情 → 根因:缺索引 / 事务太长 / 访问顺序不固定
理解锁机制不是为了背概念,而是为了在写每一行 SQL 时,脑子里能预判它会加什么锁、和谁互斥、会不会阻塞别人。这种"锁意识"是区分初级和资深后端开发的关键分水岭。
以上就是MySQL锁机制全解析:共享锁、排他锁、意向锁、行锁、表锁的详细内容,更多关于MySQL锁机制全解析的资料请关注脚本之家其它相关文章!
相关文章
MySQL 中的行锁(Record Lock) 和 间隙锁(Gap Lock)详解
InnoDB中的行锁(RecordLock)和间隙锁(GapLock)是用于保证事务隔离性的两种锁机制,本文给大家介绍MySQL中的行锁(Record Lock) 和间隙锁(Gap Lock),感兴趣的朋友跟随小编一起看看吧2025-11-11
SELECT INTO 和 INSERT INTO SELECT 两种表复制语句详解(SQL数据库和Oracle数据库的
我们经常会遇到需要表复制的情况,如将一个table1的数据的部分字段复制到table2中,或者将整个table1复制到table2中,这时候我们就要使用SELECT INTO 和 INSERT INTO SELECT 表复制语句了2019-03-03


最新评论