这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Lock 保护事务之间的数据一致性,生命周期通常到提交或回滚;Latch 保护 MySQL 和 InnoDB 内部临界区,生命周期极短。两者都表现为“等待”,但排查方式完全不同。
18.1 Lock 与 Latch 的区别
| 项目 | Lock | Latch |
|---|---|---|
| 保护对象 | 表、行、区间、元数据 | 内存临界区、页、哈希表、互斥量 |
| 生命周期 | 事务级别或语句级别 | 微秒或毫秒级 |
| 是否参与死锁检测 | InnoDB 行锁和表锁参与 | 通常以自旋等待为主 |
| 常见表现 | lock wait timeout、deadlock | mutex wait、性能下降 |
| 排查工具 | data_locks、innodb_status |
Performance Schema wait events |
用户日常说的“数据库锁”多数指 Lock,但性能毛刺常常来自 Latch。
18.2 锁的模式
常见锁模式:
| 锁 | 说明 |
|---|---|
| IS | 意向共享锁 |
| IX | 意向排他锁 |
| S | 共享锁 |
| X | 排他锁 |
| AUTO-INC | 自增锁 |
| RECORD | 记录相关锁,包含记录、间隙、Next-Key |
| TABLE | 表级锁 |
| METADATA LOCK | 元数据锁 |
兼容矩阵简化:
| 已持有 / 请求 | S | X |
|---|---|---|
| S | 兼容 | 冲突 |
| X | 冲突 | 冲突 |
意向锁用于快速判断表级操作是否与行级锁冲突,不需要扫描所有行锁。
18.3 共享锁与排他锁
共享读:
START TRANSACTION;
SELECT *
FROM orders
WHERE id = 1
FOR SHARE;
COMMIT;
排他读:
START TRANSACTION;
SELECT *
FROM orders
WHERE id = 1
FOR UPDATE;
COMMIT;
区别:
FOR SHARE允许其他事务也加 S 锁;FOR SHARE阻塞其他事务的 X 锁;FOR UPDATE阻塞其他 S 和 X 锁;- 二者都是当前读;
- 都受执行计划和索引影响。
如果 WHERE 没有有效索引,锁范围可能扩大,执行时间也可能变长。
18.4 意向锁
事务准备给行加 S 锁前,先在表上标记 IS;准备给行加 X 锁前,先标记 IX。
作用:
表级操作只需要检查意向锁
不需要逐行判断是否存在行锁
例如:
LOCK TABLES orders WRITE;
如果 orders 上存在 IX,则该请求需要等待。
18.5 表级锁
显式表锁:
LOCK TABLES orders READ;
UNLOCK TABLES;
LOCK TABLES orders WRITE;
UNLOCK TABLES;
生产 OLTP 很少使用 LOCK TABLES,常见问题:
- 锁粒度过大;
- 阻塞面广;
- 与事务语义混淆;
- 备份工具或管理操作误用;
- 忘记 UNLOCK。
如果需要一致性备份,InnoDB 应优先使用 MVCC 快照工具,如 mysqldump --single-transaction 或物理备份。
18.6 元数据锁 MDL
MDL 保护表结构稳定性:
| 操作 | 通常需要 |
|---|---|
| SELECT | MDL 读锁 |
| DML | MDL 读锁 |
| ALTER TABLE | MDL 写锁 |
风险场景:
1. 长查询持有 MDL 读锁;
2. ALTER 请求 MDL 写锁并排队;
3. 后续所有新 SELECT 在 MDL 队列后排队;
4. 表看起来完全不可访问。
查看 MDL:
SELECT *
FROM performance_schema.metadata_locks
WHERE OBJECT_SCHEMA = 'shop'
AND OBJECT_NAME = 'orders';
MySQL 8.0 支持设置 DDL 等待时间:
SET SESSION lock_wait_timeout = 10;
ALTER TABLE orders
ADD COLUMN remark VARCHAR(255),
ALGORITHM=INPLACE,
LOCK=NONE;
若超时失败,避免后续请求被长期堵塞。
18.7 记录锁、间隙锁与 Next-Key Lock
InnoDB 行级锁实际作用对象是索引记录及其间隙:
| 锁 | 范围 |
|---|---|
| Record Lock | 索引记录本身 |
| Gap Lock | 索引记录之间的开区间 |
| Next-Key Lock | 记录 + 前面的间隙,左开右闭 |
| Insert Intention Lock | 插入前等待间隙冲突释放 |
示例索引值:
10, 20, 30
可能存在:
(-∞, 10), (10, 20), (20, 30), (30, +∞)
第 19 章会用两个会话详细实验。
18.8 自增锁
查看模式:
SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode';
| 模式 | 含义 |
|---|---|
| 0 | 传统模式,语句级 AUTO-INC 表锁 |
| 1 | 连续模式,简单插入用轻量互斥,批量插入可能用表锁 |
| 2 | 交错模式,并发最高,插入 ID 可能交错 |
MySQL 8.0 默认是 2。主从使用 STATEMENT Binlog 时,模式 2 可能导致复制不确定;ROW 格式下更安全。
查看自增值:
SHOW TABLE STATUS LIKE 'orders'\G
自增 ID 不保证连续,也不保证不重用,业务不要依赖它表达顺序号或编号连续性。
18.9 外键锁
虽然互联网业务常不使用外键,但如果表存在外键,InnoDB 会执行引用检查,可能锁父表或子表相关行。
查看外键:
SELECT *
FROM information_schema.REFERENTIAL_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = 'shop';
外键风险:
- 写入额外检查;
- 父子表锁交互;
- 批量导入变慢;
- DDL 和迁移复杂;
- 死锁概率上升。
不使用外键时,要通过应用校验和定时对账补偿。
18.10 查看锁
MySQL 8.0 使用 Performance Schema:
SELECT
ENGINE_TRANSACTION_ID,
THREAD_ID,
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME,
LOCK_TYPE,
LOCK_MODE,
LOCK_DATA
FROM performance_schema.data_locks;
查看锁等待:
SELECT
requesting.ENGINE_TRANSACTION_ID AS requesting_trx,
blocking.ENGINE_TRANSACTION_ID AS blocking_trx,
requesting.OBJECT_NAME AS table_name,
requesting.LOCK_TYPE,
requesting.LOCK_MODE,
requesting.LOCK_DATA
FROM performance_schema.data_lock_waits w
JOIN performance_schema.data_locks requesting
ON requesting.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_ID
JOIN performance_schema.data_locks blocking
ON blocking.ENGINE_LOCK_ID = w.BLOCKING_ENGINE_LOCK_ID;
查看 InnoDB 死锁:
SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';
SHOW ENGINE INNODB STATUS\G
18.11 Latch 与互斥量
常见 Latch:
| 对象 | 说明 |
|---|---|
| buffer pool mutex | 保护缓冲池结构 |
| page latch | 保护数据页 |
| lock system mutex | 保护锁哈希表 |
| dict mutex | 保护数据字典 |
| log mutex | 保护日志写入结构 |
| purge coordinator | Purge 调度相关 |
查看等待:
SELECT
EVENT_NAME,
COUNT_STAR,
SUM_TIMER_WAIT / 1000000000 AS wait_ms
FROM performance_schema.events_waits_summary_global_by_event_name
WHERE EVENT_NAME LIKE 'wait/synch/%'
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 20;
常见原因:
- 高并发热点页;
- Buffer Pool 过小;
- 自增或索引热点;
- 长时间持有内部锁;
- 高频元数据操作;
- 版本缺陷;
- 资源不足。
处理方向:
- 升级小版本修复;
- 降低热点;
- 拆分写入位置;
- 调整 Buffer Pool;
- 收集 Performance Schema;
- 找数据库专家或厂商分析。
18.12 锁等待参数
查看超时:
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
会话级设置:
SET SESSION innodb_lock_wait_timeout = 10;
默认通常为 50 秒。在线应用应设置更短超时并做好重试,避免请求堆积。
跳过锁等待:
SELECT *
FROM task_queue
WHERE status = 'PENDING'
LIMIT 1
FOR UPDATE SKIP LOCKED;
不等待:
SELECT *
FROM task_queue
WHERE id = 1
FOR UPDATE NOWAIT;
SKIP LOCKED 适合任务队列抢占;NOWAIT 适合明确告诉用户资源忙。
18.13 锁设计原则
- 锁范围尽量精确;
- 关联条件必须有索引;
- 多行更新使用稳定顺序;
- 事务尽量短;
- 避免锁内做外部调用;
- 热点数据拆分或异步化;
- 幂等键降低重复写入;
- 死锁要有重试策略;
- 管理操作要评估 MDL;
- 监控锁等待、长事务和死锁。
本章小结
Lock 保护事务数据,粒度包括表、元数据、记录、间隙和 Next-Key;Latch 保护数据库内部临界区。排查业务锁等待使用 data_locks、data_lock_waits 和 InnoDB 状态;排查性能毛刺则要关注 Performance Schema 的同步等待。锁优化的本质是缩小锁范围、缩短持有时间和消除热点冲突。
思考题
- Lock 和 Latch 有什么区别?
- 意向锁的作用是什么?
- MDL 为什么可能让一个 ALTER 影响所有 SELECT?
SKIP LOCKED和NOWAIT分别适合什么场景?- 如何区分行锁等待和 Latch 等待?