使用去重技术实现最终一致性的事务
作者:Nga Tran / 用例,开发者
2023年3月13日
导航至
本文最初发表在InfoWorld上,现经许可在此重发。
去重是分布式数据库中最终一致性用例的有效替代方案。以下是原因。
构建分布式数据库很复杂,需要考虑许多因素。之前,我讨论了两种重要技术,分片和分区,用于从数据库中获得更高的吞吐量和性能。在这篇文章中,我将讨论另一种重要技术,去重,它可以用作替换具有定义主键的最终一致性用例的事务。
如InfluxDB之类的时序数据库为客户端提供了易用性,并接受重复接收相同数据。例如,边缘设备可以在重新连接时直接发送数据,而无需记住之前成功传输的部分。为了在这种情况下返回正确的结果,时序数据库通常应用去重,以获得数据的最终一致性视图。数据。对于经典的交易系统,去重技术可能并不明显适用,但实际上是适用的。让我们通过一些示例来了解它是如何工作的。
理解事务
数据插入和更新通常以原子提交的方式执行,这是一种将一组不同的更改作为一个单一操作应用的操作。更改要么全部成功,要么全部失败,没有中间状态。数据库中的原子提交称为事务。
实现事务需要包括恢复活动,这些活动可以重做或撤销更改,以确保事务在事务过程中发生事故时要么完成,要么完全中止。
在分布式数据库中,实现事务更加复杂,因为需要节点之间的通信以及容忍各种通信问题。《Paxos》(计算机科学)和《Raft》是常用技术,用于实现分布式系统中的事务,并且因其复杂性而广为人知。
图1显示了一个使用事务数据库的钱转移系统的示例。当客户使用银行系统从账户A向账户B转账100美元时,银行启动一个转账作业,开始两个更改的事务:从A账户中提取100美元,并将100美元存入B账户。如果两个更改都成功,则过程将结束,作业完成。如果由于某些原因无法执行提取和/或存款,则系统中的所有更改都将被中止,并将信号发送回作业,指示其重新启动事务。只有在过程成功完成后,A和B才能分别看到提取和存款。否则,他们的账户将不会有任何变化。
非事务性流程
显然,事务性过程构建和维护都很复杂。然而,如图2所示,系统可以简化。在这里,“非事务性过程”中的作业也发出提取和存款。如果两个更改都成功,则作业完成。如果没有一个或只有其中一个更改成功,或者发生错误或超时,则数据将处于“中间状态”,并且将要求作业重复提取和存款。
“中间状态”中的数据结果可能因同一转移的多次重启而不同,但只要最终发生正确的完成状态,它们都是可接受的。让我们通过一个例子来展示这些结果,并解释为什么它们是可接受的。表1显示了事务成功时的两个预期更改。每个更改包括四个字段
- AccountID - 唯一标识一个账户。
- Activity - 要么是提取,要么是存款。
- Amount - 要提取或存入的金额。
- BankJobID - 唯一标识系统中的一项作业。
表1:货币转移事务的两个更改。
AccountID | Activity | Amount | BankJobID |
A | Withdrawal | 100 | 543 |
B | Deposit | 100 | 543 |
在图2中展示的提取和存款的每次重复发行中,都有四种可能的结果
- 无更改。
- 只有A被提取。
- 只有B被存入。
- A被提取且B被存入。
继续我们的例子,假设作业在四次尝试后成功,并发出成功确认。第一次尝试产生“只有B被存入”,因此系统只有一个更改,如表2所示。第二次尝试无结果。第三次尝试产生“只有A被提取”,因此系统现在有两行,如表3所示。第四次尝试产生“A被提取且B被存入”,因此完成状态的数据看起来如表4所示。
表2:第一次和第二次尝试后的系统数据。
AccountID | Activity | Amount | BankJobID |
B | Deposit | 100 | 543 |
表3:第三次尝试后系统中的数据。
AccountID | Activity | Amount | BankJobID |
B | Deposit | 100 | 543 |
A | Withdrawal | 100 | 543 |
表4:第四次尝试后系统中的数据,现在处于完成状态。
AccountID | Activity | Amount | BankJobID |
B | Deposit | 100 | 543 |
A | Withdrawal | 100 | 543 |
A | Withdrawal | 100 | 543 |
B | Deposit | 100 | 543 |
数据去重以实现最终一致性
上面的四次尝试示例在系统中创建了三个不同的数据集,如表2、3和4所示。为什么我们说这是可接受的?答案是,只要我们能有效地管理,系统中的数据允许有冗余。如果我们能识别出冗余数据并在读取时消除这些数据,我们就能得到预期的结果。
在这个例子中,我们说AccountID、Activity和BankJobID的组合唯一地标识了一个更改,并被称为键。如果有许多与同一键关联的更改,则在读取时只返回其中之一。消除冗余信息的流程称为去重。因此,当我们从表3和4读取和去重数据时,我们将得到与表1中显示的预期结果相同的返回值。
对于只包含一个更改的表2,返回的值将是表1预期结果的一部分。这意味着我们不会得到强事务保证,但如果我们愿意等待对账户进行对账,我们最终会得到预期的结果。在现实生活中,即使我们在账户中看到它,银行也不会立即为我们释放转账的金额。换句话说,如果银行仅在一天或两天后使转账金额可用,那么表2表示的部分更改是可以接受的。因为我们的交易过程会重复直到成功,所以一天的时间足够账户对账。
如图2所示的不可交易插入过程和读取时的数据去重不会立即提供预期的结果,但最终结果将与预期相同。这被称为最终一致性系统。相比之下,如图1所示的交易系统始终产生一致的结果。然而,由于需要复杂的通信来保证一致性,因此交易确实需要时间来完成,并且每秒的交易次数将相应地受限。
实践中的去重
如今,大多数数据库将更新实现为删除然后插入,以避免昂贵的原地数据修改。然而,如果系统支持去重,我们可以在表中添加一个“序列”字段来标识数据进入系统的顺序,那么更新就可以简单地作为一个插入操作来完成。
例如,在成功完成如表5所示的转账后,假设我们发现金额应该是200美元。这可以通过用一个具有相同BankJobID但更高序列号的新的转账来修复,如表6所示。在读取时,去重将只返回具有最高序列号的行。因此,金额为100美元的行将永远不会返回。
表5:“更新”前的数据
AccountID | Activity | Amount | BankJobID | 序列 |
B | Deposit | 100 | 543 | 1 |
A | Withdrawal | 100 | 543 | 1 |
表6:“更新”后的数据
AccountID | Activity | Amount | BankJobID | 序列 |
B | Deposit | 100 | 543 | 1 |
A | Withdrawal | 100 | 543 | 1 |
A | Withdrawal | 200 | 543 | 2 |
B | Deposit | 200 | 543 | 2 |
由于去重需要比较数据以查找具有相同键的行,因此正确组织数据和实现正确的去重算法至关重要。常见的技巧是对数据插入进行按键排序,并使用合并算法来查找重复项并进行去重。数据的组织和合并细节将取决于数据的性质、大小以及系统中的可用内存。例如,Apache Arrow实现了多列排序合并,这对于有效地去重至关重要。
在读取时执行去重会增加查询数据所需的时间。为了提高查询性能,可以将去重作为一个后台任务来提前删除冗余数据。大多数系统已经运行后台作业来重新组织数据,例如删除之前标记为删除的数据。去重非常适合这种读取数据、去重或删除冗余数据并将结果写回的模式。
为了避免与数据加载和读取共享CPU和内存资源,这些后台作业通常在称为压缩器(compactor)的单独服务器上执行,这是一个值得单独讨论的另一个大主题。