JEPSEN原文

MySQL 是世界上最流行的开源关系型数据库,然而流行并不意味着先进,甚至流行的东西也很可能是错误的。 JEPSEN 对 MySQL 的隔离等级评测彻底捅穿了这层窗户纸 —— 在数据库最基本的正确性上,MySQL的表现一塌糊涂。 MySQL 默认的RR隔离等级实际上非常垃圾:可能会出现多种本不该出现的异常,造成各种数据一致性与正确性的问题。 其文档声称自己实现了可重复读隔离等级,但实际提供的正确性保证却弱的多,仅稍强于 RC,这样的行为极具误导性。 考虑到唯一能解决这些问题的SR/可串行化隔离等级难以生产实用,我们只能认为 MySQL 在正确性上确实有着巨大的局限性,应当谨慎用于数据一致性/正确性要求较高的关键场景。

JEPSEN 是分布式系统领域最为权威的测试框架,他们最近发布了针对 MySQL 最新的 8.0.34 版本重新进行了隔离等级的测试。 与 2014 年DDIA作者 Martin Kleppmann 的发起的 Hermitage 项目测试结果相比,MySQL的隔离等级问题并未改善。

JEPSEN 的测试进一步指出,MySQL 默认的隔离等级 —— RR/可重复读(REPEATABLE READ)不仅出现了 G2-item、G-single 和丢失更新异常, 甚至还违反了内部一致性和单调原子视图(Monotonic Atomic View)—— 即事务可以观察到另一个事务的结果,再次尝试观察后却又无法看到同样的结果。 这使得 MySQL RR隔离级别的实际水平从 MAV 进一步降级到仅比 RC 略好一点的程度。 而唯一能解决这些问题的SR/可串行化隔离等级,又因其糟糕的表现难以生产实用,甚至都不是MySQL手册/社区鼓励的行为。 此外,JEPSEN 甚至在 AWS RDS MySQL 集群的 可串行化 隔离等级上观察到了违反串行要求的异常表现。

无独有偶,腾讯的李海翔腾教授在其《第三代分布式数据库》论文中对包括 MySQL 在内的多种数据库的实际隔离等级进行了测评,在一致性上,对 MySQL 的评语是 “一无是处”。

mysql-levels.png

MySQL RR隔离等级表现仅略微强于 RC,无法避免多种并发异常

mysql-consistency.png

带有 “A” 标记的黄色方块代表会出现并发异常

最近,JEPSEN 发表了一篇文章,测试了最新的 MySQL 8.0.34 的一致性表现,结果摘要如下:

  • MySQL 的默认RR隔离等级(可重复读)不称职,会出现三种并发异常:G2-item, G-single, P4.
  • MySQL 的RR隔离等级实际水准,在 2014 年的 Hermitage 项目中被评为 MAV (单调原子视图)
  • JEPSEN 指出 MySQL 的RR隔离等级实际上比 MAV 还要垃圾,违反了内部一致性,只能说比 RC 略强一点。
  • MySQL 比默认RR等级更高的SR隔离等级(可串行化)难以生产实用,且在 AWS RDS MySQL 测试此隔离等级上经常出现不和要求的异常。

与 2014 年相比,

MySQL 是一种流行的关系型数据库。我们重新审视了 Kleppmann 在 2014 年的 Hermitage 研究,并确认 MySQL 的 Repeatable Read 仍然允许 G2-item、G-single 和丢失更新的发生。 通过使用我们的事务一致性检查器 Elle,我们展示了 MySQL Repeatable Read 同样违反了内部一致性。 此外,它还违反了 Monotonic Atomic View:事务可以观察到另一个事务的部分效果,然后后来却无法观察到同一事务的其他效果。 我们展示了对 ANSI SQL 对于 Repeatable Read 的要求的违反情况。 我们认为 MySQL 的 Repeatable Read 比 Read Committed 稍强。 作为额外内容,我们展示了 AWS RDS MySQL 集群常规地违反了可串行性。 这项工作是独立完成的,没有获得报酬,并且是按照 Jepsen 道德政策进行的。

https://jepsen.io/analyses/mysql-8.0.34 这项工作是独立进行的,无报酬,并按照 Jepsen 道德政策进行。 结果包括:

  • MySQL 可重复读取违反了内部一致性并违反了单调原子视图
  • AWS RDS MySQL 集群经常违反可串行性
  • MySQL 的 binlog 复制显得很脆弱。我们在本地 Jepsen 测试中观察到许多复制停止的神秘场景。 该文重温了 关于 MySQL 隔离级别的工作,发现 8.0.34 中存在令人惊讶的行为。
  • MySQL 的重复读取(REPEATABLE READ)不仅表现出 G2-item、G-single 和丢失更新,还违反了内部一致性和单调原子视图(Monotonic Atomic View)。
  • 它既不符合 Adya 的可重复读取标准,也不符合含糊不清的 ANSI SQL 定义。
  • 还发现 AWS RDS MySQL 集群经常在 SERIALIZABLE 隔离级别上违反串行化原则。

MySQL is a popular relational database. We revisit Kleppmann’s 2014 Hermitage and confirm that MySQL’s Repeatable Read still allows G2-item, G-single, and lost update. Using our transaction consistency checker Elle, we show that MySQL Repeatable Read also violates internal consistency. Furthermore, it violates Monotonic Atomic View: transactions can observe some of another transaction’s effects, then later fail to observe other effects of that same transaction. We demonstrate violations of ANSI SQL’s requirements for Repeatable Read. We believe MySQL Repeatable Read is somewhat stronger than Read Committed. As a lagniappe, we show that AWS RDS MySQL clusters routinely violate Serializability. This work was performed independently without compensation, and conducted in accordance with the Jepsen ethics policy.

腾讯李海翔老师的《第三代分布式数据库》系列文章,特别是一致性八仙图,对多种数据库的实际隔离等级进行了测评。

0x01 引子

大多数数据库都会同时被多个客户端访问。如果它们各自读写数据库的不同部分,这是没有问题的,但是如果它们访问相同的数据库记录,则可能会遇到并发异常

下图是一个简单的并发异常案例:两个客户端同时在数据库中增长一个计数器。(假设数据库中没有自增操作)每个客户端需要读取计数器的当前值,加1再回写新值。因为有两次增长操作,计数器应该从42增至44;但由于并发异常,实际上只增长至43。

conncurrent-race-condition.png

图 两个客户之间的竞争状态同时递增计数器

事务ACID特性中的I,即隔离性(Isolation)就是为了解决这种问题。隔离性意味着,同时执行的事务是相互隔离的:它们不能相互踩踏。传统的数据库教科书将隔离性形式化为可串行化(Serializability),这意味着每个事务可以假装它是唯一在整个数据库上运行的事务。数据库确保当事务已经提交时,结果与它们按顺序运行(一个接一个)是一样的,尽管实际上它们可能是并发运行的。

如果两个事务不触及相同的数据,它们可以安全地并行(parallel)运行,因为两者都不依赖于另一个。当一个事务读取由另一个事务同时进行修改的数据时,或者当两个事务试图同时修改相同的数据时,并发问题(竞争条件)才会出现。只读事务之间不会有问题,但只要至少一个事务涉及到写操作,就有可能出现冲突,或曰:并发异常。

并发异常很难通过测试找出来,因为这样的错误只有在特殊时机下才会触发。这样的时机可能很少,通常很难重现。也很难对并发问题进行推理研究,特别是在大型应用中,你不一定知道有没有其他的应用代码正在访问数据库。在一次只有一个用户时,应用开发已经很麻烦了,有许多并发用户使其更加困难,因为任何数据都可能随时改变。

出于这个原因,数据库一直尝试通过提供**事务隔离(transaction isolation)**来隐藏应用开发中的并发问题。从理论上讲,隔离可以通过假装没有并发发生,让程序员的生活更加轻松:可串行化的隔离等级意味着数据库保证事务的效果与真的串行执行(即一次一个事务,没有任何并发)是等价的。

实际上不幸的是:隔离并没有那么简单。可串行化会有性能损失,许多数据库与应用不愿意支付这个代价。因此,系统通常使用较弱的隔离级别来防止一部分,而不是全部的并发问题。这些弱隔离等级难以理解,并且会导致微妙的错误,但是它们仍然在实践中被使用。一些流行的数据库如Oracle 11g,甚至没有实现可串行化。在Oracle中有一个名为“可串行化”的隔离级别,但实际上它实现了一种叫做**快照隔离(snapshot isolation)**的功能,这是一种比可串行化更弱的保证

在研究现实世界中的并发异常前,让我们先来复习一下SQL92标准定义的事务隔离等级。

0x02 SQL92标准

按照ANSI SQL92的标准,三种**现象(phenomena)**区分出了四种隔离等级,如下表所示:

隔离等级 脏写P0 脏读 P1 不可重复读 P2 幻读 P3
读未提交RU ⚠️ ⚠️ ⚠️
读已提交RC ⚠️ ⚠️
可重复读RR ⚠️
可串行化SR
  • 四种现象分别缩写为P0,P1,P2,P3,P是**现象(Phenonmena)**的首字母。
  • 脏写没有在标准中指明,但却是任何隔离等级都需要必须避免的异常

这四种异常可以概述如下:

P0 脏写(Dirty Write)

事务T1修改了数据项,而另一个事务T2在T1提交或回滚之前就修改了T1修改的数据项。

无论如何,事务必须避免这种情况。

P1 脏读(Dirty Read)

事务T1修改了数据项,另一个事务T2在T1提交或回滚前就读到了这个数据项。

如果T1选择了回滚,那么T2实际上读到了一个事实上不存在(未提交)的数据项。

P2 不可重复读( Non-repeatable or Fuzzy Read)

事务T1读取了一个数据项,然后另一个事务T2修改或删除了该数据项并提交。

如果T1尝试重新读取该数据项,它就会看到修改过后的值,或发现值已经被删除。

P3 幻读(Phantom)

事务T1读取了满足某一搜索条件的数据项集合,事务T2创建了新的满足该搜索条件的数据项并提交。

如果T1再次使用同样的搜索条件查询,它会获得与第一次查询不同的结果。

标准的问题

SQL92标准对于隔离级别的定义是有缺陷的 —— 模糊,不精确,并不像标准应有的样子独立于实现。标准其实针对的是基于锁调度的实现来讲的,而基于多版本的实现就很难对号入座。有几个数据库实现了“可重复读”,但它们实际提供的保证存在很大的差异,尽管表面上是标准化的,但没有人真正知道可重复读的意思。

标准还有其他的问题,例如在P3中只提到了创建/插入的情况,但实际上任何写入都可能导致异常现象。 此外,标准对于可串行化也语焉不详,只是说“SERIALIZABLE隔离级别必须保证通常所知的完全序列化执行”。

现象与异常

现象(phenomena)异常(anomalies)并不相同。现象不一定是异常,但异常肯定是现象。例如在脏读的例子中,如果T1回滚而T2提交,那么这肯定算一种异常:看到了不存在的东西。但无论T1和T2各自选择回滚还是提交,这都是一种可能导致脏读的现象。通常而言,异常是一种严格解释,而现象是一种宽泛解释。

0x03 现代模型

相比之下,现代的隔离等级与一致性等级对于这个问题有更清晰的阐述,如图所示:

conncurrent-isolation-level.png

图:隔离等级偏序关系图

conncurrent-isolation-levels.png

图:一致性与隔离等级偏序关系

右子树主要讨论的是多副本情况下的一致性等级,略过不提。为了讨论便利起见,本图中刨除了MAV、CS、I-CI、P-CI等隔离等级,主要需要关注的是快照隔离SI

表:各个隔离等级及其可能出现的异常现象

等级\现象 P0 P1 P4C P4 P2 P3 A5A A5B
读未提交 RU ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
读已提交 RC ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
游标稳定性 CS ⚠️? ⚠️? ⚠️ ⚠️ ⚠️?
可重复读 RR ⚠️
快照隔离 SI ✅? ⚠️
可序列化 SR

带有?标记的表示可能出现异常,依具体实现而异。

主流关系型数据库的实际隔离等级

相应地,将主流关系型数据库为了“兼容标准”而标称的隔离等级映射到现代隔离等级模型中,如下表所示:

表:主流关系型数据库标称隔离等级与实际隔离之间的对照关系

实际\标称 PostgreSQL/9.2+ MySQL/InnoDB Oracle(11g) SQL Server
读未提交 RU RU RU
读已提交 RC RC RC, RR RC RC
可重复读 RR RR
快照隔离 SI RR SR SI
可序列化 SR SR SR SR

以PostgreSQL为例

如果按照ANSI SQL92标准来看,PostgreSQL实际上只有两个隔离等级:RC与SR。

隔离等级 脏读 P1 不可重复读 P2 幻读 P3
RU,RC ⚠️ ⚠️
RR,SR

其中,RU和RC隔离等级中可能出现P2与P3两种异常情况。而RR与SR则能避免P1,P2,P3所有的异常。

当然实际上如果按照现代隔离等级模型,PostgreSQL的RR隔离等级实际上是快照隔离SI,无法解决A5B写偏差的问题。直到9.2引入可串行化快照隔离SSI之后才有真正意义上的SR,如下表所示:

标称 实际 P2 P3 A5A P4 A5B
RC RC ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
RR SI ⚠️
SR SR

作为一种粗略的理解,可以将RC等级视作语句级快照,而将RR等级视作事务级快照。

以MySQL为例

MySQL的RR隔离等级因为无法阻止丢失更新问题,被认为没有提供真正意义上的快照隔离/可重复读。

标称 实际 P2 P3 A5A P4 A5B
RC RC ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
RR RC ✅? ⚠️ ⚠️
SR SR

参考测试用例:ept/hermitage/mysql