可重复读 (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. 所有的写操作 :UPDATE、DELETE、INSERT。
    2. 所有显式加锁的读操作 :SELECT ... FOR UPDATE(加排他锁/X锁)、SELECT ... LOCK IN SHARE MODE(加共享锁/S锁)
  • 底层物理动作 :
    1. 强制撕破快照 :一旦触发当前读,执行器会彻底无视当前事务的 ReadView 状态。无论快照规则规定你应该看哪个历史版本,当前读一律跳过判定逻辑。
    2. 直击最新物理行 :执行器直接定位到聚簇索引 B+ 树的叶子节点,强制抓取该行数据在磁盘/内存中最新且已提交的真实物理版本。
    3. 瞬间加锁封锁 :在读取到最新版本的同时,执行器会立刻在聚簇索引的这行记录上加锁(如果条件允许,还会加上锁定前后范围的 Next-Key Lock)。如果是 UPDATE/DELETE 或 FOR 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 根本无法在中间插队修改。
相关推荐
IvorySQL7 小时前
PostgreSQL 日报|修复截断 zstd 备份检测(10 月 9 日)
数据库·postgresql
刘胡子大叔8 小时前
SQL 脚本的导入顺序
数据库·sql
代码什么用8 小时前
Spring对IoC的实现
数据库·spring
hz5678910 小时前
涉密视频会议设备配置指南:终端、音视频采集与配套设施选型
服务器·网络·数据库·安全·实时音视频·信息与通信·智能硬件
广州浮点FLOATLIC10 小时前
许可证服务器迁移后软件打不开:研发 IT 怎样定位连接问题
linux·服务器·数据库
程序员Sunday10 小时前
MySQL 为什么使用 B+ 树索引?把范围查询、回表和覆盖索引连起来
数据库·mysql
半杯咖啡半行码11 小时前
Qt开发实战:数据库、MV 模式、QProcess与串口通信全攻略
数据库·qt
小米里的大麦11 小时前
16 MySQL 事务
数据库·mysql
西柚小萌新11 小时前
【LLM&&AI应用开发 八股文】--4.3.Agent智能体(下)
java·开发语言·数据库
邓工说电12 小时前
智慧断路器安全吗?数据加密、离线保护与合规认证全解读
大数据·数据库·人工智能·智能断路器·炜晔科技