问题

当前读和快照读有什么区别?

答案

核心概念

  • 快照读(Snapshot Read):读取的是记录的快照版本(历史版本),不加锁,基于MVCC实现
  • 当前读(Current Read):读取的是记录的最新版本,会加锁,基于锁机制实现

1. 快照读(Snapshot Read)

定义

读取的是事务开始时(或每次查询时)的数据快照,通过MVCC机制实现,不加锁

触发场景

-- 1. 普通SELECT语句
SELECT * FROM user WHERE id = 1;

-- 2. SELECT ... FROM(子查询)
SELECT * FROM (SELECT * FROM user WHERE age > 20) t;

核心特点

  1. 不加锁:读操作不会阻塞写操作
  2. 基于MVCC:通过ReadView + Undo Log版本链实现
  3. 读历史版本:可能读不到最新提交的数据(RR级别)
  4. 高并发:读写互不阻塞,性能高

工作原理

事务A:SELECT * FROM user WHERE id = 1;
    ↓
生成/复用 ReadView
    ↓
遍历 Undo Log 版本链
    ↓
根据可见性规则找到可见版本
    ↓
返回快照数据(不加锁)

同时,事务B可以自由UPDATE该记录(不被阻塞)

2. 当前读(Current Read)

定义

读取的是数据库中的最新版本,会对记录加锁,确保读到的数据是最新且不会被其他事务修改。

触发场景

-- 1. SELECT ... LOCK IN SHARE MODE (共享锁/读锁)
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;

-- 2. SELECT ... FOR UPDATE (排他锁/写锁)
SELECT * FROM user WHERE id = 1 FOR UPDATE;

-- 3. INSERT
INSERT INTO user (id, name) VALUES (1, 'Tom');

-- 4. UPDATE
UPDATE user SET age = 20 WHERE id = 1;

-- 5. DELETE
DELETE FROM user WHERE id = 1;

核心特点

  1. 加锁读取:根据隔离级别和查询条件加不同类型的锁
  2. 读最新数据:读取当前最新提交的数据
  3. 阻塞其他事务:可能导致其他事务等待
  4. 防止幻读:RR级别下通过Next-Key Lock(记录锁+间隙锁)

工作原理

事务A:SELECT * FROM user WHERE id = 1 FOR UPDATE;
    ↓
读取最新版本的记录
    ↓
对记录加排他锁(X锁)
    ↓
返回最新数据

事务B尝试修改:UPDATE user SET age = 20 WHERE id = 1;
    ↓
被阻塞,等待事务A释放锁

3. 快照读 vs 当前读对比

对比维度 快照读 当前读
读取版本 历史快照版本 最新���本
是否加锁 不加锁 加锁(共享锁/排他锁)
实现机制 MVCC(ReadView + Undo Log) 锁机制 + MVCC
触发SQL SELECT SELECT FOR UPDATE
SELECT LOCK IN SHARE MODE
UPDATEDELETEINSERT
并发性能 高(读写不阻塞) 低(可能互相阻塞)
数据一致性 快照一致性 实时一致性
是否防幻读 防(RR级别快照读) 防(通过Next-Key Lock)
应用场景 普通查询、报表统计 更新前查询、防止并发修改

4. 实战对比示例

场景1:快照读示例

-- 事务A(快照读)
START TRANSACTION;
SELECT age FROM user WHERE id = 1; -- 结果:20(生成ReadView)

-- 事务B修改并提交
UPDATE user SET age = 30 WHERE id = 1;
COMMIT;

-- 事务A再次查询(RR级别)
SELECT age FROM user WHERE id = 1; -- 结果:仍是20(使用快照)

COMMIT;

场景2:当前读示例

-- 事务A(当前读)
START TRANSACTION;
SELECT age FROM user WHERE id = 1 FOR UPDATE; -- 结果:20,并加锁

-- 事务B尝试修改
UPDATE user SET age = 30 WHERE id = 1; -- 被阻塞,等待事务A释放锁

-- 事务A提交
COMMIT; -- 释放锁

-- 事务B的UPDATE才能执行

5. 混合使用导致的问题

问题场景:快照读 + 当前读

-- 事务A
START TRANSACTION;

-- 第一步:快照读
SELECT * FROM user WHERE age > 20; -- 返回3条

-- 事务B插入新数据并提交
INSERT INTO user (id, age) VALUES (100, 25);
COMMIT;

-- 第二步:当前读
SELECT * FROM user WHERE age > 20 FOR UPDATE; -- 返回4条(幻读!)

-- 第三步:再次快照读
SELECT * FROM user WHERE age > 20; -- 返回3条(不一致!)

COMMIT;

问题分析

  • 快照读看到的是快照版本(3条)
  • 当前读看到的是最新数据(4条)
  • 同一事务中,两种读方式结果不一致

解决方案

-- 方案1:统一使用当前读
START TRANSACTION;
SELECT * FROM user WHERE age > 20 FOR UPDATE; -- 全程加锁
-- ... 其他操作
SELECT * FROM user WHERE age > 20 FOR UPDATE; -- 结果一致
COMMIT;

-- 方案2:使用SERIALIZABLE隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
SELECT * FROM user WHERE age > 20; -- 自动加共享锁,等同于当前读
COMMIT;

6. 锁类型详解

当前读的加锁规则

SQL语句 锁类型 说明
SELECT ... LOCK IN SHARE MODE 共享锁(S锁) 允许其他事务读取,但不能修改
SELECT ... FOR UPDATE 排他锁(X锁) 不允许其他事务读取或修改
UPDATE 排他锁(X锁) 对涉及的记录加锁
DELETE 排他锁(X锁) 对涉及的记录加锁
INSERT 排他锁(X锁) 对新插入的记录加锁

RR级别下的Next-Key Lock

-- 假设表中id: 1, 5, 10

-- 当前读,加Next-Key Lock
SELECT * FROM user WHERE id > 3 FOR UPDATE;

-- 加锁范围:
-- (3, 5]:Next-Key Lock
-- (5, 10]:Next-Key Lock
-- (10, +∞):Gap Lock

-- 事务B无法在(3, +∞)范围内插入数据
INSERT INTO user (id, name) VALUES (6, 'Test'); -- 被阻塞

7. 应用场景选择

使用快照读的场景

  1. 普通查询:不需要锁定数据,读取快照即可
    SELECT * FROM orders WHERE user_id = 123;
    
  2. 报表统计:需要一致性快照,避免统计过程中数据变化
    SELECT DATE(created_at), COUNT(*) FROM orders GROUP BY DATE(created_at);
    
  3. 高并发读取:不希望读操作阻塞写操作

使用当前读的场景

  1. 更新前查询:确保读到最新数据后再修改
    SELECT balance FROM account WHERE id = 1 FOR UPDATE;
    UPDATE account SET balance = balance - 100 WHERE id = 1;
    
  2. 防止超卖:库存扣减场景
    SELECT stock FROM product WHERE id = 1 FOR UPDATE;
    UPDATE product SET stock = stock - 1 WHERE id = 1;
    
  3. 唯一性检查:确保插入前数据唯一
    SELECT * FROM user WHERE username = 'tom' FOR UPDATE;
    -- 如果不存在,则插入
    

面试要点总结

  1. 快照读不加锁,当前读加锁,这是最核心的区别
  2. 快照读基于MVCC,当前读基于锁机制
  3. RR级别下,快照读和当前读可能看到不同的数据
  4. 当前读在RR级别下通过Next-Key Lock防止幻读
  5. UPDATE、DELETE、INSERT都是当前读,这是面试常考点
  6. 混合使用快照读和当前读可能导致数据不一致问题
  7. 业务开发中,更新前查询必须使用FOR UPDATE(当前读)
  8. 理解两者区别是理解MySQL并发控制的关键