当前读和快照读
概述
特性 | 快照读 (Snapshot Read) | 当前读 (Current Read) |
---|---|---|
核心原理 | 基于 MVCC 和多版本数据 | 基于 锁 机制 |
读取内容 | 历史版本数据(某个时间点的快照) | 数据的最新已提交版本 |
是否加锁 | 不加锁(非阻塞) | 加锁(S锁或X锁) |
实现方式 | 普通的 SELECT 语句 |
SELECT ... FOR UPDATE , SELECT ... LOCK IN SHARE MODE , UPDATE , DELETE , INSERT |
隔离级别 | 在 RC 和 RR 级别下生效 | 所有隔离级别都适用 |
一致性 | 一致性非锁定读 | 锁定读 |
一、快照读 (Snapshot Read)
1. 定义
快照读是指 InnoDB 使用 多版本控制 (MVCC) 机制,为查询呈现一个基于某个时间点的数据库快照的读取方式。它读取的不是数据的最新状态,而是记录的一个历史版本。
2. 如何工作
- 当一个事务执行快照读(如
SELECT * FROM table;
)时,InnoDB 会为该事务生成一个 Read View(读视图)。 - 通过这个 Read View,结合数据行中的 DB_TRX_ID(事务ID)和 DB_ROLL_PTR(回滚指针)在 Undo Log 中形成的版本链,InnoDB 可以找到对这个事务可见的那个版本的数据。
- 正如上一个问题中举例的那样,它可能会跳过由未提交事务或在本事务开始之后才提交的事务所做的修改。
3. 特点
- 不加锁:这是快照读最大的优点,它使得读操作不会阻塞写操作,写操作也不会阻塞读操作,极大地提高了数据库的并发性能。
- 非阻塞:读取操作无需等待任何锁的释放。
- 依赖隔离级别:
- REPEATABLE READ (可重复读):在第一次执行快照读时生成 Read View,后续所有快照读都复用这个 Read View。因此,在整个事务过程中,每次读到的都是同一个一致性快照,实现了可重复读。
- READ COMMITTED (读已提交):在每次执行快照读时都会生成一个新的 Read View。因此,每次读都能看到最新已提交的数据,但不可重复读。
4. 示例
-- 假设隔离级别是 REPEATABLE READ
START TRANSACTION; -- 事务开始
-- T1时刻:第一次快照读,生成ReadView
SELECT * FROM users WHERE id = 1; -- 返回 balance = 1000
-- 在此期间,另一个事务提交了:UPDATE users SET balance = 900 WHERE id = 1;
-- T2时刻:第二次快照读,复用T1时刻的ReadView
SELECT * FROM users WHERE id = 1; -- 仍然返回 balance = 1000 (可重复读)
COMMIT;
在这个例子中,尽管数据已经被其他事务修改,但第二个 SELECT
仍然返回和第一个相同的结果,因为它读取的是快照,而不是最新数据。
二、当前读 (Current Read)
1. 定义
当前读是指读取数据的最新已提交版本,并且为了保证在读取过程中数据不被其他事务修改,会对读取的记录加锁。
2. 如何工作
- 当前读通过给记录加锁来实现。
SELECT ... LOCK IN SHARE MODE
会加一个 S锁(共享锁)。SELECT ... FOR UPDATE
、UPDATE
、DELETE
、INSERT
会加 X锁(排他锁)。
- 加锁后,其他事务如果想修改这条记录(需要X锁),或者也想用
SELECT ... FOR UPDATE
来读取它,就会被阻塞,直到当前事务释放锁。
3. 特点
- 加锁:这是当前读的核心,通过锁来保证数据的一致性。
- 读取最新数据:它总是读取记录已提交的最新版本,而不是历史快照。
- 会阻塞/被阻塞:因为涉及锁的竞争,当前读操作可能会被其他事务的锁阻塞,也可能会阻塞其他事务。
- 解决并发问题:当前读是解决“丢失更新”等问题的关键手段。例如,在并发转账场景中,必须先通过
SELECT ... FOR UPDATE
当前读获取最新的余额并加锁,然后再进行更新,否则可能发生覆盖。
4. 示例:经典的并发扣款场景
START TRANSACTION;
-- 使用当前读,获取id=1的用户的最新余额,并为其加上X锁(排他锁)
-- 这会阻止其他事务也使用当前读或修改这条记录
SELECT balance FROM users WHERE id = 1 FOR UPDATE;
-- 应用程序中计算:new_balance = current_balance - 100
-- 基于当前读到的、最新的、且已被锁定的数据进行更新
UPDATE users SET balance = new_balance WHERE id = 1;
COMMIT; -- 提交事务,释放锁
如果不使用 FOR UPDATE
,两个事务可能同时快照读到相同的余额(例如1000),然后各自计算并更新(900和900),最终结果(900)是错误的,因为有一笔扣款被丢失了。当前读通过加锁确保了操作的串行化。
三、重要区别与联系
-
默认行为:单纯的
SELECT
语句默认是快照读,而UPDATE
、DELETE
、INSERT
等写操作默认是当前读。UPDATE
操作的过程其实是:先当前读找到要更新的记录并加锁,然后修改数据。 -
解决不同的问题:
- 快照读 解决了读-写并发冲突,提升了数据库的读并发能力。
- 当前读 解决了写-写并发冲突,保证了数据更新的正确性。
-
组合使用:在一个事务中,可以混合使用快照读和当前读。
START TRANSACTION; -- 快照读:查看大致情况,不加锁 SELECT * FROM orders; -- 当前读:决定要处理某个订单时,用当前读锁定它,防止别人修改 SELECT * FROM orders WHERE id = 123 FOR UPDATE; -- ... 处理业务逻辑 ... UPDATE orders SET status = 'processed' WHERE id = 123; COMMIT;
总结
操作类型 | 语句示例 | 读类型 | 说明 |
---|---|---|---|
快照读 | SELECT * FROM table; |
历史数据 | 基于MVCC,不加锁,实现非阻塞读 |
当前读 | SELECT * FROM table LOCK IN SHARE MODE; |
最新数据 | 加S锁(共享锁) |
SELECT * FROM table FOR UPDATE; |
最新数据 | 加X锁(排他锁) | |
UPDATE table ... |
最新数据 | 先当前读加X锁,再修改 | |
DELETE FROM table ... |
最新数据 | 先当前读加X锁,再删除 | |
INSERT INTO table ... |
- | 会对新插入的数据加X锁(隐式) |