【穿越1995】如果让我来设计 MySQL 的事务架构

9 阅读4分钟

1. 序幕:时空错乱的 1995

时间: 1995 年

地点: 瑞典 TcX 公司

场景: 我坐在笨重的显示器前,作为 MySQL 的总设计师。此时,我面临着数据库领域的“三座大山”。如果处理不好并发,用户会发现数据在玩“瞬移”:

  • 脏读: 事务 A 还在草稿纸上改数据,还没定稿,事务 B 就偷看到了,结果 A 撕了草稿(回滚),B 读到了假数据。

  • 不可重复读: 事务 A 正在查余额,事务 B 冲进来改了金额并提交。A 揉揉眼再看,钱变了。

  • 幻读: 事务 A 统计员工 10 人,事务 B 悄悄入职了一个,A 一查变成 11 人。

大幕拉开,作为总工,我必须拿出一套方案。

2. 设计第一阶段:简单粗暴的“锁”

最初的想法很简单:谁改数据谁加锁,谁读数据也加锁。

写完代码我发现,这款产品不该叫 MySQL,该叫 “My蜗牛SQL”。

“我要一步一步往上爬,在最高点看哪个用户还没跑掉……”

总工笔记: 串行化(Serializable)确实能解决一切并发问题,但代价是极低的吞吐量。高并发下,大家都在排队。不行,我需要一套“读写不冲突”的方案。

3. 设计第二阶段:时空胶囊——MVCC

既然不能让读操作等待,那就给数据拍“快照”。

我决定在每一行数据的背后,安插两个秘密监控:

  1. trx_id:标记这行数据最后是谁改的(事务 ID)。

  2. roll_pointer:一个时空指针,指向旧版本。

当数据被修改时,我不删除旧数据,而是把旧值塞进 Undo Log,用指针串起来。

这就是版本链。 只要顺着指针往回找,我就能看到这行数据的“前世今生”。

4. 设计核心:谁该看哪个版本?(ReadView 规则引擎)

现在手里有一串历史版本,事务 B 进来时该看哪个?

我设计了一个**“规则引擎”——ReadView**。在查询的一刻,我给全系统活跃事务截个图:

  • m_ids:还没提交的哥们(活跃事务列表)。
  • min_id:最老的活跃哥们。
  • max_id:系统即将分配的下一个新 ID。
  • creator_trx_id: 创建者ID

总工制定的“可见性三原则”:

  1. 已发生的历史: trx_id < min_id,说明你开启前人家就提交了,随便看。

  2. 未发生的未来: trx_id >= max_id,说明那是你拍完快照后才产生的,不许看。

  3. 正在发生的混乱: 如果落在中间,看它在不在 m_ids 列表里。在,说明还没提交,不能看;不在,说明已提交,可以看。

公式如下:

一看自己一定看:trx_id == creator_trx_id
二看过往放心看:trx_id < min_id ,放心大胆看
三看未来不可看:trx_id >= max_id 时间还没到,看啥看
列表查无方可看:trx_id not in m_ids

5. 档位调节:RC 与 RR 的本质

为了应对不同场景,我设计了两个“档位”:

• RC 档(读已提交): 每次 SELECT 都重新拍一张 ReadView。

• 评价: 灵活,但因为快照一直在变,依然会看到别人新提交的数据,解决不了不可重复读。

• RR 档(可重复读): 整个事务期间,只在第一次查询时拍快照,后面一直复用。

• 评价: 稳重。只要我不关事务,我眼里的世界永远停留在开启那一刻。这就是 MVCC 的完全体。

6. 终极补丁:不仅仅是快照,还有“间隙”

MVCC 完美解决了“快照读”的幻读。但如果事务 A 正在执行 Update(当前读),它必须看到最新的数据,这时候快照就失效了,幻读可能再次发生。

总工的终极补丁:Next-Key Lock(间隙锁)。

在 RR 级别下,当我们通过索引查询数据时,我不仅锁住记录本身,还锁住记录之间的“缝隙”。

“在我地盘这 你就得听我的!插队是不可能的!”

7. 结语:设计师的自白

设计 MySQL 的事务系统,本质上是在“一致性”与“性能” 之间跳舞。

MVCC 是空间换时间的艺术,日志是顺序 IO 欺骗物理定律的魔法。 祝大家2026一定实现自己的梦想!