Carry の Blog Carry の Blog
首页
  • Nginx
  • Prometheus
  • Iptables
  • Systemd
  • Firewalld
  • Docker
  • Sshd
  • DBA工作笔记
  • MySQL
  • Redis
  • TiDB
  • Elasticsearch
  • Python
  • Shell
  • MySQL8-SOP手册
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Carry の Blog

好记性不如烂键盘
首页
  • Nginx
  • Prometheus
  • Iptables
  • Systemd
  • Firewalld
  • Docker
  • Sshd
  • DBA工作笔记
  • MySQL
  • Redis
  • TiDB
  • Elasticsearch
  • Python
  • Shell
  • MySQL8-SOP手册
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • MySQL8-SOP

  • MySQL实战45讲学习笔记

    • MySQL45讲学习笔记
    • MySQL基础架构
    • MySQL日志系统 (Redo Log 与 Binlog)
    • MySQL字符串字段索引优化
    • MySQL索引原理与优化
    • MySQL锁机制详解
    • MySQL事务与MVCC机制
    • MySQL普通索引与唯一索引的选择
    • MySQL优化器如何选择索引
    • MySQL抖动刷脏页
    • 表空间管理与回收
    • count函数详解
    • 日志索引
    • orderby工作原理
    • 随机排序实现
    • SQL性能差异函数与转换
    • 慢查询分析锁与版本
    • 幻读与间隙锁
      • 1. 幻读概念解析
        • 1.1 什么是幻读
        • 1.2 幻读与不可重复读的区别
      • 2. 幻读产生的原因
        • 2.1 读取一致性问题
        • 2.2 索引范围扫描
      • 3. 间隙锁机制详解
        • 3.1 间隙锁的概念
        • 3.2 间隙锁的类型
        • 3.3 间隙锁的加锁规则
      • 4. 幻读与间隙锁的关系
        • 4.1 间隙锁解决幻读
        • 4.2 间隙锁的锁定范围
      • 5. 实际应用场景
        • 5.1 防止幻读的正确做法
        • 5.2 间隙锁对并发的影响
      • 6. 常见问题与解决方案
        • 6.1 间隙锁导致的死锁
        • 6.2 避免间隙锁影响的策略
      • 7. 间隙锁监控与诊断
        • 7.1 查看锁等待情况
        • 7.2 诊断锁冲突
      • 8. 隔离级别对幻读的影响
        • 8.1 可重复读(REPEATABLE READ)
        • 8.2 读已提交(READ COMMITTED)
      • 9. 最佳实践建议
        • 9.1 合理使用事务隔离级别
        • 9.2 优化锁的使用
        • 9.3 监控锁竞争
      • 10. 总结
    • 加锁规则分析
    • 应急性能优化方法
    • 数据持久化保证
    • 主备一致性原理
    • 高可用架构与切换
    • 备库延迟分析与优化
    • 主备切换GTID
    • 读写分离实践与问题
    • 数据库健康检查
    • 锁与死锁
    • 数据误删恢复
    • Kill命令详解
    • 查询与内存使用分析
    • Join原理与选择
    • 临时表原理与应用
    • 内部临时表详解
    • Memory引擎详解
    • 自增ID详解
    • Insert加锁分析
    • 表复制方法比较
    • Grant与权限管理
    • 分区表详解
    • SQL语句中的Join问题
    • 自增ID用尽问题
  • 专题系列
  • MySQL实战45讲学习笔记
Carry の Blog
2024-07-27
目录

幻读与间隙锁

# 幻读与间隙锁

在数据库事务隔离中,幻读是一个重要而又容易混淆的概念。同时,间隙锁作为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

# 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. 幻读产生的原因

# 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

# 3. 间隙锁机制详解

# 3.1 间隙锁的概念

间隙锁(Gap Lock)是InnoDB存储引擎中的一种锁机制,它锁定索引记录之间的间隙,而不是记录本身。

-- 查看锁信息
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
1
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

# 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

# 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

# 3.3.2 原则2:查找过程中访问到的对象才会加锁

-- 只有访问到的记录才会加锁
SELECT * FROM test_table WHERE value > 100 FOR UPDATE;
-- 只锁定实际访问到的记录和间隙
1
2
3

# 3.3.3 优化1:等值查询,唯一索引加锁退化为行锁

-- 唯一索引等值查询
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 退化为行锁,不加间隙锁
1
2
3

# 3.3.4 优化2:等值查询向右遍历时的间隙锁退化

-- 等值查询向右遍历但不满足条件时退化为间隙锁
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 如果id=10不存在,会加间隙锁 (5,10]
1
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

# 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

# 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

# 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

# 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

# 6.2 避免间隙锁影响的策略

# 6.2.1 使用唯一索引

-- 使用唯一索引避免间隙锁
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 只加行锁,不加间隙锁
1
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

# 6.2.3 合理使用LIMIT

-- 使用LIMIT限制结果集
SELECT * FROM users WHERE age > 25 LIMIT 10 FOR UPDATE;
-- 减少锁定的记录数
1
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

# 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

# 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

# 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

# 9. 最佳实践建议

# 9.1 合理使用事务隔离级别

-- 根据业务需求选择合适的隔离级别
-- 高一致性要求:REPEATABLE READ
-- 高并发要求:READ COMMITTED
1
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

# 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

# 10. 总结

幻读和间隙锁是MySQL事务处理中的重要概念。间隙锁通过锁定索引记录之间的间隙来防止幻读,确保在可重复读隔离级别下的数据一致性。理解间隙锁的工作原理和使用场景,对于开发高性能、高可靠性的数据库应用至关重要。

在实际应用中,应该根据业务需求合理选择事务隔离级别,优化查询语句,避免不必要的锁竞争,同时建立完善的监控体系来及时发现和解决锁相关问题。通过这些措施,可以有效避免幻读问题,提高系统的并发性能和数据一致性。

#MySQL#幻读#间隙锁#锁机制#学习笔记
上次更新: 3/4/2026

← 慢查询分析锁与版本 加锁规则分析→

最近更新
01
表空间管理与回收
03-04
02
MySQL抖动刷脏页
03-04
03
count函数详解
03-04
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式