近日,一篇题为“PostgreSQL's MVCC is bad. So is everyone else's”的技术博文在数据库圈引发热议。作者以犀利的笔锋指出,PostgreSQL引以为傲的多版本并发控制(MVCC)机制存在严重缺陷,而更令人沮丧的是,这一困境并非PostgreSQL独有——几乎所有主流数据库的MVCC实现都“半斤八两”。此言一出,迅速激起了数据库架构师、DBA以及开源社区的分歧与反思。
MVCC:并发控制的双刃剑
MVCC(Multi-Version Concurrency Control)是现代关系数据库的基石。它允许多个事务同时读写数据而不互相阻塞,通过为每个事务维护数据行的“快照”来保证隔离性。PostgreSQL正是MVCC的典型代表:当一行数据被更新时,旧版本不会立即删除,而是保留为“死元组”,供正在读取的旧事务使用。
然而,这种设计在带来高并发能力的同时,也埋下了隐患。批评者指出,PostgreSQL的MVCC实现存在三大痛点:元组膨胀、垃圾回收(Vacuum)开销以及事务ID回卷风险。其中,元组膨胀是最直观的问题——频繁的更新操作会导致表内堆积大量死元组,不仅占用磁盘空间,更会拖慢索引扫描和顺序扫描的性能。为了清理这些垃圾,PostgreSQL必须依赖定期或自动的VACUUM操作,而VACUUM本身又会带来额外的I/O和锁竞争,在写密集场景下往往捉襟见肘。
文章进一步指出,PostgreSQL的事务ID(XID)是一个32位整数,一旦回卷将导致数据可见性错乱,虽然后续版本引入了冻结机制,但管理复杂度并未降低。这位作者直言:“每次看到生产环境因VACUUM不及时导致性能雪崩,我都想对着MVCC设计者大喊‘你们到底在想什么?’”
同行皆困:MySQL、Oracle亦非净土
如果问题仅存于PostgreSQL,那么换用其他数据库便可解决。但文章毫不留情地拆穿了这一幻想:“MySQL InnoDB的MVCC使用了回滚段(undo log)来存储旧版本,虽然避免了元组膨胀,却带来了回滚段空间管理和长事务锁定旧版本的问题。Oracle的MVCC则依赖撤销表空间,当长事务存在时,同样会产生ORA-01555快照过旧错误。”
换言之,所有MVCC实现都面临一个根本性矛盾:保留旧版本以服务并发读,但保留旧版本会导致空间膨胀或回收延迟。MySQL选择将旧版本写入独立的undo表空间,通过purge线程异步清理;Oracle则采用更加精细的undo保留策略,但复杂的长事务依旧可能耗尽undo空间。作者总结:“没有一种MVCC方案能在所有场景下兼顾性能、空间和可管理性。PostgreSQL的糟糕不是个案,而是整个行业在设计取舍上的集体困境。”
争议与回应:MVCC是否真的无可救药?
这篇博文引发了不少技术同行的讨论。PostgreSQL核心贡献者之一在社交平台回应称,MVCC的“坏”并非设计缺陷,而是“经典权衡”。他指出,PostgreSQL的元组级版本控制提供了极高的并发合规性,并且社区正在通过增量VACUUM、Zheap存储引擎(旨在减少膨胀)以及更高效的清理策略来缓解问题。而MySQL阵营也有人反驳,InnoDB的回滚段机制在大多数在线交易处理(OLTP)场景下表现稳定,长事务导致的ORA-01555在Oracle中则可通过合理的UNDO_RETENTION设置规避。
更有观点认为,MVCC的“坏”是相对应用场景而言的。对于读多写少的分析型负载,PostgreSQL的膨胀并不致命;对于高并发写入的金融系统,Oracle的undo管理经过数十年打磨已然成熟。真正的问题在于,开发者往往在选型时只看MVCC的表面优势,却忽略了其背后的运维成本。
未来之路:放弃MVCC还是改造MVCC?
面对MVCC的固有困局,业界开始探索替代方案。一方面,无锁数据结构(如Calvin、FaRM)和OCC(乐观并发控制) 在某些分布式数据库中获得了应用;另一方面,谷歌Spanner、CockroachDB等分布式数据库则采用混合逻辑时钟+两阶段锁,试图在一致性上超越传统MVCC。但遗憾的是,这些方案要么牺牲了部分功能(如外键、完整SQL支持),要么引入了复杂的时钟同步难题。
或许正如那篇博文最后所言:“MVCC不是完美的,但它依然是我们在关系数据库领域能找到的最不坏的并发控制方案。与其抱怨,不如接受它的缺点,并找到合适的工具来弥补——比如合理的监控、自动化运维,以及选型时的清醒认知。”
这场关于MVCC的争论注定不会有标准答案。对于DBA和架构师而言,最重要的或许不是纠结“谁的MVCC更好”,而是理解每种实现背后的代价,并为自己系统的负载特点做出最合适的选择。毕竟,“糟糕”是相对的,而“适用”才是永恒的。