一、现象引入:违背直觉的读写不一致
在 MySQL InnoDB 默认可重复读(REPEATABLE READ, RR) 隔离级别下,存在一个非常反直觉的现象:
事务 A 先开启,事务 B 插入一条记录并提交;事务 A 中普通 SELECT 查不到该记录,但执行 UPDATE / DELETE 却能成功更新/删除这条记录。
可复现场景 SQL
1 | CREATE TABLE user_info ( |
| 事务 A(RR) | 事务 B |
|---|---|
| BEGIN; | |
| SELECT * FROM user_info WHERE id=2; → 空 | |
| BEGIN; | |
| INSERT INTO user_info VALUES(2, ‘lisi’); | |
| COMMIT; | |
| SELECT * FROM user_info WHERE id=2; → 依然空 | |
| UPDATE user_info SET name=’lisics’ WHERE id=2; → 影响行数 1 | |
| SELECT * FROM user_info WHERE id=2; → 还是空 | |
| COMMIT; |
现象总结
SELECT 看不见,UPDATE 却能更新成功,这是 RR 级别下的标准行为,并非 MySQL Bug。
二、底层原理:快照读 vs 当前读
MySQL InnoDB 在 RR 级别下,读操作分为两种完全不同的机制,这是现象产生的根本原因。
1. 快照读(Consistent Read)
触发语句
普通 SELECT(无 FOR UPDATE / LOCK IN SHARE MODE)
机制
基于 MVCC(多版本并发控制) 实现
事务启动时生成 ReadView(一致性快照)
后续所有普通 SELECT 都基于该快照执行
只能看到 快照生成前已提交 的数据
对应现象
事务 A 的普通 SELECT 查不到事务 B 后续提交的 id=2 记录,符合“可重复读”的定义。
2. 当前读(Current Read)
触发语句
UPDATE
DELETE
SELECT … FOR UPDATE(排他锁)
SELECT … LOCK IN SHARE MODE(共享锁)
机制
不使用事务启动时的快照
直接读取 数据库最新已提交的数据
读取时会对目标数据加锁(行锁/间隙锁),保证数据一致性
不受事务启动时的 ReadView 限制
对应现象
事务 A 的 UPDATE 能直接读取到事务 B 已提交的 id=2 记录,并成功更新,这是当前读的核心特性。
三、执行流程完整解释
结合上述场景,逐步骤拆解底层执行逻辑,清晰理解“查不到却能更新”的全过程:
事务 A 执行 BEGIN 开启,InnoDB 为其生成一个 ReadView(一致性快照),此时 user_info 表中无 id=2 的记录。
事务 A 执行第一次 SELECT * FROM user_info WHERE id=2 → 触发快照读,基于 ReadView 查询,返回空结果。
事务 B 执行 BEGIN 开启,插入 id=2、name=lisi 的记录,随后 COMMIT 提交 → 该记录写入磁盘,成为数据库最新已提交数据。
事务 A 执行第二次 SELECT * FROM user_info WHERE id=2 → 依然触发快照读,ReadView 未更新(事务未结束,快照不变),依旧返回空结果。
事务 A 执行 UPDATE user_info SET name=’lisics’ WHERE id=2 → 触发当前读:
跳过事务 A 的 ReadView,直接扫描数据库最新的数据页
命中 id=2 的记录(事务 B 已提交),对该记录加排他锁
执行更新操作,更新成功后返回“受影响行数 1”
事务 A 执行第三次 SELECT * FROM user_info WHERE id=2 → 仍然触发快照读,ReadView 依旧未变,还是看不到更新后的记录。
事务 A 执行 COMMIT 提交 → 事务结束,ReadView 被销毁,更新后的 id=2 记录对后续所有事务可见。
四、与幻读的关系
1. 标准 SQL 中幻读的定义
同一事务内,对相同查询条件执行多次 SELECT,结果集出现新增的行(即“幻行”),这种现象称为幻读。
2. MySQL InnoDB RR 级别对幻读的处理
InnoDB 通过 MVCC + Next-Key Lock(间隙锁) 在 RR 级别下 部分解决幻读,具体分为两种场景:
纯快照读场景:通过 MVCC 的 ReadView 机制,多次 SELECT 结果一致,完全避免幻读。
当前读场景:无法避免幻读,当前读会读取所有已提交的最新数据,包括其他事务提交的“幻行”,因此会出现“查不到但能更新”的现象。
3. 结论
这种“查不到却能更新”的现象,本质是 MySQL RR 级别下的 半幻读/部分幻读,是 InnoDB 为了平衡并发性能与数据一致性设计的标准行为,并非异常。
五、核心总结
RR 隔离级别 ≠ 完全屏蔽外部数据变化:仅约束普通 SELECT(快照读),不约束 UPDATE/DELETE(当前读)。
快照读与当前读的核心区别:SELECT = 快照读(看事务启动时的历史数据),UPDATE/DELETE = 当前读(看数据库最新已提交数据)。
同一事务内,读和写可能基于不同的数据版本:这是“查不到却能更新”的核心原因。
该现象是 MySQL InnoDB RR 级别的正常行为,不是 Bug,无需修复,需在开发中规避逻辑风险。
六、开发实践建议
针对该现象,结合实际开发场景,给出3条实用建议,避免业务逻辑异常:
- 同一事务内需要“先查后改”时,统一使用
SELECT ... FOR UPDATE(当前读),确保读、写基于同一数据版本,避免“查不到却能更新”导致的逻辑错误。示例:
`BEGIN;
– 用当前读查询,确保读取最新数据
SELECT * FROM user_info WHERE id=2 FOR UPDATE;
– 基于查询结果执行更新,避免逻辑异常
UPDATE user_info SET name=’lisics’ WHERE id=2;
COMMIT;`
对数据一致性要求极高的场景(如金融、支付),建议使用
SERIALIZABLE(串行化)隔离级别,彻底杜绝幻读和半幻读,但会降低并发性能,需权衡使用。开发中需摒弃“RR 级别下完全看不到外部新数据”的错误认知,明确区分快照读与当前读的使用场景,避免依赖错误的隔离级别特性编写业务逻辑。