本文最初发布于 Arjun Narayan 的博客,经原作者授权由聊聊架构翻译并分享。
我花了相当多的精力来理解数据库的事务,最近也与 Justin Jaffray 一起研究了该话题。本文总结了我们在这期间的所学。
数据库及分布式基础架构的相关社区花了数十年来理解事务的本质这一反复出现的问题。在阅读相关文献时需要读者理解其中的背景,不然就会被其中诸多矛盾的术语及认知所迷惑。为了澄清这点,下文阐述了数据库事务相关论述的演变过程。
在一开始(1990 年?),数据库连接还极其简单粗暴。在丛林法则下,没有行业标准的数据库与使用它们的程序员们必须处理许多异常现象。为了解决这些问题,ANSI SQL-92 标准首次尝试统一定义事务隔离级别。该标准选择了如下方式:
1. 存在一些已知的异常现象,即脏读、不可重复读和幻读。
2. 可以利用一些锁策略来防止部分甚至全部上述的异常现象。它们被称为“读未提交”(不进行隔离)、“读已提交”(没有脏读)、可重复读(没有脏读及不可重复读)。
3. 最终的隔离级别避免了全部三种异常现象。它被称为可串行化(serializable),但是这和字面意思有些出入。暂且可以认为这是整个标准中的一个缺陷。
可串行化的历史相当久远。其定义可追溯到 20 世纪 60 年代,并通过 _ 事务中的历史记录 _ 来定义。 引用 Christos Papadimitriou 在一篇早期讨论事务的论文中所述:
可串行化是指,一系列原子性的用户更新、获取操作,且其全局的效果就像用户轮流,按一定顺序,独立执行各自的事务一样。
阅读该论文有一定难度,所以简单看来, Herlihy-Wing 已很好地解释了可串行化。但是其中的思想非常简单:每一个事务就像只发生在某一瞬间,而且它们之间存在一定的顺序(但并没有说其中的顺序对用户甚至事务本身需要明确!只需要事后可回溯)。
几年之后,微软数据库团队写了一篇名为《批判ANSI SQL 隔离级别》的论文,其中指出ANSI SQL 中定义的可串行化并不是真正的可串行化!ANSI 认为,只要消除了脏读、不可重复读和幻读,就是可串行化的。这一观点是不正确的。事实证明,如果消除这三种异常现象,得到的是另一种新的隔离级别,他们称之为快照隔离。
该论文另一个有价值的贡献是指出了,通过“是否存在特定的异常现象”来界定隔离级别并不是理想的方法。接着来讨论下事务的历史记录,这段时间的相关文献都是通过定义_ 特定的锁策略_,并证明其是否会产生异常现象来定义隔离级别的。这会带来一些困惑,因为同一隔离级别可以通过多种锁策略来实现,但其隔离级别的名称又与策略是相关联的,所以同一隔离级别会存在多个名称。
这时出现了一位大神来清理这一乱局:Atul Adya 与他的博士论文《弱一致性:一个通用理论和分布式事务的的乐观实现》。Adya 从追溯事务的历史记录开始。其策略基本上是“根据一些事务的历史记录,可以研究其中变量的转变图”,然后在图中标识出一些形状(其实大部分记录的是不同的周期),并表示“这其中存在问题”。然后,将这些问题的矛头指回到ANSI 的定义上,并可以追溯到ANSI 隔离级别的一些基础的数学定义中。该论文的巨大价值在于,终于可以精确且独立于具体实现锁策略地定义隔离级别!
除了给出四个ANSI 级别的定义之外,他还指出了其它更微妙的异常情况,例如 G2 ,又称“反依赖循环”,该情况很难被察觉,其存在于快照隔离和可串行化之间的未知领域中。
Adya 的论文在数据库领域是开创性的。这是第一次连贯地通过事务中变量的转变图来给出隔离级别的数学定义。当年是 1999 年,基础设施还不完善。
接下来出现了下一个问题:当我们对事务有了更多的理解后,我们应该如何处理现存的那些混乱的数据库(即 Oracle 和 Postgres,MySQL 至今仍然只是一个玩具产品)?它们声称实现了可串行化,但实际上只是实现了快照隔离,因为它们是按 ANSI 定义来构建的。
Alan Fekete 等人在 2005 年提出了一个好办法,他们称之为“让快照隔离可串行化”。基本上就是通过一个普通的快照隔离数据库,并在 SQL 语句中进行一些校验,以确保其可串行化。他们使用 TPC-C 作为运行示例,该系统已被明确设计为始终可串行化,即使在快照隔离数据库上运行时也是如此。一般应用开发程序员,即使被迫使用的是快照隔离数据库,也可以通过这个技巧得到可串行化的隔离级别。
有了这个好想法,Fekete 在 2008 年的一篇名为《快照隔离数据库上的可串行化隔离》的论文中对这项工作进行了扩展。论文基本上是以该想法为基础,并将那些检查代码从应用程序代码中迁移到了数据库上。因此,用户不必废除已有的事务处理引擎,只需要依赖额外的校验,从而让数据库可串行化。该技术被称为SSI(可串行化快照隔离),由于采用了两个技术定义,将它们结合起来重新命名一个全新的定义并不奇怪。
在2012 年,就有开发者决定将这个好想法在 PostgreSQL 上实现,从而让 Postgres 最终拥有可串行化隔离级别。注意,这并不是默认的 postgres 模式,因为 ANSI 认为快照隔离(但仍被称为可串行化)已经足够了。需要指出ANSI 的定义并没有被修复。
该策略在一篇名为《批判快照隔离》的论文中得到了延续。文中指出,SSI 引导算法可以被简化。原始的SSI 算法检查双写冲突,为了扩展,Yabandeh 建议改为检测读写冲突。虽然这需要内存中的数据结构记录每次读取的最新时间戳的信息,但对于一些日常的负载来说,其具有更好的_ 并发控制_ 能力,因为它并不会中止读取操作,而只会中止写入操作。
也正是从此,大家开始意识到他们需要关心可串行性,一部分原因是 Jeff Dean 认为其非常重要。与此同时,持续呼吁该观点将近 20 年的 Michael Stonebraker 表示很无奈,大家显然只在 Google 明星工程师声明某件事的重要性时才会关心它。
当下学者们仍有很多焦虑,因为现在我们的处境非常尴尬
1. 根据理论,可串行化是唯一的选择
2. 很多人使用非串行化的数据库,但基本上并没有遇到问题?
3. 因此,我们是否在浪费时间?可串行化是否真的有必要?我们到底需要的是什么?
Peter Bailis 在 2014 年的一篇博文中总结了这一点。他说:
尽管存在诸多的弱隔离,但我还没遇到任何数据库架构师、研究人员或用户,可以解释何时以及为什么(这可能更重要)“读已提交”的隔离级别足以满足应用的正确执行。众所周知,这些弱隔离模型代表了“实践中的 ACID”,但我认为我们并没有真正了解为何大量的应用程序看上去(!?)可以在该级别下正确运行。
反面观点多认为:那样的应用并不总能保证正常运行,当你发现问题时,就可能已经太迟了,所以不要尝试。该论点也来之不易,在大家使用 NoSQL 数据库大约十年后,该观点才开始被关注。
查看英文原文: A History of Transaction Histories
感谢郭蕾对本文的审校。
评论