Epoxy:跨不同数据存储的 ACID 事务


Epoxy 利用 Postgres 事务数据库作为主数据库/协调数据库,并扩展多版本并发控制 (MVCC) 以实现跨数据存储隔离。它通过乐观并发控制 (OCC) 和两阶段提交 (2PC) 协议提供隔离性以及原子性和持久性。

环氧树脂被用作五种不同数据存储的接口层:Postgres, MySQL, Elasticsearch, MongoDB, 和Google Cloud Storage (GCS).

Epoxy 是开源的,网址为  https://github.com/DBOS-project/apiary

Epoxy 的动机是为面对两种日益流行的趋势提供交易保证,这使得实现这一目标变得更加困难。异构数据:除了数据库记录之外,应用程序还存储和访问大型媒体 blob。  微服务:许多系统由多个服务组成,每个服务管理自己的数据。

酒店预订应用程序:客房供应服务将数据存储在 Postgres 中。客户预订服务将数据存储在 MongoDB 中。工作负载包括 80%:搜索可用房间,在 Postgres 中执行读取,在 MongoDB 中执行地理空间搜索;20%:预订房间,在 Postgres 中执行读取和更新,在 MongoDB 中执行插入。如果没有 Epoxy,这些操作将无法以原子和隔离的方式执行,从而导致异常。

电子商务服务:购物车和目录存储在 Postgres 中,目录复制到 Elasticsearch 以进行快速搜索。工作负载包括 90%:搜索和添加项目(Elasticsearch 搜索和 Postgres 读取、插入、更新),8%:结账(Postgres 读取、删除、两次插入,用于购物车到订单的转换),1%:目录插入(Postgres 和 Elasticsearch),1%:目录更新(Postgres 和 Elasticsearch)。如果没有 Epoxy,并发搜索和添加以及目录更新可能会导致购物车添加错误。

Epoxy 协议方法
Epoxy 的想法是提供螺栓式事务支持,利用 Postgres 作为协调器/主数据库,并通过添加填充层将额外的数据存储加入到此设置中。(请注意,协调器和主数据库略有不同。协调器是主数据库之上的垫片。)

解决这个问题的现状如何?如果您没有 Epoxy 来解决这个问题,您将自己编写自定义粘合代码。您将采用以工作流为中心的解决方案,并在粘合代码中嵌入/强制执行业务应用程序逻辑。

某种程度上(以定制的方式),您可以将 OLTP 事务扩展到应用程序中。但这是定制的,并且更难重用,并且处理原子性和隔离性的表面积很大,因为你会在你的代码库中涂抹它。

作为一个更可重用、抽象化的解决方案,您可以考虑使用分布式事务协议(如X/Open XA ),基于两阶段提交,以便跨数据存储执行事务。然而,X/Open XA 缺乏事务隔离,仅提供原子性。Epoxy 通过提供快照隔离超越了 X/Open XA,使其成为更强大的解决方案。

此外,X/Open XA 方法要求数据存储实现两阶段提交的参与者协议,从而造成与 MongoDB、CockroachDB 和 Redis 的兼容性问题。此外,在像S3/GCS这样的非事务性数据存储中,实现X/Open XA的“准备”步骤是不可行的。

Epoxy 协议:设置
在了解 Epoxy 如何提供跨数据存储的事务保证之前,我们先回顾一下 Epoxy 对主数据库(用作协调器)和辅助数据存储的要求。主数据库必须提供至少具有快照隔离的 ACID 事务。这是使用 Epoxy 中的 Postgres 实现的。二级存储必须确保:

  • 单对象写入操作是可线性化且持久的。
  • 每条记录都有一个唯一可识别的密钥。
  • [可选地提高性能]记录可以包含元数据,并且可以根据该元数据有效地过滤数据存储中的查询。


Epoxy 使用四种数据存储来实现:Elasticsearch、MongoDB、GCS、MySQL,满足这些辅助存储要求。
Epoxy 成为访问辅助存储表的独占模式:使用该存储的一个应用程序采用 Epoxy,强制访问该表进行操作的所有应用程序都采用 Epoxy。

每个 Epoxy 事务都链接到一个快照,代表其可见的所有过去事务的集合。快照表示使用两个事务 ID xmin 和 xmax,以及最近提交的事务列表 rc_txns。创建快照时:

  • xmin 是最小的活动事务 ID。
  • xmax 被指定为大于已提交的最大事务 ID 的值。
  • rc_txns 表示 ID 大于 xmin 的已提交事务的集合。
  • 如果 (x < xmin) \/ (x \in rc_txns),则 ID 为 x 的事务位于快照中。

Epoxy 辅助存储垫片通过元数据增强记录版本,以促进事务读取操作。记录版本对事务的可见性取决于事务快照中是否存在 beginTxn 以及事务快照中是否存在 endTxn。

  • 记录版本用两个值标记:beginTxn 和 endTxn。
  • beginTxn 表示创建记录版本的事务的 ID。
  • endTxn 是用新版本取代它或删除记录的事务的 ID。

Epoxy协议:OCC
Epoxy 采用两阶段提交 (2PC) 协议。辅助存储首先在其数据库内进行准备,然后主存储结束事务提交(或中止)。

辅助存储S在执行事务T时,在写入之前获取记录键上的排他锁(如果锁定失败,则T被中止)。因此,每个辅助存储垫片都为其记录包含一个锁管理器,为每个记录维护一个独占写锁。此锁可防止对先前记录版本的 endTxn 字段进行并发修改。

完成 T 后,S 通过获取独占(S 本地)验证锁来验证它。然后,S 检查 T 写入的密钥是否也由不在 T 快照中的已提交事务写入。如果验证成功,S 临时将 T 标记为已提交,释放锁,并投票提交。

仅当所有辅助存储都成功验证时,事务才会提交;否则,它将中止并回滚。通过在主数据库上执行提交操作来提交事务。主数据库上的原子提交可确保事务对所有数据存储上的未来事务可见(出现在其快照中)。辅助存储在获悉提交后释放写锁(或者如果决定中止,也用于完成回滚)。

如果事务验证失败或在任何数据存储中遇到任何错误,它将启动中止。为了防止无限期挂起客户端故障,如果与客户端的连接超时,协调器也会中止事务。中止过程删除新添加的记录版本,并恢复记录 endTxn 字段论文列出了以下正确性不变量:

  • SI1:T 始终从 T 启动时有效的已提交信息的快照中读取数据。
  • SI2:仅当在提交时快照之外没有已提交的事务修改了打算由 T 写入的数据时,T 才能提交。
  • AC1:达成决策的所有流程都会达成相同的决策。
  • AC2:流程一旦做出决定,就无法逆转。
  • AC3&4:只有当所有进程都投票“是”时,才会做出提交决策。在没有失败且一致投赞成票的情况下,决定提交。
  • AC5:在任何具有容忍故障(崩溃故障)的执行中,如果所有故障都被修复并且在足够长的时间内没有新的故障发生,则所有进程最终都会做出决定。

如果主数据库/协调数据库发生故障,辅助存储将无法接受任何写入/更新,直到主数据库/协调数据库恢复并恢复数据为止。但它们可以提供读取服务。主/协调器故障意味着辅助存储中活动事务的中止和回滚。在发生次要或主要故障时,目标是让它们备份,并恢复辅助存储以反映已提交的事务,从而建立崩溃一致的状态。

局限性和开销
Epoxy 需要单个协调器/主节点。对于多个主选,事情会变得复杂/复杂,并且跨主选所需的分布式事务效率低下。在云中,可以使用 AWS RDS/Aurora 扩展单个 Postgres 协调器。对于地理分布,可以通过分布式 SQL 产品提供虚拟/单个协调器。

Epoxy 需要对辅助存储表进行独占访问。如果客户端在不使用 Epoxy 的情况下进行写入,则缺少版本信息会使写入对读取不可见。同样,不使用 Epoxy 进行读取可能会暴露同一记录的冲突版本。辅助存储表上的一个应用程序采用 Epoxy 需要该表上的所有其他应用程序执行相同的操作。

更高的开销来自垃圾收集。由于 Epoxy 的 MVCC 方法是通过写入创建新记录版本而不是更新现有记录,因此清理旧版本至关重要。仅当记录版本不再对任何事务可见时(由所有活动事务的快照中的 endTxn 指示),记录版本才会被删除。因此,事务协调器应该定期执行垃圾收集。垃圾收集器扫描所有活动事务以识别最小的 xmin,代表最旧的活动事务。然后,它指示辅助存储垫片删除 endTxn 小于此最小活动 xmin 的记录版本。