MySQL 锁机制
一、全局锁(Global Lock)
1. 核心定义
-
锁定整个MySQL实例,所有库/表的读写操作均被阻塞
-
仅支持读操作(加锁后),所有DDL、DML写操作均被阻塞
2. 加锁/解锁方式
sql
-- 加全局读锁(阻塞所有写操作)
FLUSH TABLES WITH READ LOCK (FTWRL);
-- 解锁
UNLOCK TABLES;
3. 适用场景
-
全库逻辑备份:保证备份数据的一致性
-
替代方案 :MySQL 5.6+ 可使用
mysqldump --single-transaction(仅限InnoDB) -
MySQL 8.0+ 推荐使用
mysqldump --source-data或物理备份工具
4. 核心风险
-
加锁期间业务完全阻塞(无法执行任何写操作)
-
主从架构中,主库只读会导致从库同步延迟
-
FTWRL会关闭所有打开的表,可能导致性能抖动
二、表级锁(Table-Level Lock)
1. 基础表锁(READ/WRITE)
| 锁类型 | 核心定义 | 兼容关系(同一张表) |
|---|---|---|
| 表读锁(READ) | 允许所有会话读 ,禁止所有会话写 | 多个READ锁兼容;与WRITE锁互斥 |
| 表写锁(WRITE) | 允许持有锁的会话读写 ,禁止其他所有会话读写 | 与任何表锁(READ/WRITE)互斥 |
sql
-- 加表读锁
LOCK TABLES `user` READ;
-- 加表写锁
LOCK TABLES `user` WRITE;
-- 解锁
UNLOCK TABLES;
2. 元数据锁(MDL)
核心定义
-
自动加锁 的表级锁,保护表结构不被并发修改
-
MySQL 5.5+引入,避免DDL与DML冲突
加锁规则
| 操作类型 | 加锁类型 | 核心特性 |
|---|---|---|
| SELECT/INSERT/UPDATE/DELETE | MDL读锁(共享) | 多个读锁兼容 |
| ALTER TABLE/DROP TABLE | MDL写锁(排他) | 与所有MDL锁互斥 |
MDL生命周期
-
读锁:事务开始时自动获取,事务提交/回滚时释放
-
写锁:DDL执行期间持有,执行完成立即释放
核心风险:MDL锁等待
sql
-- 场景:长事务阻塞DDL
-- 会话1(长事务):
BEGIN;
SELECT * FROM users WHERE id = 1; -- 获取MDL读锁
-- 不提交,保持事务打开
-- 会话2(DDL操作):
ALTER TABLE users ADD COLUMN age INT; -- 等待MDL写锁,被阻塞!
-- 会话3(新查询):
SELECT * FROM users LIMIT 1; -- 也被阻塞,等待MDL读锁
优化建议:
-
监控长事务:
SELECT * FROM information_schema.INNODB_TRX; -
DDL操作选择业务低峰期
-
使用Online DDL(MySQL 5.6+)
3. 意向锁(IS/IX)
核心定义
-
InnoDB专属表级标记锁
-
行锁的"意向声明" ,避免表锁与行锁冲突
-
减少锁冲突检查的开销(无需遍历每行)
分类与加锁规则
| 意向锁类型 | 触发条件 | 核心作用 |
|---|---|---|
| IS(意向共享) | 事务计划对某些行加S锁前 | 标记"表内有/将有共享行锁" |
| IX(意向排他) | 事务计划对某些行加X锁前 | 标记"表内有/将有排他行锁" |
兼容关系矩阵
| 请求锁↓ \ 已有锁 → | 无锁 | IS | IX | S(表读锁) | X(表写锁) |
|---|---|---|---|---|---|
| IS | ✅ | ✅ | ✅ | ✅ | ❌ |
| IX | ✅ | ✅ | ✅ | ❌ | ❌ |
| S | ✅ | ✅ | ❌ | ✅ | ❌ |
| X | ✅ | ❌ | ❌ | ❌ | ❌ |
三、行级锁(Row-Level Lock)⭐⭐⭐
注意:行锁实际上是加在索引上的锁,而在InnoDB中默认加行锁就是加临键锁
补充注意:仅当 WHERE 条件命中索引列(如主键)时才是行锁,无索引会升级为表锁 ;仅 InnoDB存储引擎 有行锁。
1. 基本行锁(S锁/X锁)
| 锁类型 | 核心定义 | 兼容关系(同一行) | 加锁场景 | 解锁时机 |
|---|---|---|---|---|
| 共享锁(S锁) | 允许读,禁止写 | S-S兼容,S-X互斥 | SELECT ... FOR SHARE |
事务提交/回滚 |
| 排他锁(X锁) | 禁止读写 | X-X互斥,X-S互斥 | SELECT ... FOR UPDATE UPDATE/DELETE |
事务提交/回滚 |
兼容性矩阵(同一行)
| 请求锁 ↓ \ 当前锁 → | 无锁 | S锁 | X锁 |
|---|---|---|---|
| 请求S锁 | ✅ | ✅ | ❌ |
| 请求X锁 | ✅ | ❌ | ❌ |
MySQL 中行锁的使用
1. SELECT ... FOR SHARE(共享锁 / S 锁)
核心说明 :加行级共享锁(S 锁),多个事务可同时对该行加共享锁,但无法加排他锁;适用于只读但需防止数据被修改的场景,MySQL 8.0 前可写为 SELECT ... LOCK IN SHARE MODE(等价)。兼容 / 阻塞场景:
- ✅ 允许:其他事务执行
SELECT ... FOR SHARE锁定该行 - ✅ 允许:普通 SELECT(不加锁查询)
- ❌ 阻塞:其他事务执行
SELECT ... FOR UPDATE锁定该行 - ❌ 阻塞:其他事务执行 UPDATE 操作修改该行
- ❌ 阻塞:其他事务执行 DELETE 操作删除该行
2. SELECT ... FOR UPDATE(排他锁 / X 锁)
核心说明 :加行级排他锁(X 锁),锁定后其他事务无法对该行加任何锁(共享锁 / 排他锁),仅能执行不加锁的普通查询;锁的释放时机为事务提交或回滚。兼容 / 阻塞场景:
- ✅ 允许:普通 SELECT(不加锁查询)
- ❌ 阻塞:其他事务执行
SELECT ... FOR SHARE锁定该行 - ❌ 阻塞:其他事务执行
SELECT ... FOR UPDATE锁定该行 - ❌ 阻塞:其他事务执行 UPDATE 操作修改该行
- ❌ 阻塞:其他事务执行 DELETE 操作删除该行
3. UPDATE/DELETE 语句(自动加排他锁 / X 锁)
核心说明:执行 UPDATE/DELETE 时,InnoDB 会自动为 WHERE 条件命中的行加排他锁(X 锁),锁释放时机为事务提交或回滚;依赖索引实现行锁,无索引则升级为表锁。兼容 / 阻塞场景:同上X锁
4. INSERT 语句(隐式排他锁 + 间隙锁 / 临键锁)
核心说明 :INSERT 会为插入的行自动加排他锁(X 锁);兼容 / 阻塞场景:同上X锁;与其他不同的是:若插入列有唯一约束 / 主键,其他事务插入相同值会被阻塞;若无唯一约束,会触发间隙锁 / 临键锁(防止幻读)。
补充关键注意事项
-
锁生效前提:仅 InnoDB 引擎下生效,事务需显式开启(关闭自动提交或用 BEGIN/START TRANSACTION);
-
索引影响:WHERE 条件未命中索引时,行锁升级为表锁;
-
锁释放时机:行锁仅在事务 COMMIT/ROLLBACK 时释放,长时间未提交易导致锁等待 / 死锁;
-
死锁处理 :InnoDB 自动检测死锁并回滚其中一个事务 ,可通过
SHOW ENGINE INNODB STATUS查看死锁日志。
2. 记录锁、间隙锁、临键锁
在InnoDB中默认加行锁就是加临键锁
(1) 记录锁(Record Lock)
-
锁定单行记录 (基于主键/唯一索引)
- 记录锁是上述行锁的一种具体实现
-
作用 :防止并发修改同一行
-
示例:
sql
-- id=1的记录被锁定
UPDATE users SET balance = balance - 100 WHERE id = 1;
(2) 间隙锁(Gap Lock)
-
锁定索引记录之间的间隙 (不包含记录本身)
- 即左开右开
-
核心作用 :防止幻读(在RR隔离级别下)
-
触发条件:RR隔离级别 + 范围查询/非唯一索引等值查询
sql
-- 示例数据:id=10, 20, 30
-- 锁定间隙:(10, 20), (20, 30), (30, +∞)
-- 事务1:
SELECT * FROM users WHERE id > 15 FOR UPDATE;
-- 阻止其他事务插入 id=16, 25, 35 等记录
-- 事务2(被阻塞):
INSERT INTO users (id, name) VALUES (25, '张三');
间隙锁特性:
-
仅RR级别生效:RC级别无间隙锁
-
索引相关:基于索引的键值间隙
-
多区间锁定:可能锁定多个间隙
-
兼容性规则 :间隙锁之间兼容(两个事务可以锁定相同间隙)
间隙锁的边界问题
sql
-- 示例表:id (10, 20, 30)
-- 最小边界:(-∞, 第一行]
-- 最大边界:[最后一行, +∞)
-- 中间间隙:(前一行, 后一行)
-- 事务1:查询不存在的记录
SELECT * FROM users WHERE id = 15 FOR UPDATE;
-- 锁定间隙:(10, 20) -- 注意:不包含10和20
-- 事务2:插入边界值测试
INSERT INTO users (id) VALUES (9); -- ✅ 允许(不在锁定间隙)
INSERT INTO users (id) VALUES (10); -- ✅ 允许(记录已存在,记录锁不冲突)
INSERT INTO users (id) VALUES (11); -- ❌ 阻塞(在锁定间隙内)
INSERT INTO users (id) VALUES (20); -- ✅ 允许(记录已存在)
(3) 临键锁(Next-Key Lock)
-
记录锁 + 间隙锁(InnoDB默认行锁策略)
-
公式:临键锁 = 记录锁 + 前一个间隙锁
-
锁定范围:当前记录 + 前一个间隙
-
即左开右闭
-
sql
-- 示例数据:id=10, 20, 30
-- 事务1:
SELECT * FROM users WHERE id = 20 FOR UPDATE;
-- 临键锁范围:(10, 20]
-- 包含:记录20(记录锁) + 间隙(10, 20)(间隙锁)
-- 以下操作此时会被阻塞
INSERT INTO users (id) VALUES (15); -- 插入间隙
UPDATE users SET name='test' WHERE id = 20; -- 修改记录
临键锁的特殊情况
在InnoDB中默认加行锁就是加临键锁,以下时特殊情况
场景1:唯一索引等值查询
sql
-- id是主键(唯一索引)
SELECT * FROM users WHERE id = 20 FOR UPDATE;
-- 仅加记录锁,不加间隙锁(因为唯一性保证)
场景2:非唯一索引等值查询
sql
-- name有非唯一索引,数据:('A', 'A', 'B', 'B', 'C')
SELECT * FROM users WHERE name = 'B' FOR UPDATE;
-- 锁定:
-- 1. 所有name='B'的记录锁
-- 2. 间隙锁:(第一个'B', 最后一个'B')
-- 3. 可能的前后间隙
场景3:范围查询的锁升级
sql
-- 事务1:
SELECT * FROM users WHERE id >= 15 AND id <= 25 FOR UPDATE;
-- 可能锁定:
-- 1. 所有15-25之间的记录锁
-- 2. 间隙:(上一个记录, 15], (15, 25], (25, 下一个记录]
-- 实际范围可能大于WHERE条件!
3. 行锁与隔离级别的关系
| 隔离级别 | 记录锁 | 间隙锁 | 临键锁 | 幻读防护 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ | ❌ | ❌ | ❌ |
| READ COMMITTED | ✅ | ❌ | ❌ | ❌ |
| REPEATABLE READ | ✅ | ✅ | ✅(默认) | ✅ |
| SERIALIZABLE | ✅ | ✅ | ✅ | ✅ |
四、乐观锁与悲观锁
这是锁的设计思想,并非 MySQL 内置的具体锁类型,而是业务 / 代码层面的实现方式。
1. 悲观锁(Pessimistic Lock)
-
核心思想 :假设并发冲突一定会发生,先加锁再操作
-
实现方式 :利用 MySQL 内置锁(如行锁
SELECT ... FOR UPDATE、表锁) -
适用场景:并发冲突概率高、写操作多的场景
-
示例:
sql-- 用行锁实现悲观锁 BEGIN; SELECT * FROM users WHERE id=1 FOR UPDATE; -- 加排他锁 UPDATE users SET balance=balance-100 WHERE id=1; COMMIT;
2. 乐观锁(Optimistic Lock)
-
核心思想 :假设并发冲突不会发生,先操作后校验
-
实现方式 :通过版本号(
version)或时间戳(update_time)实现 -
适用场景:并发冲突概率低、读操作多的场景
-
示例:
sql-- 1. 查询数据时获取版本号 SELECT balance, version FROM users WHERE id=1; -- 2. 更新时校验版本号 UPDATE users SET balance=balance-100, version=version+1 WHERE id=1 AND version=原版本号; -- 3. 判断影响行数:若为0则说明版本冲突,需重试
五、锁机制实战要点
1. 索引与锁的关系
sql
-- 情况1:使用主键索引(记录锁)
UPDATE users SET age=25 WHERE id=100; -- 仅锁定id=100的行
-- 情况2:使用普通索引
-- 假设name有普通索引
UPDATE users SET age=25 WHERE name='张三';
-- 锁定:1.name='张三'的记录锁 2.相关间隙锁
-- 情况3:无索引(表锁!)
UPDATE users SET age=25 WHERE age=30; -- 如果age无索引→表锁
2. 死锁分析与规避
死锁场景示例:
sql
-- 事务1
BEGIN;
UPDATE users SET balance=balance-100 WHERE id=1; -- 锁定id=1
UPDATE users SET balance=balance+100 WHERE id=2; -- 尝试锁定id=2
-- 事务2(并发执行)
BEGIN;
UPDATE users SET balance=balance-200 WHERE id=2; -- 锁定id=2
UPDATE users SET balance=balance+200 WHERE id=1; -- 尝试锁定id=1(死锁!)
死锁规避策略:
- 统一加锁顺序:约定所有事务按相同顺序加锁
- 减少事务粒度:拆分大事务,及时提交
- 使用索引:避免无索引查询导致表锁
- 设置超时 :
SET innodb_lock_wait_timeout = 30; - 死锁检测 :
SHOW ENGINE INNODB STATUS\G查看死锁信息
3. 锁优化建议
-
索引设计:为WHERE条件、JOIN条件创建合适索引
-
事务设计:
- 保持事务短小
- 避免长事务中的锁持有
- 批量操作使用LIMIT分批次
-
SQL优化:
- 避免
SELECT *,只取需要字段 - 使用覆盖索引减少回表
- 避免大范围UPDATE/DELETE
- 避免
-
隔离级别选择:
- 默认使用RR级别(防幻读)
- 高并发场景可考虑RC级别(减少间隙锁)
六、锁监控与排查命令
1. 锁状态查看
sql
-- 查看表锁使用情况
SHOW OPEN TABLES WHERE In_use > 0;
-- 查看当前锁等待
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 查看事务详情
SELECT * FROM information_schema.INNODB_TRX;
-- 查看MDL锁(MySQL 5.7+)
SELECT * FROM performance_schema.metadata_locks;
2. InnoDB引擎状态
sql
-- 查看详细锁信息(包含最近死锁)
SHOW ENGINE INNODB STATUS\G
-- 重点关注:
-- 1. LATEST DETECTED DEADLOCK(最近死锁)
-- 2. TRANSACTIONS(当前事务)
-- 3. ROW OPERATIONS(行操作)
3. 性能监控
sql
-- 锁等待超时设置
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
SET GLOBAL innodb_lock_wait_timeout = 30;
-- 死锁检测
SHOW VARIABLES LIKE 'innodb_deadlock_detect';
-- ON:自动检测死锁(默认)
-- OFF:关闭检测,依赖超时机制
-- 查看锁统计
SHOW STATUS LIKE 'Innodb_row_lock%';
-- Innodb_row_lock_current_waits:当前等待行锁数量
-- Innodb_row_lock_time:行锁总等待时间
-- Innodb_row_lock_time_avg:平均等待时间
-- Innodb_row_lock_time_max:最长等待时间
-- Innodb_row_lock_waits:行锁等待总次数
七、补充
实战建议
-
明确索引设计:了解每个查询使用的索引类型
-
监控锁等待:定期检查锁等待情况
-
测试锁行为:在测试环境验证锁范围
-
使用EXPLAIN:分析查询执行计划,预测锁行为
-
考虑业务场景:权衡一致性与并发性的需求
总结要点
-
全局锁用于全库备份,慎用
-
表级锁中,MDL锁是常见阻塞原因
-
行级锁是InnoDB核心,理解记录锁、间隙锁、临键锁的区别
-
RR级别默认使用临键锁防幻读,RC级别无间隙锁
-
死锁可以通过统一加锁顺序、短事务、合适索引来规避
-
监控工具是排查锁问题的关键