在使用 MySQL(InnoDB 引擎)进行事务开发时,经常会遇到两个核心概念:快照读(Snapshot Read) 和 当前读(Current Read) 。它们直接影响事务的并发行为、数据一致性以及锁的使用。
tips:本文对应的幽默版本请跳转:juejin.cn/post/749873…
本文将系统讲解:
- 两者的定义与区别
- 常见 SQL 的读类型归属
- 实际应用场景与选择策略
- 如何处理幻读问题
- 常见误区澄清
三大关键点需特别牢记:
✅ 当前读读取的是其他事务已提交的“最新数据” ,绕过自身快照视图
✅ 当前读可通过加锁(如 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)
- T1 开始事务并执行快照读:
START TRANSACTION;
SELECT * FROM product WHERE id = 1;
-- 读取快照版本(即使 T2 更新,也看不到)
- T2 执行更新并提交:
START TRANSACTION;
UPDATE product SET stock = stock - 1 WHERE id = 1;
COMMIT;
- T1 再次读取(快照读):
SELECT * FROM product WHERE id = 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 UPDATE | LOCK 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 并发控制、事务一致性控制的关键能力。在日常开发中,建议:
- 以快照读为默认手段,性能高、无锁;
- 在涉及“读+改”的逻辑中,切换为当前读;
- 明确使用场景,避免误用锁或出现死锁风险。
熟悉这些原理,有助于写出性能高、正确性强的数据库访问代码。