MySQL 中的快照读与当前读:原理、区别与应用详解

555 阅读6分钟

在使用 MySQL(InnoDB 引擎)进行事务开发时,经常会遇到两个核心概念:快照读(Snapshot Read)当前读(Current Read) 。它们直接影响事务的并发行为、数据一致性以及锁的使用。

tips:本文对应的幽默版本请跳转:juejin.cn/post/749873…

本文将系统讲解:

  1. 两者的定义与区别
  2. 常见 SQL 的读类型归属
  3. 实际应用场景与选择策略
  4. 如何处理幻读问题
  5. 常见误区澄清

三大关键点需特别牢记:

当前读读取的是其他事务已提交的“最新数据” ,绕过自身快照视图

当前读可通过加锁(如 next-key lock)解决幻读问题

LOCK IN SHARE MODE 会加共享锁,锁定数据防止别人修改,但自己不改


一、基本概念

1. 快照读(Snapshot Read)

  • 是指读取的是事务开始时的数据快照,而不是当前最新的数据。
  • 基于 MVCC(多版本并发控制)实现,不加锁,性能高。
  • 避免读写冲突,但可能存在幻读问题。

示例:

SELECT * FROM product WHERE id = 1;

在 REPEATABLE READ 隔离级别下,无论其他事务是否修改 id=1 的记录,本事务都看到的是开始时的版本

2. 当前读(Current Read)

  • 读取的是当前已提交的最新数据版本,并且加锁(X锁或S锁)。
  • 是为了修改数据或防止其他事务改动当前正在使用的数据。

示例:

SELECT * FROM product WHERE id = 1 FOR UPDATE;

读取的是最新提交的数据,并对该行加排他锁,防止其他事务更新或删除该行。


二、操作类型归类

SQL 语句类型是否加锁说明
SELECT ...快照读默认使用 MVCC,无锁
SELECT ... FOR UPDATE当前读是(X锁)当前读 + 排他锁
SELECT ... LOCK IN SHARE MODE当前读是(S锁)当前读 + 共享锁,不修改
UPDATE ...当前读是(X锁)读取最新数据并加排他锁
DELETE ...当前读是(X锁)同上,读取+删除需加锁
INSERT(普通插入)写操作无需读现有数据,不加锁
INSERT ... ON DUPLICATE KEY当前读检查冲突键需先读并加锁

三、快照读 vs 当前读:对比总结

项目快照读当前读
数据版本事务开始时快照最新已提交的数据
是否加锁是(X锁或S锁)
并发性能相对较低
一致性保障依赖隔离级别,可能幻读数据行强一致,避免幻读
适用场景数据展示、统计分析等秒杀、扣库存、更新操作前读

四、什么是幻读?快照读和当前读如何解决?

1. 幻读定义

幻读(Phantom Read)是指:

在同一个事务中,两次相同条件的查询,结果集不同,出现了“幽灵”行 —— 例如有其他事务新增了满足条件的记录。

2. 快照读的表现

  • REPEATABLE READ 下,快照读保证的是读取的一致性视图(旧版本),所以即使别的事务插入了新行,当前事务看不到,表面上“避免了幻读”。
  • 但本质上,快照读只是“忽略”了这些新增行,而非真正加锁控制住范围

3. 当前读的解决方案

当前读通过 间隙锁 + 行锁(即 next-key lock) 实现真正意义上的防幻读机制:

  • 锁定某个查询范围或索引区间
  • 阻止其他事务插入、删除、修改这些范围内的数据

示例:

SELECT * FROM order WHERE amount > 100 FOR UPDATE;
-- 锁定了所有 amount > 100 的记录和插入间隙,防止别人插入新满足条件的记录

✅ 所以:

  • 快照读避免幻读,是视图机制使然,不感知新增数据;
  • 当前读通过加锁,从物理层面防止了幻读发生。

五、事务并发行为演示

场景:T1 和 T2 并发访问同一行数据(id=1)

  1. T1 开始事务并执行快照读:
START TRANSACTION;
SELECT * FROM product WHERE id = 1;
-- 读取快照版本(即使 T2 更新,也看不到)
  1. T2 执行更新并提交:
START TRANSACTION;
UPDATE product SET stock = stock - 1 WHERE id = 1;
COMMIT;
  1. T1 再次读取(快照读):
SELECT * FROM product WHERE id = 1;
-- 仍然看到旧版本(快照视图)
  1. T1 若执行当前读:
SELECT * FROM product WHERE id = 1 FOR UPDATE;
-- 获取 T2 已提交的最新数据并加锁

六、开发实战建议

✅ 选择使用快照读的场景:

  • 报表统计、图表展示、商品浏览页面
  • 管理后台分页、模糊搜索、高并发查询

✅ 选择当前读的场景:

  • 秒杀 / 抢购 / 扣减库存等操作
  • 先查后改、先查后删逻辑
  • 判断是否存在再插入(防止并发冲突)

七、是否必须使用 LOCK IN SHARE MODE

很多人会疑惑,既然 FOR UPDATE 本身也能加锁,为何还需要 LOCK IN SHARE MODE 呢?

虽然两者在防止他人修改方面都能达到目的,但 LOCK IN SHARE MODE 存在的重要意义在于:

比较点FOR UPDATELOCK IN SHARE MODE
锁类型排他锁(X锁)共享锁(S锁)
阻塞其他读(当前读)
并发友好性
表达意图要读并修改只读不改

✅ 因此,当你明确不会修改数据时,使用 LOCK IN SHARE MODE 更加语义清晰、并发友好,且降低死锁概率。在业务逻辑中做出准确区分,有助于提升数据库系统的可维护性和稳定性。


八、Spring Boot 中如何实现“当前读+修改”事务

@Service
public class ProductService {

    @Transactional
    public void purchase(Long productId) {
        Product product = productMapper.selectForUpdate(productId);

        if (product.getStock() <= 0) {
            throw new RuntimeException("库存不足");
        }

        product.setStock(product.getStock() - 1);
        productMapper.updateById(product);
    }
}

Mapper 中:

@Select("SELECT * FROM product WHERE id = #{id} FOR UPDATE")
Product selectForUpdate(Long id);

事务注解 @Transactional 保证了当前读和后续写操作在同一事务中完成。


九、常见误区澄清

误区说法正确解释
SELECT ... FOR UPDATE 是快照读❌ 是当前读,读取最新版本并加锁
快照读能看到其他事务提交的更新❌ 不行,始终读事务开始时的视图
LOCK IN SHARE MODE 不加锁❌ 加共享锁,防止他人改动数据
插入操作不会被锁阻塞✅ 通常不锁已有行,但如违反唯一键,也可能阻塞

十、结语

理解并正确使用快照读和当前读,是 MySQL 并发控制、事务一致性控制的关键能力。在日常开发中,建议:

  • 以快照读为默认手段,性能高、无锁;
  • 在涉及“读+改”的逻辑中,切换为当前读
  • 明确使用场景,避免误用锁或出现死锁风险。

熟悉这些原理,有助于写出性能高、正确性强的数据库访问代码。