MySQL 锁机制
一、锁机制的总体分类
MySQL 中的锁用于管理对共享资源的并发访问。理解其分类有助于选择合适的策略。
1. 从加锁粒度分类
锁定的数据范围大小。
- 表级锁 (Table-level Lock):
锁定整张数据表。
优点:实现简单,开销小,加锁快,不会出现死锁。
缺点:锁定粒度最大,并发冲突概率最高,并发性能最低。
适用引擎:MyISAM (主要使用), InnoDB (特定情况下也会使用, 如LOCK TABLES
或某些 DDL)。
示例(显式加表锁):
-- 加写锁,其他会话无法读写
LOCK TABLES my_table WRITE;
-- 加读锁,其他会话可读但不可写,当前会话也不可写
LOCK TABLES my_table READ;
-- 释放当前会话持有的所有表锁
UNLOCK TABLES;
-
行级锁 (Row-level Lock):
仅锁定操作涉及的行记录(更准确地说是索引记录)。
优点:锁定粒度最小,并发冲突概率最低,并发性能最好。
缺点:实现复杂,开销较大,加锁较慢,可能会出现死锁。
适用引擎:InnoDB (主要使用)。 -
页级锁 (Page-level Lock):
锁定数据页(InnoDB 默认页大小 16KB)。粒度和开销介于表锁和行锁之间。
适用引擎:BDB (BerkeleyDB) 引擎(现在较少使用)。
2. 从加锁模式分类
锁的状态或类型,决定了兼容性。
-
共享锁 (Shared Lock / S锁 / 读锁):
事务持有 S 锁可以读取数据,但不能修改。
多个事务可以同时持有同一资源的 S 锁(读读不阻塞)。
其他事务不能获取该资源的 X 锁(读阻塞写)。
获取方式(显式):SELECT ... LOCK IN SHARE MODE;
-
排他锁 (Exclusive Lock / X锁 / 写锁):
事务持有 X 锁可以读取和修改数据。
同一时间只能有一个事务持有该资源的 X 锁。
其他事务不能获取该资源的 S 锁或 X 锁(写阻塞读写)。
获取方式(显式):SELECT ... FOR UPDATE;
(隐式):INSERT
,UPDATE
,DELETE
操作会自动加 X 锁。
3. 从加锁方式分类
是用户主动请求还是系统自动添加。
-
显式加锁:
用户通过特定 SQL 语句明确请求加锁。
示例:LOCK TABLES
,UNLOCK TABLES
,SELECT ... LOCK IN SHARE MODE
,SELECT ... FOR UPDATE
。 -
隐式加锁:
由存储引擎根据事务隔离级别和执行的 SQL 语句自动添加,用户无需干预。这是 InnoDB 最常见的加锁方式。
示例: 执行UPDATE products SET stock = stock - 1 WHERE id = 10;
时,InnoDB 会自动在id=10
的行(索引)上加 X 锁。
二、InnoDB 锁机制详解(核心)
InnoDB 是支持事务和行级锁的主流存储引擎。
1. 行级锁
InnoDB 行锁是基于索引实现的。如果查询条件未使用索引,可能导致全表扫描,锁定所有行,性能下降。
- Record Lock (记录锁):
锁定单个索引记录。这是最基本的行锁。
示例:
-- 假设 id 是主键或唯一索引
-- 事务 T1 执行:
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- InnoDB 会在 id=5 的索引记录上加一个 X 记录锁。
-- 其他事务无法修改或删除 id=5 的行,也无法获取 id=5 的 S 锁。
- Gap Lock (间隙锁):
锁定索引记录之间的“间隙”,防止其他事务在这个间隙中插入新记录。它不锁定记录本身。主要用于REPEATABLE READ
隔离级别防止幻读。
示例 (概念):
-- 假设 age 索引上有值 20, 30
-- 事务 T1 执行 (RR 级别):
SELECT * FROM users WHERE age = 25 FOR UPDATE;
-- InnoDB 可能会锁定 (20, 30) 这个间隙,防止其他事务插入 age=25 或其他在 (20,30) 范围内的记录。
-- 注意:Gap Lock 本身不互斥,不同事务可以持有相同间隙的 Gap Lock。
- Next-Key Lock (临键锁):
Record Lock + Gap Lock 的组合。锁定一个索引记录以及该记录之前的间隙。这是 InnoDB 在REPEATABLE READ
隔离级别下的默认锁策略,用于防止幻读。
示例 (概念):
-- 假设 age 索引上有值 20, 30, 40
-- 事务 T1 执行 (RR 级别):
SELECT * FROM users WHERE age = 30 FOR UPDATE;
-- InnoDB 会锁定 age=30 这条记录 (Record Lock),同时锁定 (20, 30] 这个区间 (Next-Key Lock)。
-- 这意味着其他事务不能插入 age 在 (20, 30] 范围内的记录,也不能修改或删除 age=30 的记录。
2. 表级锁(InnoDB支持但不常用)
LOCK TABLES ... WRITE/READ
: 显式表锁,会极大降低并发,不推荐在 InnoDB 中常规使用。- 自动表锁场景: DDL 操作(如
ALTER TABLE
)会获取表级元数据锁 (MDL),阻塞 DML。某些特殊插入(如涉及AUTO_INCREMENT
的复杂插入)可能短暂持有表级锁。
3. 意向锁(Intention Locks)
意向锁是 表级锁,用于指示事务 打算 在表中的某些行上加什么类型的锁(S 或 X)。它们不直接阻塞行锁,而是用于快速判断表级锁和行级锁的兼容性。
- 意向共享锁 (IS): 事务准备在某些行上加 S 锁。获取行 S 锁前必须先获取表的 IS 锁。
- 意向排他锁 (IX): 事务准备在某些行上加 X 锁。获取行 X 锁前必须先获取表的 IX 锁。
工作原理:当一个事务想获取整个表的 S 锁或 X 锁时,只需检查表上是否存在冲突的意向锁(如获取表 X 锁要检查是否有 IS 或 IX 锁),而无需扫描每一行,提高了效率。IS 与 IX 锁是兼容的。
4. 自动锁定行为
SELECT ... FOR UPDATE
: 对匹配行加 X 锁(Record / Next-Key)。SELECT ... LOCK IN SHARE MODE
: 对匹配行加 S 锁(Record / Next-Key)。INSERT
: 在新插入行上加 X 锁。可能产生 Gap/Next-Key 锁以防唯一键冲突或幻读。UPDATE
: 对匹配的行加 X 锁。DELETE
: 对匹配的行加 X 锁。
三、事务与锁的关系
锁是实现 ACID 中隔离性 (Isolation) 的关键技术。
1. 四种事务隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 | InnoDB RR 典型锁策略 (简化) |
---|---|---|---|---|
READ UNCOMMITTED | 可能发生 ✅ | 可能发生 ✅ | 可能发生 ✅ | 基本无锁 (很少用) |
READ COMMITTED | 不会发生 ❌ | 可能发生 ✅ | 可能发生 ✅ | Record Lock (锁记录) |
REPEATABLE READ | 不会发生 ❌ | 不会发生 ❌ | 基本不发生 ❌ | Next-Key Lock (锁记录+间隙) |
SERIALIZABLE | 不会发生 ❌ | 不会发生 ❌ | 不会发生 ❌ | 可能使用表锁/更强范围锁 |
2. 幻读与间隙锁
在 REPEATABLE READ
级别,InnoDB 主要通过 Next-Key Lock (包含 Gap Lock) 来防止幻读。通过锁定查询范围内的间隙,阻止了其他事务插入满足该范围条件的新行。
四、锁冲突与兼容性
1. 锁兼容性矩阵(简化版,行锁与意向锁)
请求锁 \ 已持有锁 | IS | IX | S (行) | X (行) |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 冲突 |
IX | 兼容 | 兼容 | 冲突 | 冲突 |
S (行) | 兼容 | 冲突 | 兼容 | 冲突 |
X (行) | 冲突 | 冲突 | 冲突 | 冲突 |
(表级 S/X 锁与行级锁的兼容性通过意向锁判断)
五、死锁与事务管理
1. 死锁产生条件 (需同时满足)
- 互斥条件: 资源独占。
- 占有且等待: 持有资源同时请求其他资源。
- 非抢占: 不能强行剥夺已持有资源。
- 循环等待: 事务间形成等待资源的闭环。
2. InnoDB 死锁检测
InnoDB 能自动检测死锁,并自动回滚其中一个(通常是影响最小的)事务来解决死锁。可以通过 innodb_deadlock_detect
参数开关(通常保持开启)。
3. 死锁示例
场景:两个事务试图以相反顺序更新两行记录。
-- 终端 1 (事务 A)
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 成功,持有 id=1 的 X 锁
-- 稍等
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 尝试获取 id=2 的 X 锁,可能阻塞-- 终端 2 (事务 B)
START TRANSACTION;
UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- 成功,持有 id=2 的 X 锁
-- 稍等
UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- 尝试获取 id=1 的 X 锁,阻塞-- 此时,事务 A 等待事务 B 释放 id=2 的锁,事务 B 等待事务 A 释放 id=1 的锁,形成死锁。
-- InnoDB 会检测到死锁,并回滚其中一个事务,另一个事务得以继续。
4. 查看死锁日志
获取最近死锁的详细信息。
SHOW ENGINE INNODB STATUS;
在输出中查找 LATEST DETECTED DEADLOCK
部分。
六、锁的监控与分析
1. 查看锁信息视图
-- 查看当前活动事务
SELECT * FROM information_schema.innodb_trx;
-- 查看当前持有的锁 (可能需要高权限)
SELECT * FROM information_schema.innodb_locks;
-- 查看锁等待关系
SELECT * FROM information_schema.innodb_lock_waits;
2. 查看进程列表
识别慢查询或阻塞的连接。
SHOW FULL PROCESSLIST;
3. 终止连接
强制结束有问题的连接/事务。
KILL <thread_id>; -- 从 SHOW FULL PROCESSLIST 获取 Id
警告:KILL 操作可能导致事务回滚和数据问题,务必谨慎。
七、MyISAM 引擎的锁机制(对比了解)
1. 表级锁
MyISAM 只使用表级锁,并发性能较差。写操作会阻塞所有其他读写,读操作会阻塞写操作。
2. 锁类型
- 表读锁 (共享): 允许多个读,阻塞写。
- 表写锁 (排他): 阻塞所有其他读写。
3. 写优先
写锁请求通常优先于读锁请求,可能导致读请求长时间等待(饿死)。
八、锁的开发实践与优化建议
1. 使用 InnoDB 引擎: 获得事务支持和行级锁带来的高并发性。
2. 优化 SQL,精准锁定: 使用索引(尤其是主键/唯一索引)进行 WHERE
过滤,减少锁定的行数和范围。避免无索引条件的 DML 操作。
3. 控制事务范围和时长: 保持事务简短。将非数据库操作移出事务。快速 COMMIT
或 ROLLBACK
。
4. 合理选择隔离级别: 在满足业务一致性前提下,使用最低隔离级别(如 READ COMMITTED
)通常能获得更好并发性。
5. 谨慎使用显式锁: 仅在必要时使用 FOR UPDATE
/ LOCK IN SHARE MODE
。
6. 避免死锁:
- 约定资源访问顺序。
- 减小事务粒度。
- 使用较低隔离级别。
- 设置合理的
innodb_lock_wait_timeout
。
九、其他相关内容(延伸)
1. Metadata Lock (MDL / 元数据锁)
保护表结构定义。DDL 操作会获取 MDL,阻塞后续对该表的 DML 和 DDL。未提交事务持有的读锁会阻塞 DDL。
2. Auto-inc Lock (自增锁)
用于 AUTO_INCREMENT
列生成连续值。通常是表级锁(旧模式)或更轻量级锁(新模式,由 innodb_autoinc_lock_mode
控制),影响并发插入性能。
3. 外键锁
维护外键约束时,对父表相关行的短暂锁定(通常是 S 锁),以确保引用完整性检查的原子性。
练习题 (Practice Exercises - Locks with Answers)
-
SELECT ... LOCK IN SHARE MODE
获取的是什么类型的锁?它允许其他事务做什么,不允许做什么?
答案: 获取的是行级共享锁 (S 锁)。它允许其他事务读取这些行并获取 S 锁,但不允许其他事务获取这些行的 X 锁(即不允许修改或删除)。 -
InnoDB 在
REPEATABLE READ
隔离级别下,默认使用哪种锁策略来防止幻读?它的组成是什么?
答案: 默认使用 Next-Key Lock (临键锁)。它由 Record Lock (记录锁) 和 Gap Lock (间隙锁) 组成。 -
如果事务 A 更新了
id=1
的行,事务 B 也想更新id=1
的行,会发生什么?为什么?
答案: 事务 B 会被阻塞。因为事务 A 更新时会隐式地在id=1
的行(索引记录)上获取排他锁 (X 锁)。X 锁与任何其他锁(包括其他事务想获取的 X 锁)都是冲突的。事务 B 必须等待事务 A 提交或回滚释放锁后才能继续。 -
如何查看 MySQL 中最近发生的死锁信息?
答案: 使用命令SHOW ENGINE INNODB STATUS;
,然后在输出中查找LATEST DETECTED DEADLOCK
部分。 -
为什么在 InnoDB 中通常不推荐使用
LOCK TABLES
命令?
答案: 因为LOCK TABLES
是显式的表级锁,它会锁定整个表,大大降低了 InnoDB 行级锁所能提供的高并发性能。应尽量利用 InnoDB 的自动行级锁定机制。