一、现象引入:违背直觉的读写不一致

在 MySQL InnoDB 默认可重复读(REPEATABLE READ, RR) 隔离级别下,存在一个非常反直觉的现象:

事务 A 先开启,事务 B 插入一条记录并提交;事务 A 中普通 SELECT 查不到该记录,但执行 UPDATE / DELETE 却能成功更新/删除这条记录

可复现场景 SQL

1
2
3
4
5
6
CREATE TABLE user_info (
id INT PRIMARY KEY,
name VARCHAR(20)
);

INSERT INTO user_info VALUES (1, 'zhangsan');
事务 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 记录,并成功更新,这是当前读的核心特性。


三、执行流程完整解释

结合上述场景,逐步骤拆解底层执行逻辑,清晰理解“查不到却能更新”的全过程:

  1. 事务 A 执行 BEGIN 开启,InnoDB 为其生成一个 ReadView(一致性快照),此时 user_info 表中无 id=2 的记录。

  2. 事务 A 执行第一次 SELECT * FROM user_info WHERE id=2 → 触发快照读,基于 ReadView 查询,返回空结果。

  3. 事务 B 执行 BEGIN 开启,插入 id=2、name=lisi 的记录,随后 COMMIT 提交 → 该记录写入磁盘,成为数据库最新已提交数据。

  4. 事务 A 执行第二次 SELECT * FROM user_info WHERE id=2 → 依然触发快照读,ReadView 未更新(事务未结束,快照不变),依旧返回空结果。

  5. 事务 A 执行 UPDATE user_info SET name=’lisics’ WHERE id=2 → 触发当前读:

    • 跳过事务 A 的 ReadView,直接扫描数据库最新的数据页

    • 命中 id=2 的记录(事务 B 已提交),对该记录加排他锁

    • 执行更新操作,更新成功后返回“受影响行数 1”

  6. 事务 A 执行第三次 SELECT * FROM user_info WHERE id=2 → 仍然触发快照读,ReadView 依旧未变,还是看不到更新后的记录。

  7. 事务 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 为了平衡并发性能与数据一致性设计的标准行为,并非异常。


五、核心总结

  1. RR 隔离级别 ≠ 完全屏蔽外部数据变化:仅约束普通 SELECT(快照读),不约束 UPDATE/DELETE(当前读)。

  2. 快照读与当前读的核心区别:SELECT = 快照读(看事务启动时的历史数据),UPDATE/DELETE = 当前读(看数据库最新已提交数据)。

  3. 同一事务内,读和写可能基于不同的数据版本:这是“查不到却能更新”的核心原因。

  4. 该现象是 MySQL InnoDB RR 级别的正常行为,不是 Bug,无需修复,需在开发中规避逻辑风险。


六、开发实践建议

针对该现象,结合实际开发场景,给出3条实用建议,避免业务逻辑异常:

  1. 同一事务内需要“先查后改”时,统一使用 SELECT ... FOR UPDATE(当前读),确保读、写基于同一数据版本,避免“查不到却能更新”导致的逻辑错误。示例:
    `BEGIN;

– 用当前读查询,确保读取最新数据
SELECT * FROM user_info WHERE id=2 FOR UPDATE;
– 基于查询结果执行更新,避免逻辑异常
UPDATE user_info SET name=’lisics’ WHERE id=2;
COMMIT;`

  1. 对数据一致性要求极高的场景(如金融、支付),建议使用 SERIALIZABLE(串行化) 隔离级别,彻底杜绝幻读和半幻读,但会降低并发性能,需权衡使用。

  2. 开发中需摒弃“RR 级别下完全看不到外部新数据”的错误认知,明确区分快照读与当前读的使用场景,避免依赖错误的隔离级别特性编写业务逻辑。