[小技巧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 创建时已经提交。

相关推荐
冰暮流星10 小时前
javascript之二重循环练习
开发语言·javascript·数据库
万岳科技系统开发10 小时前
食堂采购系统源码库存扣减算法与并发控制实现详解
java·前端·数据库·算法
冉冰学姐10 小时前
SSM智慧社区管理系统jby69(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·管理系统·智慧社区·ssm 框架
杨超越luckly10 小时前
HTML应用指南:利用GET请求获取中国500强企业名单,揭秘企业增长、分化与转型的新常态
前端·数据库·html·可视化·中国500强
斯普信专业组11 小时前
构建基于MCP的MySQL智能运维平台:从开源服务端到交互式AI助手
运维·mysql·开源·mcp
Elastic 中国社区官方博客11 小时前
Elasticsearch:Workflows 介绍 - 9.3
大数据·数据库·人工智能·elasticsearch·ai·全文检索
仍然.11 小时前
MYSQL--- 聚合查询,分组查询和联合查询
数据库
一 乐11 小时前
校园二手交易|基于springboot + vue校园二手交易系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端
啦啦啦_999911 小时前
Redis-0-业务逻辑
数据库·redis·缓存
自不量力的A同学11 小时前
Redisson 4.2.0 发布,官方推荐的 Redis 客户端
数据库·redis·缓存