上篇「插入意向锁」时为了产生间隙锁、插入意向锁时使用了 'SELECT . . . FOR UPDATE', 但你有没有想过:为什么有时候一个普通的
SELECT不加锁,而加上FOR UPDATE就会阻塞别人? 我们就来进一步理解:快照读(Snapshot Read) 和 当前读(Current Read)。
一、基本概念:MySQL 的两种"读"法
在 Mysql 中,并非所有 SELECT 都一样。根据是否加锁、是否读最新数据,分为两类:
快照读(Snapshot Read)
- 定义 :读取事务开始时(或语句开始时)的一致性视图(Read View),不加任何锁。
- 典型语句:
sql
SELECT * FROM orders WHERE user_id = 1001;
- 底层机制 :基于 MVCC(多版本并发控制),通过 undo log 构建历史版本。
- 特点 :
- 读不阻塞写,写不阻塞读;
- 在 RR 隔离级别下,整个事务看到同一快照(可重复读);
- 在 RC 级别下,每次 SELECT 都生成新快照(不可重复读)。
当前读(Current Read)
- 定义 :读取数据库中最新的、已提交的数据 ,并加锁以防止其他事务修改。
- 典型语句:
sql
SELECT * FROM orders WHERE user_id = 1001 FOR UPDATE; -- 排他锁
SELECT * FROM orders WHERE user_id = 1001 LOCK IN SHARE MODE; -- 共享锁(MySQL 8.0+ 可用 FOR SHARE)
UPDATE orders SET status = 'paid' WHERE id = 123;
DELETE FROM orders WHERE id = 123;
- 底层机制 :直接访问聚簇索引或二级索引的最新记录,并根据隔离级别加 记录锁 / 间隙锁(RR) / Next-Key Lock。
- 特点 :
- 会阻塞其他写操作(甚至读操作,取决于锁类型)
- 是实现"悲观锁"和防止并发冲突的关键手段。
快照读 = 安静地看历史;当前读 = 大声宣布"我要改这里,请别动!"
二、使用场景:什么时候该用哪种读?
场景 1:只读查询 → 用快照读
- 用户查看订单列表、商品详情等;
- 对数据一致性要求不高,或能接受"稍旧"数据;
- 优势:零锁开销,高并发无压力。
场景 2:先查后改(Check-Then-Act)→ 必须用当前读!
java
// 伪代码:错误示范(快照读)
Order order = select("SELECT * FROM orders WHERE id = 123"); // 快照读
if (order.status == "unpaid") {
update("UPDATE orders SET status = 'paid' WHERE id = 123");
}
→ 问题 :两个线程同时执行,都看到 status=unpaid,导致重复支付!
正确做法(当前读):
sql
-- 加锁读取最新状态(仅用来体现当前读的作用,高并发场景下不建议使用 FOR UPDATE)
SELECT * FROM orders WHERE id = 123 FOR UPDATE;
-- 再判断并更新
场景 3:防止幻读(RR 级别下)
- 业务要求"范围内不能有新数据插入",如库存扣减、唯一编号生成;
- 必须用
SELECT ... FOR UPDATE触发 Next-Key Lock(记录 + 间隙锁); - 否则即使快照读看不到新数据,别人仍可插入,破坏业务逻辑。
三、避坑指南
问题 1:FOR UPDATE 导致大量阻塞甚至死锁
- 原因:范围查询未走索引,InnoDB 在 RR 下对主键全表加间隙锁;
- 案例:
sql
SELECT * FROM orders WHERE create_time > '2024-01-01' FOR UPDATE; -- create_time 无索引
→ 锁住整个表,所有 INSERT 被阻塞! 解决方案:
- 确保
WHERE条件命中索引; - 高并发场景考虑降级到 READ COMMITTED(只加记录锁,不加间隙锁);
- 避免大范围扫描,改用分页或等值查询。
问题 2:快照读 + 当前读混合,误判"幻读"
- 现象:
SQL
-- 事务内
SELECT COUNT(*) FROM t WHERE id > 10; -- 快照读,返回 0
SELECT COUNT(*) FROM t WHERE id > 10 FOR UPDATE; -- 当前读,返回 1!
- 误解:"RR 下怎么还有幻读?"
- 真相:这不是幻读!快照读看历史,当前读看现在,两者本就不该一致。
解决方案:
- 统一读取模式:要么全用快照读(接受只读一致性),要么关键路径全用当前读;
- 不要用快照读做业务判断后再用当前读更新。
问题 3:RC 级别下 FOR UPDATE 无法防止幻读
- 现象 :在 RC 下,即使用了
FOR UPDATE,别人仍可插入新数据; - 原因:RC 不使用间隙锁,只锁已有记录;
- 影响:如"查无此用户 → 插入"可能失败(唯一键冲突)。
解决方案:
- 依赖 数据库唯一索引 作为最终兜底;
- 应用层做好异常捕获与重试(如捕获
Duplicate entry); - 核心链路若需强一致性,保留 RR + 精准加锁。
快照读和当前读,是 InnoDB 实现高性能与一致性平衡的双翼。
- 快照读 让读操作如丝般顺滑;
- 当前读 为写操作筑起安全防线。
但在实际开发中,最大的风险不是技术本身,而是"不知道自己在用哪种读"。
下次当你写下 SELECT 时,不妨多想一步 "我需要的是历史快照,还是此刻的真实?"
💡 感谢你看完这篇内容,这是我自己在工作学习中遇到的case,做一些简单的研究,并总结经验,如有遗漏或不合理的地方,欢迎你提出问题,让我们一起探索。