文档数据库事务模型

2021-07-30 08:01:26

<- 返回 这是一系列比较现代操作数据库不同方面的文章中的第一篇。文档数据库是存储半结构化数据的 NoSQL 数据库,通常采用类似于 JSON 的格式。文档数据库对于现代应用程序开发非常方便,因为现代应用程序框架和语言也是基于半结构化对象而不是表格数据。它们通常比 SQL 数据库更适合,在 SQL 数据库中,严格的模式抵制对象进化,复杂的客户端对象关系映射器模拟面向对象的接口,但不完全成功。在 2010 年代的遥远过去,文档数据库不提供事务支持,而是实现各种形式的最终一致性。供应商和开源维护者认为事务性是一种不必要的、复杂的功能,会损害可扩展性和可用性——许多人声称将其添加到他们的系统中是不可能的。时间过去了,现在情况已经大不相同了。事务性的好处在 SQL 甚至一般数据库的上下文之外被广泛接受,并且关键的分布式系统问题已经解决。所有现代文档数据库现​​在都提供某种形式的事务性,但它们的实现和特征差异很大。数据库事务将不同记录的更改捆绑在一起,确保应用程序可以读取和写入数据,而不会相互冲突或以混乱或无效的方式交错更新。 ACID 首字母缩写词(“原子性、一致性、隔离性、持久性”)通常用于描述事务的基本属性,但并未完全描述使用它们的感觉。在实践中,在评估事务实现时,我们想要寻找: Couchbase 最初是通过将 Membase(具有磁盘持久性的 Memcache 的一个分支)与 CouchDB(一个用 Erlang 编写的有点实验性的文档数据库)合并而开发的。因此,它有一段相对曲折的发展历史。它也采用了一种相对折磨的交易模式。

Couchbase 事务完全在客户端工作。每个 Couchbase SDK 都实现了一种算法,该算法本质上是将锁记录写入每个分片,然后将未提交的写入作为元数据写入每个文档,然后更新锁记录,然后将元数据移动到文档中成为主数据。其他观察文档元数据的客户端必须检查锁定文档以找出事务的状态,并根据挂钟时间竞相清理未应用或中止的写入。就文档读取而言,Couchbase 的算法提供读提交隔离,但对于索引等派生数据根本没有隔离,无法判断数据是否被并发修改。它还没有为读者提供快照隔离,这意味着在任何时候没有人真正拥有整个数据库的一致视图,并且容易受到锁定机制中时钟偏差的影响。据我所知,当 Couchbase 事务跨越分片时,它们实际上并不是孤立的或原子的,因为没有具体的理由假设读者会以与作者相同的顺序观察事务记录状态,或者作者会成功更新所有锁定文件。 Couchbase 提供了几个可调的持久性和仲裁选项,它们以令人困惑的方式与事务读写一致性交互,并且默认情况下不需要写入磁盘持久性。最后,根本不支持多区域事务,因为没有同步多区域复制。 Couchbase 没有在 Jepsen 下验证他们的交易,只是做了一些内部工作来验证基本的单文档复制协议。最终,Couchbase 没有提供任何有意义的一般一致性水平,所以也许没有什么需要验证的。例如,您不能使用此系统来实现唯一约束,这是一个非常基本的目标。 Couchbase 采用了一种称为 N1QL 的类似 SQL 的语法,并将相同的策略应用于事务支持,使用显式的 START TRANSACTION/COMMIT TRANSACTION 语句。事务是交互式的,因为客户端可以无限期地保持它们打开(好吧,只要清理窗口持续)并将它们与客户端计算交错。开始交易; SELECT COUNT ( * ) FROM airport WHERE city = 'Stanted' ;更新机场 SET city = 'London' WHERE faa = 'STN'; DELETE FROM airport WHERE city = 'London' AND faa !='STN' ;提交交易;

这个接口是合理的,尽管许多其他 Couchbase 接口,甚至其他 SDK 都不是事务感知的,并且会在提交期间提供过时的读取。由于事务性操作涉及如此多的额外客户端读取和写入,因此它们比正常操作慢得多,Couchbase 将其吹捧为一项功能——因为您可以通过不使用事务来避免速度变慢。那么根据我们的标准,Couchbase 的票价如何?根据数据库标准,提供的一致性级别本质上根本没有一致性。由于事务到期不影响索引或非事务性客户端,因此通用性很差。由于多阶段客户端锁管理,理论性能相对于基线也很差。不过,只要界面支持,易用性就足够了。就我个人而言,我觉得 Couchbase 事务真的应该被称为批量写入,因为它们基本上不提供人们期望从多文档事务中获得的任何属性。 MongoDB 最初是作为 node.js 的 JSON 数据库开始的。对于 JavaScript 开发人员来说非常易于使用,在添加 WiredTiger 存储引擎之前,它一直在努力解决基本的持久性问题。经过多年宣扬事务是不必要的并提倡广泛的数据非规范化来弥补之后,MongoDB 在几年前投降并增加了事务支持。 MongoDB 实现了一个分片的主从复制系统,具有令人眼花缭乱的可调读和写一致性级别数组。它的事务是通过一个集群范围的混合逻辑时钟来实现的,事务时间戳从中派生出来。在分片内,事务首先写入主节点上的预写日志,然后将事务的值连同其时间戳写入存储引擎,该引擎为快照读取提供多版本并发控制。跨分片的事务有一个看起来像是两阶段提交协调的附加层。 MongoDB 充其量提供快照隔离。这意味着与 Couchbase 不同,它至少有一些可序列化的希望(这基本上意味着事务以某种非重叠的顺序应用,即使它不是您期望的顺序)。

不幸的是,默认的一致性级别允许在复制发生之前读取已提交的数据,因此在故障转移期间可能会发生脏读和丢失事务。与 Couchbase 一样,即使这种级别的容错隔离也仅限于单个分片。对于跨越分片的事务,没有读取端协调:读者将观察到在其分片边界撕裂的事务,违反了快照一致性。除非使用非默认读取一致性级别,否则即使事务中的读者也不会看到其他事务的隔离提交。支持唯一约束,但显然是通过与事务不同的机制,因为它们早于事务。我不清楚具有唯一约束的索引如何与事务中的快照隔离交互。 MongoDB 已经与 Jepsen 进行了独立测试,但公司和 Jepsen 团队之间对结果的正确解释存在广泛分歧。读取和写入一致性级别的许多组合甚至会违反最基本的事务属性。 MongoDB 在其驱动程序中实现了遵循相同基本句法模式的 DSL,而不是真正的查询语言。开始和结束事务需要创建一个具有正确读写一致性级别的会话对象,然后分别以(可能不同的)读写一致性级别开始事务,然后提交事务。天真地使用事务特性不会可靠地导致事务行为。会话 = db 。 getMongo()。 startSession ( { readPreference : { mode : "primary" } } );会议 。 startTransaction ( { readConcern : { level : "snapshot" } , writeConcern : { w : "majority" } } );会议 。 getDatabase ("hr") 。雇员 。 updateOne ( { 员工 : 3 } , { $set : { status : "Inactive" } } ) ;会议 。 getDatabase(“报告”)。事件。 insertOne({员工:3,状态:{新:“非活动”,旧:“活动”}});会议 。提交事务();此外,需要最有可能通过人类进行的带外协调,以确保事务写入的所有潜在读者也是事务感知的。 MongoDB 事务在其默认配置中为单个分片查询增加了很少的开销,但也没有真正提供任何事务属性。设置适当的读取和写入一致性级别意味着读取器和写入器都将在分片的主节点和辅助节点之间的仲裁上阻塞。这会影响写入的延迟配置文件以及读取的吞吐量和延迟配置文件,因为必须查询更多节点。

由于两阶段模型层在顶部的额外往返,转向分片查询会大大减慢写入速度。除非仔细设计事务和分片键以避免这种情况,否则性能不连续性可能是不可预测的。 MongoDB 的一致性水平是足够的,但由于所有的边缘情况,通用性很差。相对于基线,理论性能较差且不可预测。并且由于读、写、事务一致性的交互混乱,易用性也不好,抛开查询语言本身的任何问题。有些数据库实际上是与其他数据库的特定域接口,Firestore 就是其中之一。 Firestore 是 Firebase 实时数据库的更新版本,这是一种面向移动的云服务,最初是作为基于 MongoDB 的服务实现的。谷歌在 2014 年收购了 Firebase,并在 2019 年将接口和修改移植到了谷歌 Spanner 后端。 Firebase 实现了一个不寻常的分层数据模型,有点类似于 MUMPS。这导致了很多问题,因为很难以可预测和高性能的方式查询和更新层次结构的子树。尽管如此,将整个数据库视为单个文档确实允许子树的原子更新。 Firestore 通过切换到混合文档分层模型改进了 Firebase 数据模型。数据库现在由集合组成,而不是一棵巨大的树,集合可以包含文档,可以包含子集合,子集合可以包含子文档,或多或少是无限期的。这在文档级别创建了自然的记录边界,这与 Spanner 的底层关系模型很好地对应。该数据模型受 Google Cloud Datastore 数据模型的影响,这是一个最终一致的文档数据库,最初为 Google App Engine 构建,也早于 Google Spanner。事实上,Firestore 包含一个 Datastore 兼容模式。从概念上讲,Firestore 正在将文档查询重写为 Spanner 的关系查询,并且继承了 Spanner 的一致性模型。 Spanner 通过由物理原子时钟支持的多阶段实现来实现可串行化。可串行化是一个非常好的隔离级别。

但是,这里有一些细微差别。由于其起源于移动数据库,Firestore 是围绕混合客户端访问而设计的。由于移动设备通常具有高潜伏性,因此 Firestore 事务在具有客户端乐观并发控制的移动客户端中实现。实际上,所有移动事务都被重写为一系列增量读取,然后是写入的事务性比较和交换操作。这有很多含义: 一个事务,包括它的客户端逻辑,可能会运行多次,所以副作用是危险的 另一方面,服务器端事务使用更典型的悲观关系锁。他们打开一个事务,做一些工作,然后提交它。只读查询似乎默认为强一致性,除非您使用备用数据存储 API,否则无法像 Spanner 那样降到快照一致性。 Firestore 移动客户端还使用本地缓存进行离线同步。这意味着客户端离线时可能会发生以下几种情况:Firestore 尚未经过 Jepsen 验证,但业界没有严重怀疑 Spanner 的实现是合理的。尽管如此,移动客户端显然违反了数据库的一致性保证。 Firestore 的接口是围绕事务 lambda 组成的,它可以将客户端计算与简单的数据库读取和写入调用混合在一起。如上所述,事务是乐观地执行并可能多次执行,还是悲观地执行一次,取决于使用的是服务器还是移动驱动程序。

D b 。 runTransaction((transaction)=>{returntransaction.get(db.collection("cities").doc("SF")).then((sfDoc)=>{varnewPopulation=sfDoc.data().population+1 ;交易.更新(db.collection(“城市”).doc(“SF”),{人口:新人口});});});与 MongoDB 一样,Firestore 不提供真正的查询语言,在查询复杂性方面支持的也少得多。就事务而言,这个接口是合理的,但是无法在事务中读取自己的写入是一个不便。 Firestore 似乎在 Spanner 中将其索引实现为单独的表。它要求用户显式布置索引数据以支持应用程序的查询模式,而不是委托给下面的索引关系查询。默认情况下,Firestore 会为文档中的每个单独字段编制索引,以使原始查询更容易。这是有代价的:默认情况下交易规模相对较大。可以手动从索引中排除字段。 Firestore 提供许多 Google Cloud 单数据中心位置,但仅在美国和欧洲提供多数据中心支持。多数据中心支持会带来更高的成本和更高的写入延迟,但可能会降低读取延迟,具体取决于客户端位置。 Firestore 事务在 Spanner 的默认强一致性模式下运行,速度很快,特别是对于单数据中心数据库。多数据中心事务必须在查询中每个分片的写入仲裁上阻塞。索引如何影响事务中的分片布局尚不清楚。 Firestore 的一致性模型,在重写到 Spanner 之后,非常好。通用性是最佳的,除了有据可查和可预测的边缘情况外,没有有意义的边缘情况。但是,易用性可能会更好。性能很好。但是数据模型和索引方案很奇怪,即使他们对 Firebase 的长期用户很熟悉。与 SQL 或 N1QL 相比,缺乏查询灵活性限制了实用性。 Fauna 是一个作为 API 的数据库。它是一个文档数据库吗?是的,它也是一个文档关系数据库,因为它不仅提供事务,还提供外键、视图和连接。它支持 GraphQL 接口和自己的查询语言 FQL,还提供对数据历史的临时访问。

Fauna 基于 Calvin 实现了独特的单阶段事务架构。和 MongoDB 一样,它的数据模型是基于文档和集合的。与 Spanner 一样,它提供了一流的一致性模型。与 Firestore 一样,它具有无服务器操作模型。与其他文档数据库不同的是,它提供了通过丰富的标准库和函数模型来接近数据的计算能力——与 SQL 一样丰富或更丰富。在 Fauna 中,每个查询都是一个事务。读写事务通过事务日志严格可序列化,它提供了一个分区和复制的提交点,无需与数据副本或原子钟协调。只读事务可通过数据库和数据库驱动程序中仔细的时间戳管理进行序列化,并且在实践中与严格的可序列化没有区别。与此处的其他数据库不同,没有可能违反一致性模型的默认一致性级别问题或驱动程序功能。事务也可以读取自己的写入。事务不能做的一件事是客户端计算;相反,Fauna 专注于数据库内计算,使用称为 FQL 的图灵完备查询语言,并支持复杂的分支和循环、数学和字符串操作以及用户定义的函数。 Fauna 的一致性模型和实现已经与 Jepsen 进行了独立验证并被发现是合理的,尽管分析已经过时并且数据库自那以后有了很大改进。与 Firestore 一样,Fauna 的界面也是围绕查询 lambda 组成的。但是,在 lambda 中,用户无法进行具有副作用或具有读取或写入依赖性的应用程序端调用。相反,这项工作委托给数据库本身。默认情况下,此查询是事务性的,并引用用户定义的数据库函数“getUser”(该数据库架构的一部分)以简化逻辑。

尽管不允许对依赖项进行副作用和计算,但仍然可以使用客户端语言功能进行查询组合;例如,提前定义子查询并使用局部变量引用它。尽管 Fauna 鼓励使用类似于 Firebase 的 index-per-query 模式,但这不是必需的,而且 Fauna 不会自动为文档创建多个默认索引。 Fauna 旨在最大限度地减少多数据中心复制场景中的延迟。读写事务只需要在大多数复制站点中进行一次往返,而只读事务根本不需要任何往返。这使得性能配置文件类似于 Spanner,大概也类似于 Firestore。 Fauna 尚不提供单数据中心区域配置,因此其延迟将高于未配置多区域复制的其他数据库。 Couchbase、MongoDB 和 Firestore 都使用交互式会话模型:读取和写入以增量方式发出,计算在客户端运行。这意味着延迟与事务中数据库操作的数量或多或少呈线性关系。另一方面,Fauna 在 FQL 本身中包含了一个全面的标准库。这允许数据库执行与数据共存的业务逻辑,并保证逻辑只运行一次并且没有副作用。在 Fauna,我们希望 Fauna 成为最好的文档数据库。 Fauna 提供了尽可能高的一致性级别,并且它的通用性是最佳的,没有有意义的边缘情况。性能非常好,在多区域场景下理论上是最优的,并且实现一直在进一步完善中。尽管大多数人不熟悉 FQL,但事务的易用性是微不足道的,因为默认情况下一切都是事务,没有可能导致潜在正确性问题的“逃生路线”

......