行锁、间隙锁、临键锁
🔴 困难题目描述
解释 InnoDB 的行锁、间隙锁和临键锁的区别。以下 SQL 会加什么锁?
-- 表结构
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
KEY idx_age (age)
);
-- 数据
INSERT INTO users VALUES
(1, 'Alice', 18),
(5, 'Bob', 20),
(10, 'Charlie', 25);
-- 查询 1
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- 查询 2
SELECT * FROM users WHERE age = 20 FOR UPDATE;
-- 查询 3
SELECT * FROM users WHERE age > 18 AND age < 25 FOR UPDATE;提示
- 行锁:锁定单行记录
- 间隙锁:锁定记录之间的间隙
- 临键锁:行锁 + 间隙锁
解法
参考答案 (3 个标签)
行锁 间隙锁 临键锁
1. 行锁(Record Lock)
-- 锁定单行记录
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- 加锁:
-- Record Lock on id=5
-- 影响:
-- ✓ 可以插入 id=1, id=10
-- ✗ 不能修改 id=5
-- ✗ 其他事务不能锁定 id=5特点:
- 锁定索引记录
- 只锁一行,并发度高
- 主键或唯一索引等值查询
2. 间隙锁(Gap Lock)
-- 锁定记录之间的间隙
SELECT * FROM users WHERE age = 20 FOR UPDATE;
-- 数据:
-- (1, 'Alice', 18)
-- (5, 'Bob', 20)
-- (10, 'Charlie', 25)
-- 加锁:
-- Gap Lock: (18, 20)
-- Gap Lock: (20, 25)
-- 影响:
-- ✓ 可以修改 age=20 的记录
-- ✗ 不能插入 age=19, 21, 22, 23, 24
-- ✓ 可以插入 age=18, 25特点:
- 锁定索引记录之间的间隙
- 不锁定记录本身
- 防止幻读(防止插入新记录)
3. 临键锁(Next-Key Lock)
-- 行锁 + 间隙锁
SELECT * FROM users WHERE age > 18 AND age < 25 FOR UPDATE;
-- 数据:
-- (1, 'Alice', 18)
-- (5, 'Bob', 20)
-- (10, 'Charlie', 25)
-- 加锁:
-- Next-Key Lock: (18, 20] -- 间隙 + 记录
-- Next-Key Lock: (20, 25] -- 间隙 + 记录
-- 影响:
-- ✗ 不能插入 age=19, 20, 21, 22, 23, 24
-- ✗ 不能修改 age=20
-- ✓ 可以插入 age=18, 25特点:
- 默认锁类型(REPEATABLE READ)
- 行锁 + 间隙锁
- 防止幻读
各查询分析
查询 1:主键等值查询
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- 加锁:
-- Record Lock on id=5
-- 原因:主键唯一,只需锁定该记录查询 2:辅助索引等值查询
SELECT * FROM users WHERE age = 20 FOR UPDATE;
-- 加锁:
-- Next-Key Lock: (18, 20]
-- Gap Lock: (20, 25)
-- 原因:
-- 1. age=20 存在,加 Next-Key Lock (18, 20]
-- 2. 优化后,右边的间隙降级为 Gap Lock (20, 25)
-- 影响:
-- ✗ 不能插入 age=19, 21, 22, 23, 24
-- ✗ 不能修改 age=20 的记录查询 3:辅助索引范围查询
SELECT * FROM users WHERE age > 18 AND age < 25 FOR UPDATE;
-- 加锁:
-- Next-Key Lock: (18, 20]
-- Next-Key Lock: (20, 25]
-- 原因:范围查询,使用 Next-Key Lock
-- 影响:
-- ✗ 不能插入 age=19, 20, 21, 22, 23, 24
-- ✗ 不能修改 age=20 的记录扩展:锁的兼容性
锁兼容矩阵
| 锁类型 | Record Lock | Gap Lock | Next-Key Lock |
|---|---|---|---|
| Record Lock | X↔X 冲突 | Gap↔Gap 兼容 | X↔X 冲突 |
| Gap Lock | Gap↔Gap 兼容 | Gap↔Gap 兼容 | Gap↔Gap 兼容 |
| Next-Key Lock | X↔X 冲突 | Gap↔Gap 兼容 | X↔X 冲突 |
注意:
- Gap Lock 只与其他 Gap Lock 冲突
- Gap Lock 不与 Record Lock 冲突
- Gap Lock 的目的是防止插入,不是防止读取
隔离级别的影响
| 隔离级别 | Gap Lock | Next-Key Lock |
|---|---|---|
| READ COMMITTED | ✗ | ✗ |
| REPEATABLE READ | ✓ | ✓ |
-- READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT * FROM users WHERE age > 18 AND age < 25 FOR UPDATE;
-- 只加 Record Lock,不加 Gap Lock
-- 可能出现幻读
-- REPEATABLE READ
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM users WHERE age > 18 AND age < 25 FOR UPDATE;
-- 加 Next-Key Lock
-- 防止幻读死锁案例
案例 1:不同顺序加锁
-- 事务 A
START TRANSACTION;
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 锁 id=1
SELECT * FROM users WHERE id = 5 FOR UPDATE; -- 等待 id=5
-- 事务 B
START TRANSACTION;
SELECT * FROM users WHERE id = 5 FOR UPDATE; -- 锁 id=5
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 等待 id=1
-- 死锁!案例 2:间隙锁冲突
-- 事务 A
START TRANSACTION;
SELECT * FROM users WHERE age > 18 AND age < 25 FOR UPDATE;
-- Gap Lock: (18, 25]
-- 事务 B
START TRANSACTION;
INSERT INTO users (id, name, age) VALUES (2, 'David', 20);
-- 等待 Gap Lock
-- 事务 A
INSERT INTO users (id, name, age) VALUES (3, 'Eve', 19);
-- 等待自己的 Gap Lock
-- 死锁!如何避免死锁
1. 固定加锁顺序
-- ❌ 不同顺序
-- A: 1 → 5
-- B: 5 → 1
-- ✓ 固定顺序
-- A: 1 → 5
-- B: 1 → 52. 使用较低隔离级别
-- READ COMMITTED 减少 Gap Lock
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;3. 减少锁持有时间
-- ❌ 长事务
START TRANSACTION;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 执行业务逻辑(耗时)
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- ✓ 短事务
UPDATE users SET balance = balance - 100 WHERE id = 1;
-- 立即提交4. 添加合理索引
-- ✗ 索引失效,升级为表锁
SELECT * FROM users WHERE YEAR(created_at) = 2024 FOR UPDATE;
-- ✓ 使用索引
SELECT * FROM users WHERE created_at >= '2024-01-01' FOR UPDATE;