MySQL快照读与当前读
一、快照读
1. 定义
快照读读取的是数据在某个时间点的快照(通常是一个历史版本),这个快照是基于undo log和ReadView机制实现的。它不会去读取其他未提交事务修改的数据,也不会因此去加锁(除非是在SERIALIZABLE隔离级别下)。
2. 实现原理:MVCC
快照读是实现MySQL MVCC(多版本并发控制) 模型的关键。InnoDB会为每一行数据维护多个版本。当一个事务启动时,它会生成一个当前系统的“快照”(在代码中体现为一个ReadView
结构),这个ReadView
决定了该事务能看到哪些版本的数据:
- 已提交的事务所做的修改:可见
- 未提交的事务或在本事务开始之后才提交的事务所做的修改:不可见
3. 触发语句
- 普通的
SELECT
语句(不加锁的情况下)。1
SELECT * FROM table WHERE ...;
4. 特点
- 不加锁:因此不会阻塞其他事务的写操作(
UPDATE
,DELETE
,INSERT
),并发性能高。 - 读到的可能是历史数据:在可重复读(REPEATABLE-READ) 隔离级别下,一个事务内多次快照读都会读取到同一个时间点的数据,从而实现可重复读。在读已提交(READ-COMMITTED) 隔离级别下,每次快照读都会生成新的
ReadView
,所以每次都能读到最新已提交的数据。
二、当前读
1. 定义
当前读读取的是数据的最新版本,并且会保证在读取过程中,其他事务无法修改这个数据,直到当前读事务结束。这是通过加锁来实现的。
2. 实现原理:锁
当前读通过给记录加锁(如行锁、间隙锁)来确保数据的一致性。它读取的是已提交的最新数据,并且在读取时防止其他事务并发修改,从而避免了脏读和不可重复读。
3. 触发语句
所有会导致加锁的读取操作都是当前读:
SELECT ... FOR UPDATE
:加排他锁(X锁)。SELECT ... LOCK IN SHARE MODE
:加共享锁(S锁)。(在MySQL 8.0中,更推荐使用SELECT ... FOR SHARE
)UPDATE ...
:更新数据前,会先进行一次当前读,读取最新数据然后加锁。DELETE ...
:同上。INSERT ...
:插入时也会涉及特殊的锁(如插入意向锁)。
4. 特点
- 加锁:会对读取的记录加锁,可能会阻塞其他试图修改这些记录的事务。
- 读取最新已提交数据:总是能读到所有已提交事务的最新修改结果。
- 解决幻读:在可重复读(REPEATABLE-READ) 隔离级别下,
SELECT ... FOR UPDATE
这样的当前读语句会通过间隙锁(Next-Key Locking) 来锁住一个范围,防止其他事务在这个范围内插入新数据,从而解决了幻读问题。
三、核心区别对比表
特性 | 快照读 | 当前读 |
---|---|---|
核心概念 | 读取数据的历史快照 | 读取数据的最新版本 |
实现机制 | MVCC(多版本并发控制) | 锁(行锁、间隙锁等) |
是否加锁 | 否(无锁读,并发性好) | 是(加共享锁或排他锁) |
数据来源 | undo log中的历史版本 | 数据页中的最新记录 |
触发语句 | 普通 SELECT 语句 |
SELECT ... FOR UPDATE , SELECT ... LOCK IN SHARE MODE , UPDATE , DELETE , INSERT |
隔离级别影响 | RR级别下,同一事务内多次读取结果一致;RC级别下,每次读取最新快照 | 在所有隔离级别下行为基本一致,都读取最新数据并加锁 |
四、一个简单的例子来帮助理解
假设有一张表 account
,有一条数据:id = 1, balance = 1000
。
时间点 T1:事务A开启,执行一个快照读:
1
2
3-- 事务A
START TRANSACTION;
SELECT balance FROM account WHERE id = 1; -- 快照读,读到 1000时间点 T2:事务B开启,并修改了这条数据,将余额减去100,然后提交。
1
2
3
4-- 事务B
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1; -- 当前读,加锁,读到最新值1000,更新为900
COMMIT; -- 提交时间点 T3:事务A再次执行快照读:
1
2-- 事务A(仍在之前的事务中)
SELECT balance FROM account WHERE id = 1; -- 快照读,仍然读到 1000(可重复读)时间点 T4:事务A执行一个当前读:
1
2-- 事务A
SELECT balance FROM account WHERE id = 1 FOR UPDATE; -- 当前读,加锁,读到最新已提交的数据 900
总结一下这个例子:
- 事务A内的两次快照读结果相同(都是1000),体现了“可重复读”。
- 事务B的更新操作是一个当前读,它读取了最新的值(1000)并进行修改。
- 事务A最后的当前读(
FOR UPDATE
)绕开了MVCC的快照,直接读取了最新的已提交数据(900)。
结论
- 当你需要高并发查询,且对实时性要求不高(允许读到历史数据)时,使用快照读(普通
SELECT
)。 - 当你需要确保读取的数据是最新的,并且在你处理的过程中不允许别人修改(例如电商库存扣减、余额检查)时,使用当前读(
SELECT ... FOR UPDATE
)。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 技术之路!
评论