[小技巧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可见

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

规则 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 创建时已经提交。

相关推荐
偷星星的贼112 小时前
数据分析与科学计算
jvm·数据库·python
Suchadar3 小时前
数据库DATABSE——sql server
数据库
檀越剑指大厂3 小时前
迁移之路的隐形陷阱:破解Oracle数据库国产化替代的核心痛点与策略
数据库·oracle
wWYy.4 小时前
详解redis(1)
数据库·redis·缓存
todoitbo4 小时前
Oracle 迁移到 KingbaseES:从问题词到成本的技术拆解
数据库·oracle·kingbasees
Mr.徐大人ゞ4 小时前
生产可用的 MySQL8 一键安装脚本和一键巡检脚本
mysql
会游泳的石头4 小时前
Java 异步事务完成后的监听器:原理、实现与应用场景
java·开发语言·数据库
数智工坊4 小时前
【操作系统-IO调度】
java·服务器·数据库
星梦清河4 小时前
MySQL—分组函数
数据库·mysql