文章目录
- [一、表锁(Table Lock)](#一、表锁(Table Lock))
- [二、行锁(Row Lock)](#二、行锁(Row Lock))
- [三、间隙锁(Gap Lock)](#三、间隙锁(Gap Lock))
- [四、临键锁(Next-Key Lock)](#四、临键锁(Next-Key Lock))
- 五、四种锁对比总结
- [六、为什么间隙锁 / 临键锁在企业中是"性能杀手"?](#六、为什么间隙锁 / 临键锁在企业中是“性能杀手”?)
- 七、如何在实际项目中避免锁冲突?
- 八、总结
在 MySQL(InnoDB)中,锁机制是并发控制的核心 。
很多线上性能问题、死锁问题、请求阻塞,最终都能追溯到一句话:
👉 你到底锁了什么?锁住了多大范围?
本文将系统讲解 InnoDB 中最常见、也最容易"踩坑"的几种锁:
- 表锁(Table Lock)
- 行锁(Row Lock)
- 间隙锁(Gap Lock)
- 临键锁(Next-Key Lock)
一、表锁(Table Lock)
表锁属于 锁整个表的粗粒度锁。
在 InnoDB 中,普通 DML 几乎不会使用表锁,表锁通常只出现在以下场景:
ALTER / DROP / TRUNCATE等 DDL 操作- 显式使用
LOCK TABLE
示例
sql
LOCK TABLE patient WRITE;
UPDATE patient SET age = 20 WHERE id = 1;
UNLOCK TABLES;
特点
- 锁住整张表
- 阻塞所有读写操作
- 并发能力极差
适用场景
- 表结构变更
- 批量数据清洗、导入
在医疗行业中常见于:
- 医保目录批量导入
- 历史数据迁移
- 临时冻结表进行修复
📌 结论:业务代码中应尽量避免显式表锁
二、行锁(Row Lock)
行锁是 InnoDB 的核心优势,也是业务中最常用、也是最理想的锁形式。
行锁的正确理解(重要)
在使用 主键或唯一索引进行等值条件更新 时,
InnoDB 会使用 记录锁(Record Lock) ,
仅锁定命中的那一行记录,不会锁定相邻间隙。
示例
sql
UPDATE patient SET age = 20 WHERE id = 100;
如果 id 是 主键或唯一索引:
👉 只会锁 id = 100 这一行(记录锁)
这是 InnoDB 中最理想、并发性最高的加锁方式。
行锁的关键特性
| 特性 | 说明 |
|---|---|
| 粒度小 | 只锁必要的数据行 |
| 并发高 | 不影响其他行 |
| 强依赖索引 | 必须命中索引,才能真正做到行锁 |
⚠️ 重点警告:索引失效 ≠ 真正的表锁,但效果接近"锁全表"
sql
UPDATE patient SET age = 20 WHERE name LIKE '%张%';
如果 name 没有索引:
- InnoDB 会进行 全表扫描
- 并对扫描到的记录 逐行加行锁
- 实际效果上:大量并发被阻塞,表现得像"锁表"
📌 这是线上事故的高发点之一,也是 SQL 优化必须重点关注的地方。
三、间隙锁(Gap Lock)
间隙锁并不是锁记录本身,而是锁 记录之间的"空隙"。
它存在的唯一目的:
👉 防止幻读(Phantom Read)
示例说明
假设表 patient,age 有索引:
| id | age |
|---|---|
| 1 | 20 |
| 2 | 30 |
| 3 | 40 |
执行:
sql
SELECT * FROM patient WHERE age > 20 FOR UPDATE;
InnoDB 会锁住区间:
(20, +∞)
这意味着:
sql
INSERT INTO patient(age) VALUES (25);
👉 会被阻塞
间隙锁出现的必要条件
- ✔ RR(Repeatable Read)隔离级别
- ✔ 范围查询(>、<、BETWEEN、≠)
- ✔ 锁定读(
FOR UPDATE/LOCK IN SHARE MODE)
间隙锁的本质作用
- 阻止范围内插入新数据
- 从根本上避免幻读
📌 代价是:插入操作会被阻塞
四、临键锁(Next-Key Lock)
临键锁 = 记录锁(行锁) + 间隙锁
这是 InnoDB 在 RR 隔离级别下的默认加锁策略。
示例
sql
SELECT * FROM patient WHERE age = 30 FOR UPDATE;
InnoDB 实际锁定的是:
- 记录
age = 30 - 左间隙
(20, 30) - 右间隙
(30, 40)
锁定范围:
(20, 40]
为什么要锁间隙?
如果只锁 age = 30 这一行:
- 其他事务可以插入
age = 25或age = 35 - 再次查询时,结果集发生变化 → 幻读
👉 Next-Key Lock 通过扩大锁范围,保证 RR 隔离级别的语义
五、四种锁对比总结
| 锁类型 | 锁住内容 | 常见场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 表锁 | 整张表 | DDL / LOCK TABLE | 简单 | 并发极差 |
| 行锁(记录锁) | 单条记录 | 主键/唯一索引等值条件 | 并发高 | 依赖索引 |
| 间隙锁 | 记录之间 | 范围查询 + 锁定读 | 防幻读 | 阻塞插入 |
| 临键锁 | 记录 + 间隙 | RR 默认行为 | 一致性强 | 锁范围大 |
六、为什么间隙锁 / 临键锁在企业中是"性能杀手"?
以下 SQL 非常危险:
sql
SELECT * FROM register_queue
WHERE status = 0
ORDER BY id
LIMIT 1
FOR UPDATE;
在高并发下,InnoDB 会:
- 锁定命中的记录
- 锁定前后相关间隙
结果是:
👉 后续事务大量阻塞
👉 死锁概率急剧上升
七、如何在实际项目中避免锁冲突?
✅ 1. 避免范围查询 + FOR UPDATE
范围越大,锁住的数据和间隙越多。
✅ 2. 保证条件字段有合适的索引
防止锁范围扩大。
✅ 3. 使用精确条件代替范围条件
❌ 不推荐:
sql
SELECT * FROM queue WHERE time > '2024-01-01' FOR UPDATE;
✔ 推荐:
sql
SELECT * FROM queue WHERE id = ? FOR UPDATE;
✅ 4. 热点队列交给 Redis 处理
常见方案:
- Redis + Lua
- Redis Stream
- 分布式锁
👉 数据库负责持久化,Redis 负责并发抢占
✅ 5. 必要时从 RR 调整为 RC
- RC 下大多数场景不会使用间隙锁
- 锁冲突明显减少
- 但需要接受不可重复读
📌 这是很多互联网公司的真实选择
八、总结
| 锁类型 | 核心价值 | 最大风险 |
|---|---|---|
| 表锁 | 实现简单 | 并发差 |
| 行锁 | 高并发 | 强依赖索引 |
| 间隙锁 | 防幻读 | 阻塞插入 |
| 临键锁 | 一致性强 | 锁范围大 |