1. 什么是幻读?
定义:
幻读是指在事务过程中,一个事务执行了某些查询操作后,另一事务插入、删除或更新了满足查询条件的新数据,当第一个事务再次执行相同的查询操作时,结果集出现了“幻觉”,即读取到(或缺少)先前查询中不存在的数据。
幻读与脏读、不可重复读的区别:
- 脏读:事务读取了另一个事务未提交的数据,可能会读到错误结果。
- 不可重复读:事务在两次读取中,读到的数据被其他事务修改了,数据值不同。
- 幻读:事务在两次读取中,结果集的记录条数或范围变化了,比如多了新记录或少了某些记录。
举例说明:
假设有一个数据库表 employees
,内容如下:
id | name | salary |
---|---|---|
1 | Alice | 5000 |
2 | Bob | 4000 |
3 | Charlie | 3000 |
场景:
事务A执行如下查询:
SELECT * FROM employees WHERE salary > 3000;
结果为:
id | name | salary |
---|---|---|
1 | Alice | 5000 |
2 | Bob | 4000 |
这时,事务B插入了一条新记录:
INSERT INTO employees (id, name, salary) VALUES (4, 'David', 4500);
如果事务A再次执行相同的查询操作,结果会变为:
id | name | salary |
---|---|---|
1 | Alice | 5000 |
2 | Bob | 4000 |
4 | David | 4500 |
事务A“感知”到了新数据的插入,这种现象就是幻读。
2. MySQL 是如何解决幻读的?
MySQL 通过两种方式解决幻读问题:
- 针对快照读:使用 MVCC(多版本并发控制)
- 针对当前读:通过加锁机制(记录锁和间隙锁)
(1)针对快照读(Snapshot Read)
快照读的定义:
快照读是一种基于 MVCC 的非阻塞读取方式。执行普通 SELECT
语句时,InnoDB 存储引擎会基于当前事务启动时的数据库版本生成一个数据快照,事务读取的内容固定为快照中的数据,忽略其他事务的更新、插入或删除操作。
如何实现快照读:
- MVCC 原理:InnoDB 为每行记录维护两个隐藏的时间戳字段:
trx_id
(创建该记录的事务ID)和roll_pointer
(指向回滚日志的指针)。trx_id
用于标记这条记录由哪个事务创建。roll_pointer
用于回滚到旧版本数据,从而形成多个版本链。
- ReadView:
- 每个事务启动时会生成一个
ReadView
,其中包含当前正在活跃事务的 ID 列表。 - 事务读取数据时,会根据
trx_id
和ReadView
判断数据版本是否可见。
- 每个事务启动时会生成一个
快照读的优点:
- 高性能:读取操作不需要等待锁释放,可以并发执行。
- 一致性:避免了其他事务导致的数据不可重复读或幻读现象。
快照读的限制:
快照读只能用于非锁定查询,例如:
SELECT * FROM table_name WHERE ...;
(2)针对当前读(Current Read)
当前读的定义:
当前读是一种锁定读取方式,事务在读取数据的同时,会对涉及的数据范围加锁,防止其他事务对数据进行修改或插入。
当前读的典型语句:
以下语句属于当前读:
SELECT ... FOR UPDATE;
SELECT ... LOCK IN SHARE MODE;
INSERT
,UPDATE
,DELETE
加锁机制:记录锁和间隙锁(Next-Key Lock)
为了防止当前读场景下的幻读,MySQL 使用以下两种锁:
-
记录锁(Record Lock)
- 锁定单条记录。
- 防止其他事务修改或删除这条记录。
-
间隙锁(Gap Lock)
- 锁定一个记录范围之间的“间隙”,即不存在记录的区域。
- 防止其他事务向这个间隙中插入新记录。
-
Next-Key Lock(记录锁 + 间隙锁)
- 锁定一个范围,同时包含范围内的所有记录和间隙。
- Next-Key Lock 是记录锁和间隙锁的组合,用于防止当前读下的幻读。
3. 通过记录锁和间隙锁防止删除操作导致的幻读的示例
我们以一个具体场景为例,说明 MySQL 如何通过记录锁和间隙锁防止幻读。
场景描述
假设有一个名为 orders
的表,存储如下数据:
id | order_name | amount |
---|---|---|
1 | Order_A | 100 |
2 | Order_B | 200 |
4 | Order_D | 400 |
事务过程:
步骤 1:事务A执行当前读
事务A读取表中 amount > 100
的记录并对这些记录加锁:
SELECT * FROM orders WHERE amount > 100 FOR UPDATE;
事务A读取到:
id | order_name | amount |
---|---|---|
2 | Order_B | 200 |
4 | Order_D | 400 |
加锁分析:
- 记录锁:锁住符合条件的记录
id=2
和id=4
。 - 间隙锁:锁住
(100, 200)
和(200, 400)
之间的间隙,以及(400, +∞)
的间隙。
步骤 2:事务B尝试插入新记录
事务B尝试插入一条新记录:
INSERT INTO orders (id, order_name, amount) VALUES (3, 'Order_C', 300);
事务B会被阻塞,原因如下:
- 插入的
amount=300
落在(200, 400)
的间隙范围,而事务A持有该范围的间隙锁,禁止其他事务在此区间插入记录。
步骤 3:事务B尝试删除或修改记录
事务B尝试删除或修改已存在的记录:
DELETE FROM orders WHERE id = 2;
事务B再次被阻塞,原因如下:
id=2
的记录已被事务A加了记录锁,其他事务无法删除或修改这条记录。
步骤 4:事务A提交后,事务B执行成功
当事务A完成操作并提交后,锁释放:
- 事务B的插入操作
id=3, amount=300
可以成功执行。 - 事务B的删除操作
id=2
也可以正常进行。
总结加锁机制防幻读的过程:
- 记录锁:防止其他事务修改或删除当前事务查询到的记录。
- 间隙锁:防止其他事务在查询范围内插入新记录。
- Next-Key Lock(记录锁 + 间隙锁):通过同时锁定记录和范围,有效防止幻读。
4. 总结
幻读的定义
幻读是指在事务中,某次查询范围内新增了数据,导致后续查询结果不一致。例如,第一次查询到的结果集中没有一条记录,但后续由于其他事务的插入操作,查询结果发生变化。
MySQL 解决幻读的两种方式:
-
快照读(Snapshot Read):使用 MVCC 技术
- 基于多版本并发控制 (MVCC),通过维护数据的多个版本及事务的
ReadView
,实现一致性读取。 - 快照读适用于非阻塞读取操作,如普通的
SELECT
查询。
- 基于多版本并发控制 (MVCC),通过维护数据的多个版本及事务的
-
当前读(Current Read):使用加锁机制
- 对读取范围内的记录和间隙加锁,通过记录锁(Record Lock)和间隙锁(Gap Lock)防止插入或修改,避免幻读。
- 当前读适用于需要锁定数据的操作,例如
SELECT ... FOR UPDATE
、UPDATE
和DELETE
。
加锁机制:记录锁和间隙锁
- 记录锁:锁定查询范围内的记录,防止其他事务修改或删除这些记录。
- 间隙锁:锁定查询范围内的“间隙”,防止其他事务向这些间隙插入新记录。
- Next-Key Lock:记录锁和间隙锁的结合,既锁定记录,又锁定范围,是防止幻读的关键机制。
示例回顾:通过记录锁和间隙锁防止删除操作导致的幻读
在示例中,我们通过以下步骤,详细分析了 MySQL 如何防止幻读:
- 事务A对查询范围内的记录和间隙加锁(Next-Key Lock)。
- 事务B试图插入新记录或修改已锁定的记录时被阻塞。
- 事务A提交后,锁释放,事务B的操作得以继续。
通过 Next-Key Lock 机制,MySQL 确保了事务A的查询范围内的数据不被其他事务破坏,从而避免了幻读现象。
总结价值与启发
- 事务隔离级别和加锁机制密切相关:了解幻读的解决方式有助于更好地设计事务逻辑,避免数据一致性问题。
- 性能与一致性的权衡:MVCC 提高了读取性能,但需要额外的存储和计算;锁机制确保了当前读的一致性,但可能引发锁争用问题。
- 应用场景选择:根据具体需求选择快照读或当前读策略。例如,在频繁更新的场景中,通过加锁防止幻读更为可靠。