幻读与间隙锁
# 幻读与间隙锁
在数据库事务隔离中,幻读是一个重要而又容易混淆的概念。同时,间隙锁作为MySQL中一种特殊的锁机制,在解决幻读问题中发挥着关键作用。本文将深入探讨幻读的本质和间隙锁的工作原理。
# 1. 幻读概念解析
# 1.1 什么是幻读
幻读(Phantom Read)是指在同一个事务中,多次执行相同的查询语句,但由于其他事务的插入或删除操作,导致查询结果集发生变化的现象。
-- 事务A开始
START TRANSACTION;
-- 第一次查询
SELECT * FROM users WHERE age > 25;
-- 假设返回3条记录
-- 事务B在此期间插入新记录
INSERT INTO users (name, age) VALUES ('Alice', 30);
-- 事务A再次查询
SELECT * FROM users WHERE age > 25;
-- 现在返回4条记录,出现了"幻影"般的记录
COMMIT;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.2 幻读与不可重复读的区别
-- 不可重复读示例
START TRANSACTION;
-- 第一次查询
SELECT age FROM users WHERE id = 1;
-- 返回 age = 25
-- 事务B修改记录
UPDATE users SET age = 30 WHERE id = 1;
-- 第二次查询
SELECT age FROM users WHERE id = 1;
-- 返回 age = 30,与第一次不同
-- 幻读示例
START TRANSACTION;
-- 第一次查询
SELECT * FROM users WHERE age > 25;
-- 返回3条记录
-- 事务B插入新记录
INSERT INTO users (name, age) VALUES ('Bob', 30);
-- 第二次查询
SELECT * FROM users WHERE age > 25;
-- 返回4条记录,多了Bob这条记录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 2. 幻读产生的原因
# 2.1 读取一致性问题
在可重复读(REPEATABLE READ)隔离级别下,事务开始时会创建一个一致性视图(Consistent Read View),这个视图在整个事务期间保持不变。但是,如果其他事务插入了新的满足查询条件的记录,这些新记录在当前事务的视图中是不可见的,这就会导致幻读。
# 2.2 索引范围扫描
-- 创建测试表
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(50),
price DECIMAL(10,2),
category_id INT,
INDEX idx_category_price (category_id, price)
);
-- 插入测试数据
INSERT INTO products VALUES
(1, 'Product A', 100.00, 1),
(2, 'Product B', 200.00, 1),
(3, 'Product C', 300.00, 1),
(4, 'Product D', 150.00, 2),
(5, 'Product E', 250.00, 2);
-- 事务A执行范围查询
START TRANSACTION;
SELECT * FROM products WHERE category_id = 1 AND price > 150;
-- 返回 Product B, Product C
-- 事务B插入新记录
INSERT INTO products VALUES (6, 'Product F', 180.00, 1);
-- 事务A再次查询
SELECT * FROM products WHERE category_id = 1 AND price > 150;
-- 返回 Product B, Product C, Product F(幻读)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 3. 间隙锁机制详解
# 3.1 间隙锁的概念
间隙锁(Gap Lock)是InnoDB存储引擎中的一种锁机制,它锁定索引记录之间的间隙,而不是记录本身。
-- 查看锁信息
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
1
2
3
2
3
# 3.2 间隙锁的类型
# 3.2.1 前开后闭区间
-- 在索引上使用范围查询时会加间隙锁
SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;
-- 会锁定 (5,10], (10,15], (15,20], (20,25] 等间隙
1
2
3
2
3
# 3.2.2 Next-Key Lock
Next-Key Lock = 记录锁 + 间隙锁,是InnoDB中默认的锁类型:
-- 示例:Next-Key Lock
SELECT * FROM users WHERE id = 15 FOR UPDATE;
-- 实际上锁定了 (10,15] 这个间隙和记录15本身
1
2
3
2
3
# 3.3 间隙锁的加锁规则
# 3.3.1 原则1:加锁的基本单位是Next-Key Lock
-- 示例:在索引上加锁
CREATE TABLE test_table (
id INT PRIMARY KEY,
value INT,
INDEX idx_value (value)
);
-- 查询时加锁
SELECT * FROM test_table WHERE value = 100 FOR UPDATE;
-- 锁定 (95,100] 间隙和记录100
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 3.3.2 原则2:查找过程中访问到的对象才会加锁
-- 只有访问到的记录才会加锁
SELECT * FROM test_table WHERE value > 100 FOR UPDATE;
-- 只锁定实际访问到的记录和间隙
1
2
3
2
3
# 3.3.3 优化1:等值查询,唯一索引加锁退化为行锁
-- 唯一索引等值查询
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 退化为行锁,不加间隙锁
1
2
3
2
3
# 3.3.4 优化2:等值查询向右遍历时的间隙锁退化
-- 等值查询向右遍历但不满足条件时退化为间隙锁
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 如果id=10不存在,会加间隙锁 (5,10]
1
2
3
2
3
# 4. 幻读与间隙锁的关系
# 4.1 间隙锁解决幻读
间隙锁的引入正是为了解决幻读问题:
-- 事务A
START TRANSACTION;
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;
-- 假设返回记录1,2,3
-- 实际加锁:(15,20], (20,25], (25,30], (30,35] 间隙锁
-- 事务B尝试插入数据
INSERT INTO users (name, age) VALUES ('New User', 25);
-- 会被阻塞,因为25在 (20,30] 间隙范围内
COMMIT;
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 4.2 间隙锁的锁定范围
-- 创建测试表
CREATE TABLE orders (
id INT PRIMARY KEY,
order_date DATE,
amount DECIMAL(10,2),
INDEX idx_date_amount (order_date, amount)
);
-- 插入测试数据
INSERT INTO orders VALUES
(1, '2024-01-01', 100.00),
(2, '2024-01-02', 200.00),
(3, '2024-01-03', 300.00),
(4, '2024-01-04', 400.00);
-- 事务A加锁
SELECT * FROM orders WHERE order_date = '2024-01-02' AND amount > 150 FOR UPDATE;
-- 会加锁 (2024-01-02, 150] 和 (2024-01-02, 300] 间隙
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 5. 实际应用场景
# 5.1 防止幻读的正确做法
-- 正确的幻读防护方式
START TRANSACTION;
-- 使用悲观锁防止幻读
SELECT * FROM accounts WHERE balance < 1000 FOR UPDATE;
-- 执行业务逻辑
UPDATE accounts SET balance = balance - 100 WHERE balance < 1000;
-- 提交事务
COMMIT;
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 5.2 间隙锁对并发的影响
-- 事务A
START TRANSACTION;
SELECT * FROM products WHERE price BETWEEN 100 AND 200 FOR UPDATE;
-- 加锁范围:(50,100], (100,150], (150,200], (200,250]
-- 事务B尝试插入
INSERT INTO products (name, price) VALUES ('New Product', 150);
-- 被阻塞,因为150在锁定范围内
-- 事务C尝试插入
INSERT INTO products (name, price) VALUES ('Another Product', 250);
-- 不会被阻塞,因为250不在锁定范围内
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 6. 常见问题与解决方案
# 6.1 间隙锁导致的死锁
-- 事务A
START TRANSACTION;
SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;
-- 加锁 (5,10], (10,15], (15,20], (20,25]
-- 事务B
START TRANSACTION;
SELECT * FROM users WHERE id BETWEEN 15 AND 25 FOR UPDATE;
-- 加锁 (10,15], (15,20], (20,25], (25,30]
-- 可能导致死锁
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 6.2 避免间隙锁影响的策略
# 6.2.1 使用唯一索引
-- 使用唯一索引避免间隙锁
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 只加行锁,不加间隙锁
1
2
3
2
3
# 6.2.2 优化查询条件
-- 避免不必要的范围查询
-- 不好的做法
SELECT * FROM users WHERE age > 25 FOR UPDATE;
-- 更好的做法
SELECT * FROM users WHERE age BETWEEN 25 AND 35 FOR UPDATE;
1
2
3
4
5
6
2
3
4
5
6
# 6.2.3 合理使用LIMIT
-- 使用LIMIT限制结果集
SELECT * FROM users WHERE age > 25 LIMIT 10 FOR UPDATE;
-- 减少锁定的记录数
1
2
3
2
3
# 7. 间隙锁监控与诊断
# 7.1 查看锁等待情况
-- 查看当前锁等待
SELECT * FROM performance_schema.data_lock_waits;
-- 查看锁信息
SELECT * FROM performance_schema.data_locks;
-- 查看事务信息
SELECT * FROM information_schema.innodb_trx;
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 7.2 诊断锁冲突
-- 查看死锁信息
SHOW ENGINE INNODB STATUS\G
-- 查看当前连接的锁情况
SELECT
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 8. 隔离级别对幻读的影响
# 8.1 可重复读(REPEATABLE READ)
-- 在可重复读级别下,幻读会被间隙锁阻止
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM users WHERE age > 25;
-- 事务开始时创建一致性视图
-- 其他事务插入新记录
INSERT INTO users (name, age) VALUES ('New User', 30);
-- 再次查询
SELECT * FROM users WHERE age > 25;
-- 仍然返回相同结果,因为间隙锁阻止了新记录的插入
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 8.2 读已提交(READ COMMITTED)
-- 在读已提交级别下,幻读可能发生
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM users WHERE age > 25;
-- 返回3条记录
-- 其他事务插入新记录
INSERT INTO users (name, age) VALUES ('New User', 30);
-- 再次查询
SELECT * FROM users WHERE age > 25;
-- 返回4条记录,出现幻读
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 9. 最佳实践建议
# 9.1 合理使用事务隔离级别
-- 根据业务需求选择合适的隔离级别
-- 高一致性要求:REPEATABLE READ
-- 高并发要求:READ COMMITTED
1
2
3
2
3
# 9.2 优化锁的使用
-- 1. 尽量使用精确的查询条件
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 2. 合理使用LIMIT
SELECT * FROM users WHERE age > 25 LIMIT 10 FOR UPDATE;
-- 3. 事务尽可能短
START TRANSACTION;
-- 执行必要的操作
COMMIT;
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 9.3 监控锁竞争
-- 监控锁等待情况
CREATE EVENT lock_monitor
ON SCHEDULE EVERY 1 MINUTE
DO
INSERT INTO lock_log (timestamp, waiting_count, blocking_count)
VALUES (NOW(),
(SELECT COUNT(*) FROM performance_schema.data_lock_waits),
(SELECT COUNT(*) FROM information_schema.innodb_trx WHERE trx_state = 'LOCK WAIT'));
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 10. 总结
幻读和间隙锁是MySQL事务处理中的重要概念。间隙锁通过锁定索引记录之间的间隙来防止幻读,确保在可重复读隔离级别下的数据一致性。理解间隙锁的工作原理和使用场景,对于开发高性能、高可靠性的数据库应用至关重要。
在实际应用中,应该根据业务需求合理选择事务隔离级别,优化查询语句,避免不必要的锁竞争,同时建立完善的监控体系来及时发现和解决锁相关问题。通过这些措施,可以有效避免幻读问题,提高系统的并发性能和数据一致性。
上次更新: 3/4/2026