可重复读 (RR) 的缺陷与“当前读”方案

1. 矛盾场景的复现

假设系统处于 可重复读 (RR) 隔离级别。

  • T1 时刻 :表 t 中有一行记录,age = 18
  • T2 时刻事务 A 开启,执行 SELECT age FROM t。根据我们刚刚总结的 ReadView 快照规则,它读到了 age = 18
  • T3 时刻事务 B 开启,执行 UPDATE t SET age = 20立刻提交 。此时物理磁盘和最新的 Undo Log 中,这行数据的最新真实值变成了 20
  • T4 时刻事务 A 准备基于它看到的数据,给 age 加 1。于是执行:UPDATE t SET age = age + 1

此时,数据库内核面临着一个生死攸关的二选一困境(矛盾点爆发)


2. 矛盾的两极(无论怎么选,好像都错)

假设选项一:坚持快照逻辑(基于 18 去改)

如果数据库强制执行你总结的"快照读"逻辑,认为事务 A 只能看到 18。

  • 执行计算 :18+1=1918 + 1 = 1918+1=19。
  • 物理动作 :事务 A 将 age = 19 写入磁盘,并覆盖掉当前记录。
  • 灾难性后果丢失更新(Lost Update) 。事务 B 辛辛苦苦改的 20 凭空消失了!如果这是银行存款,用户的钱就彻底算错了。数据库的一致性底线彻底崩塌
假设选项二:背叛快照逻辑(基于 20 去改)

如果数据库为了保护事务 B 的成果,让事务 A 去读取物理磁盘上最新的真实值。

  • 执行计算 :20+1=2120 + 1 = 2120+1=21。
  • 物理动作 :事务 A 将 age = 21 写入磁盘。
  • 灾难性后果逻辑精神分裂 。对于事务 A 来说,上一秒 SELECT 查出来的明明是 18,执行了一句 + 1 的操作,再次 SELECT 查看结果,竟然变成了 21?!说好的"可重复读(事务隔离)"呢?这直接打破了开发者的编程常识。

3. InnoDB 的终极解法:一套引擎,两套截然不同的读取规则

面对"坚持快照会导致更新丢失"与"放弃快照会导致隔离性破裂"这个死局,InnoDB 给出的破局之道是:在底层彻底切分"读"与"写"的物理执行路径。

在可重复读(RR)隔离级别下,只要你向数据库发起请求,InnoDB 的执行器就会自动根据你的 SQL 语句类型,将请求分流到以下两种完全不同的"读取模式"中:

模式 A:快照读(Snapshot Read)------ 追求极致并发的无锁查询
  • 触发条件
    所有普通的查询语句,例如 SELECT * FROM table WHERE ...(不带任何加锁后缀)。
  • 底层物理动作
    1. 依赖内存快照:执行器严格依赖当前事务在第一次查询时生成的 ReadView。
    2. 全程无锁:执行器在聚簇索引树上遍历数据时,不需要对任何数据页或记录加锁。
    3. 版本回溯 :当读取到 B+ 树叶子节点上的最新物理记录时,如果根据 trx_id 判定该最新记录对当前 ReadView 不可见,它就会顺着该记录的 roll_pointer 指针进入 Undo Log。在历史版本链中逐个套用可见性算法,直到提取出那个符合当前快照规则的旧版本数据。
  • 核心目的
    实现读写互不阻塞。即使其他事务正在对该行数据进行高频修改,快照读也丝毫不受影响,因为它读取的是通过 Undo Log 拼装出来的历史安全版本。
模式 B:当前读(Current Read)------ 捍卫数据底线的加锁读取
  • 触发条件
    1. 所有的写操作UPDATEDELETEINSERT
    2. 所有显式加锁的读操作SELECT ... FOR UPDATE(加排他锁/X锁)、SELECT ... LOCK IN SHARE MODE(加共享锁/S锁)
  • 底层物理动作
    1. 强制撕破快照 :一旦触发当前读,执行器会彻底无视当前事务的 ReadView 状态。无论快照规则规定你应该看哪个历史版本,当前读一律跳过判定逻辑。
    2. 直击最新物理行 :执行器直接定位到聚簇索引 B+ 树的叶子节点,强制抓取该行数据在磁盘/内存中最新且已提交的真实物理版本
    3. 瞬间加锁封锁 :在读取到最新版本的同时,执行器会立刻在聚簇索引的这行记录上加锁(如果条件允许,还会加上锁定前后范围的 Next-Key Lock)。如果是 UPDATE/DELETEFOR UPDATE,加的是排他锁,其物理意义是:"在我基于这个最新值计算并提交之前,任何并发事务都必须在锁外排队,禁止触碰这行数据!"
  • 核心目的
    绝对禁止丢失更新(Lost Update)。因为数据的修改必须基于当前数据库最真实的物理状态,绝不能基于虚假的"历史快照"去盲目覆盖他人的提交成果。

4. 矛盾是如何被物理化解的?

回到我们最初的 T4 时刻,当事务 A 执行 UPDATE t SET age = age + 1 时,真实的底层动作是这样的:

  1. 引擎判断 :这是一条 UPDATE 语句,必须走当前读通道。
  2. 撕破快照:事务 A 放弃使用 T2 时刻生成的 ReadView,直接去聚簇索引叶子节点抓取最新状态。
  3. 加锁获取真实值 :事务 A 发现最新的物理记录是事务 B 提交的 age = 20。于是 A 给这行记录上一把排他锁。
  4. 执行计算与更新 :引擎基于真实值进行计算:20+1=2120 + 1 = 2120+1=21。生成最新的版本,记录 trx_id = A,并将 roll_pointer 指向包含 20 的 Undo Log。

那么,这违反了"可重复读"吗?

  • 数据安全的角度:它拯救了事务 B 的更新,避免了丢失更新。
  • 开发者视角 :确实会产生疑惑(查出来是 18,加 1 变 21)。因此,在正规的业务开发中,如果你的业务逻辑是"先查出数据,再基于查出的数据做修改",强烈禁止使用普通 SELECT ,必须使用 SELECT ... FOR UPDATE 强行把查询升级为"当前读",让事务 B 根本无法在中间插队修改。
相关推荐
小峰编程2 小时前
Redis 集群模式
数据库·redis·bootstrap
填满你的记忆2 小时前
MySQL 索引:从底层类型到面试避坑
数据库·mysql·面试
LSL666_2 小时前
8 Redis 高可用进阶(主从容灾→选举机制→哨兵机制)
数据库·redis·缓存
ILL11IIL2 小时前
Mysql 集群技术
数据库·mysql·mha
茉莉玫瑰花茶2 小时前
C++ ORM 实战:ODB 框架全解析(Linux + MySQL)
jvm·数据库·oracle
chushiyunen3 小时前
django日志使用笔记
数据库·笔记·django
听雪楼主.3 小时前
某客户核心业务系统报ORA-600错误分析处理
数据库·oracle
威联通安全存储3 小时前
严谨性的数字基石:某精密医疗器械企业基于威联通的数据治理实践
运维·数据库·python
不剪发的Tony老师3 小时前
DbPaw:一款AI驱动的现代化数据库开发工具
数据库