1. 序幕:时空错乱的 1995
时间: 1995 年
地点: 瑞典 TcX 公司
场景: 我坐在笨重的显示器前,作为 MySQL 的总设计师。此时,我面临着数据库领域的“三座大山”。如果处理不好并发,用户会发现数据在玩“瞬移”:
-
脏读: 事务 A 还在草稿纸上改数据,还没定稿,事务 B 就偷看到了,结果 A 撕了草稿(回滚),B 读到了假数据。
-
不可重复读: 事务 A 正在查余额,事务 B 冲进来改了金额并提交。A 揉揉眼再看,钱变了。
-
幻读: 事务 A 统计员工 10 人,事务 B 悄悄入职了一个,A 一查变成 11 人。
大幕拉开,作为总工,我必须拿出一套方案。
2. 设计第一阶段:简单粗暴的“锁”
最初的想法很简单:谁改数据谁加锁,谁读数据也加锁。
写完代码我发现,这款产品不该叫 MySQL,该叫 “My蜗牛SQL”。
“我要一步一步往上爬,在最高点看哪个用户还没跑掉……”
总工笔记: 串行化(Serializable)确实能解决一切并发问题,但代价是极低的吞吐量。高并发下,大家都在排队。不行,我需要一套“读写不冲突”的方案。
3. 设计第二阶段:时空胶囊——MVCC
既然不能让读操作等待,那就给数据拍“快照”。
我决定在每一行数据的背后,安插两个秘密监控:
-
trx_id:标记这行数据最后是谁改的(事务 ID)。
-
roll_pointer:一个时空指针,指向旧版本。
当数据被修改时,我不删除旧数据,而是把旧值塞进 Undo Log,用指针串起来。
这就是版本链。 只要顺着指针往回找,我就能看到这行数据的“前世今生”。
4. 设计核心:谁该看哪个版本?(ReadView 规则引擎)
现在手里有一串历史版本,事务 B 进来时该看哪个?
我设计了一个**“规则引擎”——ReadView**。在查询的一刻,我给全系统活跃事务截个图:
- m_ids:还没提交的哥们(活跃事务列表)。
- min_id:最老的活跃哥们。
- max_id:系统即将分配的下一个新 ID。
- creator_trx_id: 创建者ID
总工制定的“可见性三原则”:
-
已发生的历史: trx_id < min_id,说明你开启前人家就提交了,随便看。
-
未发生的未来: trx_id >= max_id,说明那是你拍完快照后才产生的,不许看。
-
正在发生的混乱: 如果落在中间,看它在不在 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一定实现自己的梦想!