mysql||事务相关知识
「可重复读(RR)下 select 和 update 的处理逻辑不一样」。
1. 可重复读(RR)隔离级别下的“虚拟表”
- 在 InnoDB 的 RR 下,普通 一致性读(Consistent Read)(例如
SELECT ...
没有lock in share mode
或for update
)会基于事务启动时的 快照(snapshot)。
👉 所以事务 A 在第一次select id=5
时,如果这条记录不存在,就认为它在整个事务期间都不存在。
2. 那为什么还能更新?
这里要注意:
UPDATE
、DELETE
、SELECT ... FOR UPDATE
、SELECT ... LOCK IN SHARE MODE
属于 当前读(Current Read),它不是基于历史快照,而是要去读 最新版本,并且加锁。- 所以当事务 A 执行
UPDATE id=5 ...
时,它并不是在用“虚拟表”的快照结果,而是去 当前最新数据页 里找id=5
。
如果此时事务 B 已经插入了 id=5 并提交,那么事务 A 的 update 能找到它并更新。
3. 幻读的本质
- 一致性读(普通
select
):基于旧快照 → 看不到事务 B 新插入的id=5
。 - 当前读(update / select for update):直接去读最新记录 → 能看到事务 B 新插入的
id=5
。
这就是你说的“违和感”:事务 A 在普通 select 看不到,但在 update 时却能“凭空更新”了。
于是再次查询(如果用当前读),事务 A 能看到事务 B 的插入。
👉 这就是 幻读 —— 同一事务里,前后两次读的结果不一致。
4. 再强调一下:
- **虚拟表(快照)**只对一致性读有效。
- 当前读必须看最新记录,不会受虚拟表限制。
所以 “为什么还能看到插入的新数据”:
因为 update 属于当前读,不是走快照,而是直接访问最新数据页。
幻读发生情况:事务A查询第一次——>事务B插入数据——>事务A更新该条数据(记录的trx_id被改为事务A的id)——>事务A再次查询该数据(此时可见)
区间 [min_trx_id , max_trx_id )和m_ids关系是什么?
好问题 👍,很多人都会把 [min_trx_id , max_trx_id )
和 m_ids
搞混,其实两者的关系是 包含与过滤的关系。
1. 定义
- m_ids:Read View 生成时刻,系统里 真正活跃事务的 ID 列表。
- **min_trx_id **:
m_ids
中最小的事务 ID。 - **max_trx_id **:全局下一个即将分配的事务 ID(比当前最大事务 ID +1)。
所以:
[min_trx_id , max_trx_id )
这个区间是 可能活跃的事务 ID 的范围,但不是所有都活跃,
真正活跃的事务只在 m_ids 里。
2. 举个例子
假设当前系统事务 ID 已经分配到 25,下一个是 26:
- 活跃事务是 {20, 22, 24}
那么生成 Read View 时:
- m_ids = {20, 22, 24}
- min_trx_id = 20
- low_limit_id = 26
- 区间 [20, 26) = {20,21,22,23,24,25}
解释:
- 区间 [20,26) 表示这些事务号可能在 Read View 生成时还没提交。
- 但真正还没提交的只有 m_ids = {20,22,24}。
- 其他号(21,23,25)说明它们虽然号在范围内,但事务已经在 Read View 生成时提交了。
3. 可见性判断逻辑联系
对某条记录的 trx_id
:
-
如果
trx_id < min_trx_id
→ 肯定已提交,可见。 -
如果
trx_id >= max_trx_id
→ 肯定是未来事务,不可见。 -
如果
min_trx_id <= trx_id < max_trx_id
→ 看它是否在 m_ids:- 在 m_ids → 当时还活跃,没提交 → 不可见。
- 不在 m_ids → 当时已经提交 → 可见。
✅ 总结:
- 区间 [min_trx_id , max_trx_id ) = “候选集合”
- m_ids = “其中真正活跃的事务子集”
也就是说:m_ids ⊆ [min_trx_id , max_trx_id )。
undo log中每个版本都会记录修改的事务id,是吗?那所有事务的id从头到尾都是单调递增的吗?从建立这张表之后就一直是递增的,不会重置?
对的,你理解得很准确 👍,我帮你把 InnoDB 里的 事务 id(trx_id) 和 undo log 的关系说清楚:
1. undo log 里的 trx_id
-
InnoDB 采用 多版本并发控制(MVCC)。
-
每条记录有一个 隐藏列 trx_id,表示最近修改这条记录的事务 id。
-
当事务更新一条记录时:
- InnoDB 会把旧值写到 undo log,并且 undo log 里会记下生成这个版本的事务 id(也就是写入时的
trx_id
)。 - 这样,在做一致性读时,InnoDB 能根据
trx_id
判断这个版本对当前事务是否可见。
- InnoDB 会把旧值写到 undo log,并且 undo log 里会记下生成这个版本的事务 id(也就是写入时的
2. 事务 id 分配
- 每开启一个新事务,InnoDB 都会给它分配一个全局唯一的 事务 id。
- 这个 id 是 单调递增的,全局的(不是针对单张表)。
- 从 MySQL 实例启动之后就一直递增,不会因为某张表 truncate 或 drop 而重置。
3. 会不会重置?
- 不会重置,即使表删掉重建,trx_id 也继续从上次的最大值往上加。
- 因为事务 id 是 InnoDB 系统级别的,用来区分全局所有事务的先后顺序。
- 就算表不存在了,其他表里的事务也必须保证顺序正确,所以 trx_id 不能按表重置。
4. 溢出问题
- InnoDB 的事务 id 是一个 64 位无符号整数,上限非常大(2^64-1)。
- 理论上几乎不可能在实际场景下溢出(就算每秒产生一百万个事务,也能用几十万年 😅)。
✅ 总结:
- undo log 中每个版本都会带上生成它的事务 id。
- 所有事务 id 在实例范围内全局唯一,并且单调递增。
- 从实例启动后一直递增,不会因为表操作而重置。
- 几乎不会溢出。
那查找版本链的时候,查找到最后一个能看到的版本记录是吗?因为之前的版本肯定能看到,此时需要查找到最新能看到版本。
完全正确 👍,这就是 MVCC 版本链查找的核心逻辑。我给你详细梳理一下:
1. 版本链结构
- 每条记录(主记录)有一个 最新版本。
- 每次更新/删除都会生成一个 undo log 版本,并通过 回滚指针(roll_pointer) 链接到前一个版本。
- 这样就形成了一条 版本链:
最新版本 -> 上一个版本 -> 上上个版本 -> ... -> 初始版本
2. 一致性读(select)的查找逻辑
当事务 A 执行一致性读时(RR 隔离级别):
- 从最新版本开始向 undo log 方向回溯。
- 对每个版本,判断它对事务 A 是否可见(用 trx_id + Read View 的 m_ids/up_limit_id/low_limit_id 规则)。
- 找到 第一个可见版本就返回。
⚠️ 注意:
- 找到的版本不一定是最新的版本号(trx_id 最大的),而是 对当前事务可见的最新版本。
- 之前的版本肯定不可见(要么已经被覆盖,要么事务在生成 Read View 时还没提交)。
3. 为什么不直接返回最新版本?
因为 最新版本可能是由另一个事务生成且未提交,如果直接返回最新版本,会违反事务隔离。
- 所以必须沿版本链回溯,确保返回的版本 既最新又对当前事务可见。
✅ 总结:
- 版本链按时间倒序存储(最新 -> 最旧)。
- 一致性读沿链回溯,判断每个版本可见性。
- 返回 第一个可见版本,就是事务当前能看到的最新数据。
undolog 日志进行回滚,那么回滚之后的trx_id设置之前的?还是现在执行回滚的事务id?
好问题 👍,这里容易搞混:回滚(rollback)的时候,记录的 trx_id
究竟算谁的?
1. 记录的 trx_id
是怎么来的
- InnoDB 里,每条记录都有一个隐藏字段
trx_id
,表示最后一次修改它的事务 id。 - 当事务 执行 DML(INSERT/UPDATE/DELETE) 时,这个
trx_id
会被设置为当前事务的 id。 - 同时,InnoDB 把旧值写入 undo log,undo log 里也记录生成它的事务 id。
2. 回滚时的处理
-
回滚时不会新写一个
trx_id
,因为回滚的逻辑就是“恢复旧版本”。 -
InnoDB 做的操作是:
- 顺着 undo log 找到之前的版本;
- 把记录恢复为该版本的数据;
- 并且把记录的
trx_id
恢复成当时生成旧版本的事务 id(即 原始的 trx_id)。
3. 为什么不能用“现在执行回滚的事务 id”?
- 如果把回滚动作当成一次新更新,给记录设置“现在的事务 id”,就会导致 MVCC 逻辑出错。
- 因为这样做相当于告诉别人“这条记录是新事务改的”,会影响可见性判断。
- 实际上,回滚只是“撤销”,应该恢复到 回滚前就已经存在的版本,而不是制造一个“新版本”。
4. 举个例子
-
事务 T1(trx_id=100):
update user set age=20 where id=1;
trx_id=100
写到最新版本。- 旧值写入 undo log,带着原来的 trx_id(比如 90)。
-
T1 rollback:
- InnoDB 用 undo log 把记录恢复成
age=10, trx_id=90
。 - 而不是
age=10, trx_id=101(rollback)
。
- InnoDB 用 undo log 把记录恢复成
这样一来,其他事务再判断可见性时,就能正确认为“这条记录是事务 90 最后修改的”。
✅ 结论:
- 回滚后,记录的
trx_id
会恢复成旧版本的事务 id(原始的 trx_id)。 - Undo log 用来回溯,回滚不是产生新事务,而是“消除”当前事务的修改痕迹。