[小技巧41]InnoDB 如何判断一行数据是否可见?MVCC 可见性机制深度解析

一、Read View 是什么

当一个事务执行第一个快照读(如 SELECT) 时,InnoDB 会为它创建一个 Read View(读视图)。这个 Read View 包含以下关键信息:

字段 含义
m_ids 创建 Read View 时,所有活跃(未提交)事务的 ID 列表
min_trx_id m_ids 中的最小值(即最早开始但未提交的事务 ID)
max_trx_id 创建 Read View 时,下一个将要分配的事务 ID(即当前最大已分配 ID + 1)
creator_trx_id 当前事务自己的 ID(如果是只读事务,则为 0)

举个例子:

  • 当前已分配事务 ID:100, 101, 102
  • 活跃事务(未提交):101, 102
  • 新事务 T(ID=103)执行 SELECT → 创建 Read View:
    • m_ids = [101, 102]
    • min_trx_id = 101
    • max_trx_id = 104(因为 103 已分配,下一个是 104)
    • creator_trx_id = 103

1. 什么是只读事务

正确理解:"只读事务" ≠ "执行了 SELECT 的事务"

常见误解:

"我写了一个 SELECT 语句,所以这是个只读事务,creator_trx_id = 0。"

真相:

在 InnoDB 中,是否为"只读事务"不是由是否写了 SELECT 决定的,而是由事务是否被显式/隐式标记为"只读"决定的。

creator_trx_id = 0 仅当满足以下条件之一:

情况一:使用 START TRANSACTION READ ONLY

sql 复制代码
START TRANSACTION READ ONLY;
SELECT * FROM users;  -- 这才是真正的"只读事务"
-- 此时 Read View 的 creator_trx_id = 0

情况二:设置会话为只读模式

sql 复制代码
SET SESSION TRANSACTION READ ONLY;
SELECT * FROM users;  -- 自动以只读事务运行

情况三:MySQL 自动优化(5.6+)

  • 如果一个事务从开始到结束都只执行 SELECT(无任何写操作),
  • 并且没有显式开启事务(即 autocommit=1 下的单条 SELECT),
  • 那么 InnoDB 可能不会分配事务 ID,也不会创建完整的 Read View,
  • 而是直接读取最新已提交的数据 (行为类似 READ COMMITTED)。

但注意:这种情况下甚至可能不创建 Read View,所以谈不上 creator_trx_id = 0,而是压根没事务上下文。

2. 普通 SELECT 的真实情况

情况一:默认 autocommit=1

sql 复制代码
SELECT * FROM users WHERE id = 1;
  • 这是一个自动提交的事务(autocommit transaction)
  • InnoDB 会分配一个真实的事务 ID(比如 12345)
  • 执行 SELECT 时会创建 Read View
  • creator_trx_id = 12345(不是 0!)

验证方法:

sql 复制代码
SELECT trx_id, trx_state, trx_started
FROM information_schema.innodb_trx
WHERE trx_mysql_thread_id = CONNECTION_ID();

即使只执行 SELECT,你也能看到一个活跃事务记录(短暂存在)。
注意事项:

autocommit=ON 下的 SELECT 并不会进入 innodb_trx 表!
关键知识点:

在 MySQL 中,只有当事务处于"活动状态(RUNNING)"且未提交时,才会出现在 information_schema.innodb_trx 中。

而普通 SELECT 在 autocommit=ON 模式下是自动提交的,执行完立刻提交,事务生命周期极短,可能在查询 innodb_trx之前就已经结束了。

情况二:如何让 SELECT 的事务"可见"?

你需要一个 持续运行的事务 ,而不是瞬间完成的 SELECT。

方法一:使用 BEGIN 显式开启事务(推荐)

Step 1:在 Session A(主会话)中

sql 复制代码
-- 开启事务(此时会分配 trx_id)
BEGIN;

-- 执行一个长查询(模拟长时间运行)
SELECT SLEEP(10), id FROM test_version WHERE id = 1;

这个查询会卡住 10 秒,事务一直存在。

Step 2:在 Session B(监控会话)中立即执行

sql 复制代码
SELECT
    trx_id,
    trx_state,
    trx_started,
    trx_mysql_thread_id,
    trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started DESC;

会看到类似结果:

trx_id trx_state trx_started trx_mysql_thread_id trx_query
42185673 RUNNING 2026-01-21 17:30:05 123 SELECT SLEEP(10), id FROM test_version WHERE id = 1

成功捕获到事务!

方法二:使用 SET autocommit = OFF + SELECT

Step 1:关闭自动提交

sql 复制代码
SET autocommit = OFF;

-- 执行 SELECT
SELECT SLEEP(10), id FROM test_version WHERE id = 1;

Step 2:在另一个会话中查看 innodb_trx

sql 复制代码
SELECT * FROM information_schema.innodb_trx;

同样可以看到事务记录。

二、可见性判断规则

假设当前事务 T 要读取某一行数据,该行的 DB_TRX_ID = X。

InnoDB 按以下顺序判断是否可见:

规则 1:如果 X == creator_trx_id → 可见

  • 含义:这行是当前事务自己修改的(还没提交,但自己能看到)
  • 场景 :事务内先 UPDATE 再 SELECT,能读到自己改的数据

规则 2:如果 X < min_trx_id → 可见

  • 含义 :这行是在当前 Read View 创建之前就已提交的事务修改的
  • 为什么?
    因为 min_trx_id 是最早未提交事务的 ID,
    所有 < min_trx_id 的事务必然已经提交 (否则会在 m_ids 中)

规则 3:如果 X >= max_trx_id → 不可见

  • 含义 :这行是由当前 Read View 创建之后才开始的事务修改的
  • 为什么?
    max_trx_id 是"下一个将分配的事务 ID",
    所以 X >= max_trx_id 表示这个事务在 Read View 创建之后才出现

规则 4:如果 min_trx_id <= X < max_trx_id → 需查 m_ids

  • 这表示 X 是 Read View 创建时已经存在的事务
  • 再判断 :
    • 如果 X ∈ m_ids(X 在活跃事务列表中)→ 不可见(因为对方还没提交)
    • 如果 X ∉ m_ids(X 不在活跃列表中)→ 可见(说明 X 已经提交了)

如果当前版本不可见,就通过 DB_ROLL_PTR 找到 Undo Log 中的上一个版本 ,

然后重复上述 4 条规则,直到:

  • 找到可见版本 → 返回该版本数据
  • 遍历完所有旧版本(到达初始插入版本)仍不可见 → 返回空(该行对该事务不存在)

三、示例

场景设定

  • 初始数据:id=1, name="Tom"(由事务 50 插入)
  • 事务 100:UPDATE name="Jerry"
  • 事务 101:UPDATE name="Alice"(未提交)
  • 事务 102:执行 SELECT name FROM t WHERE id=1;

此时事务 102 的 Read View:

  • m_ids = [101](101 未提交)
  • min_trx_id = 101
  • max_trx_id = 103
  • creator_trx_id = 102

数据版本链(从新到旧):

  1. 最新版 :name="Alice", DB_TRX_ID=101
  2. 中间版 :name="Jerry", DB_TRX_ID=100
  3. 初始版 :name="Tom", DB_TRX_ID=50

事务 102 的可见性判断过程:

  1. 检查最新版(DB_TRX_ID=101) :
    • 101 ∈ m_ids → 不可见 → 继续找旧版本
  2. 检查中间版(DB_TRX_ID=100) :
    • 100 < min_trx_id (101) → 可见!
  3. 返回结果 :name="Jerry"

即使事务 101 修改了数据,但因为它未提交,事务 102 看不到 "Alice",而是看到上一个已提交版本 "Jerry"。

四、不同隔离级别的影响

REPEATABLE READ(RR)

  • Read View 在事务第一次 SELECT 时创建,并复用整个事务
  • 所以多次 SELECT 看到同一快照

READ COMMITTED(RC)

  • 每次 SELECT 都创建新的 Read View
  • 所以能读到其他事务最新已提交的数据

注意:可见性判断规则本身不变,变的只是 Read View 的创建时机!

五、总结

一行数据对当前事务可见,当且仅当:
它的 DB_TRX_ID 对应的事务,在当前 Read View 创建时已经提交。

相关推荐
ClouGence1 小时前
数据库变更审批困局,被这个开源工具解决了
数据库·开源
TDengine (老段)1 小时前
TDengine IDMP 十大实用功能(9)
大数据·数据库·人工智能·物联网·时序数据库·tdengine·idmp
starrocks_stella2 小时前
EMR Serverless StarRocks Object Table:用 SQL 连接 OSS 文件与多模态 AI
数据库
这个DBA有点耶2 小时前
以为自增ID只是有空洞?聊聊双主架构下无声的“数据覆盖”
数据库·mysql·dba
陈布衣3 小时前
MySQL(1)_MySQL 5.7.21 Linux 安装笔记
linux·笔记·mysql
钱栈up3 小时前
内网全断,怎么核实高炉和烧结系统的数据库对接信息
数据库
weavecodes3 小时前
双层缓存与权限实时生效:织码在线教育系统的 Caffeine + Redis 设计
数据库
bksczm4 小时前
InnoDB 页结构详解:页类型、页头页尾、数据行与页目录
数据库·mysql
bksczm4 小时前
InnoDB 行格式详解:DYNAMIC / COMPACT / COMPRESSED / REDUNDANT
java·开发语言·数据库
余槐i5 小时前
MySQL 8.0 联合索引走错:为什么加了索引查询反而从 200ms 变成 8s
java·spring boot·mysql·docker·性能优化