【MySQL】间隙锁 与 排他锁 的区别
文章目录
- [【MySQL】间隙锁 与 排他锁 的区别](#【MySQL】间隙锁 与 排他锁 的区别)
-
- [📊 核心区别对比表](#📊 核心区别对比表)
- [🔍 详细解释](#🔍 详细解释)
-
- [1. 排他锁(X锁)- 行级锁](#1. 排他锁(X锁)- 行级锁)
- [2. 间隙锁(Gap Lock)- 范围锁](#2. 间隙锁(Gap Lock)- 范围锁)
- [🎯 实际工作场景](#🎯 实际工作场景)
-
- [场景1:防止幻读(Phantom Read)](#场景1:防止幻读(Phantom Read))
- [场景2:组合使用 - 临键锁(Next-Key Lock)](#场景2:组合使用 - 临键锁(Next-Key Lock))
- [⚠️ 重要注意事项](#⚠️ 重要注意事项)
-
- [1. 间隙锁只作用于非唯一索引](#1. 间隙锁只作用于非唯一索引)
- [2. 查看锁信息](#2. 查看锁信息)
- [3. 死锁风险](#3. 死锁风险)
- [🔧 性能影响与优化](#🔧 性能影响与优化)
- [💡 最佳实践建议](#💡 最佳实践建议)
- [📝 总结要点](#📝 总结要点)
这是一个非常核心的数据库锁机制问题。简单来说:排他锁(X锁)是"锁什么",间隙锁(Gap Lock)是"锁哪里"。
📊 核心区别对比表
| 特性 | 排他锁 (X锁) | 间隙锁 (Gap Lock) |
|---|---|---|
| 锁的类型 | 锁类型 - 定义锁的"权限" | 锁范围 - 定义锁的"位置" |
| 作用对象 | 已存在的数据行 | 数据行之间的间隙 |
| 兼容性 | 与任何其他锁都不兼容 | 与其他间隙锁兼容,但与插入意向锁冲突 |
| 主要目的 | 防止其他事务读写数据 | 防止其他事务在范围内插入新数据 |
| 可见范围 | 行记录本身 | 索引记录之间的"空隙" |
| 何时使用 | UPDATE、DELETE、SELECT...FOR UPDATE | 可重复读隔离级别下的范围查询 |
🔍 详细解释
1. 排他锁(X锁)- 行级锁
sql
-- 示例:对uid=8的行加排他锁
SELECT * FROM student WHERE uid = 8 FOR UPDATE;
-- 或
UPDATE student SET age = 60 WHERE uid = 8;
特点:
- 锁定具体的数据行
- 其他事务不能读取(SELECT...FOR UPDATE)也不能修改(UPDATE/DELETE)该行
- 可以看作"写锁"
2. 间隙锁(Gap Lock)- 范围锁
sql
-- 示例:锁定uid在(5, 10)之间的所有间隙
SELECT * FROM student WHERE uid BETWEEN 6 AND 9 FOR UPDATE;
-- 或
SELECT * FROM student WHERE uid > 5 AND uid < 10 FOR UPDATE;
假设你的表数据:
sql
uid: 1, 2, 3, 4, 5, 7, 8, 10, 11
间隙包括:(-∞,1), (1,2), (2,3), ..., (5,7), (7,8), (8,10), (10,11), (11,+∞)
特点:
- 锁定索引记录之间的空隙,而不是记录本身
- 防止其他事务在范围内插入新数据
- 只在可重复读(REPEATABLE-READ) 隔离级别下有效
🎯 实际工作场景
场景1:防止幻读(Phantom Read)
sql
-- 事务一
BEGIN;
SELECT * FROM student WHERE age BETWEEN 20 AND 30 FOR UPDATE;
-- 锁定所有age在20-30之间的间隙
-- 事务二尝试插入age=25的新记录(会被阻塞)
INSERT INTO student (name, age, sex) VALUES ('new_student', 25, 'M');
场景2:组合使用 - 临键锁(Next-Key Lock)
sql
MySQL 实际上经常使用 **临键锁 = 记录锁 + 间隙锁**
-- 锁定uid=8的行及其前后的间隙
SELECT * FROM student WHERE uid = 8 FOR UPDATE;
-- 实际锁定:(7, 8] 和 (8, 10)
⚠️ 重要注意事项
1. 间隙锁只作用于非唯一索引
sql
-- 如果name有唯一索引,不会加间隙锁
SELECT * FROM student WHERE name = 'bbbb' FOR UPDATE;
-- 只对'bbbb'这一行加排他锁
-- 如果name没有索引或不是唯一索引,可能会锁全表间隙
SELECT * FROM student WHERE name LIKE 'b%' FOR UPDATE;
2. 查看锁信息
sql
-- 查看当前的锁(MySQL 8.0+)
SELECT
engine_transaction_id as trx_id,
lock_type,
lock_mode,
lock_data,
index_name
FROM performance_schema.data_locks
WHERE object_name = 'student';
3. 死锁风险
间隙锁容易导致死锁,特别是并发插入时:
sql
-- 事务A
BEGIN;
SELECT * FROM student WHERE uid = 15 FOR UPDATE; -- 间隙锁(10, +∞)
-- 事务B
BEGIN;
SELECT * FROM student WHERE uid = 16 FOR UPDATE; -- 间隙锁(10, +∞)
-- 事务A
INSERT INTO student (uid, name) VALUES (15, 'test'); -- 等待B
-- 事务B
INSERT INTO student (uid, name) VALUES (16, 'test'); -- 等待A → 死锁!
🔧 性能影响与优化
| 情况 | 影响 | 解决方案 |
|---|---|---|
| 全表间隙锁 | 锁住所有间隙,并发极差 | 使用唯一索引查询 |
| 范围查询 | 锁住大范围间隙 | 缩小查询范围 |
| 批量操作 | 可能锁大量间隙 | 分批次操作 |
| 高并发插入 | 容易死锁 | 降低隔离级别为RC |
💡 最佳实践建议
-
明确使用场景:
- 需要防止幻读 → 使用间隙锁(RR隔离级别)
- 只需要防止数据修改 → 使用行级排他锁
-
索引设计:
sql-- 好的设计:使用唯一索引避免不必要的间隙锁 CREATE UNIQUE INDEX idx_uid ON student(uid); -- 查询时使用唯一索引 SELECT * FROM student WHERE uid = 8 FOR UPDATE; -- 只锁一行 -
隔离级别选择:
sql-- 如果不需要防止幻读,使用读已提交 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 此时不会加间隙锁 -
避免长事务:
sql-- 尽快提交事务,减少锁持有时间 BEGIN; -- 立即执行需要加锁的操作 SELECT ... FOR UPDATE; UPDATE ...; -- 尽快提交 COMMIT;
📝 总结要点
- 排他锁 是"行锁" - 锁已存在的数据
- 间隙锁 是"范围锁" - 锁不存在的间隙
- MySQL 默认使用 临键锁(行锁+间隙锁)
- 间隙锁主要在 RR隔离级别 防止幻读
- 设计好索引可以大大减少不必要的间隙锁
在你的学生表例子中,如果 uid 是唯一索引,SELECT ... WHERE uid=8 FOR UPDATE 只会加行锁。如果 name 不是唯一索引,相同的查询可能会加间隙锁。