使用去重实现最终一致性事务

导航至

本文最初发表于 InfoWorld,并经许可在此处转载。

对于分布式数据库的最终一致性用例,去重是事务的有效替代方案。原因如下。

构建分布式数据库既复杂又需要考虑许多因素。之前,我讨论了两种重要的技术,分片分区,用于从数据库中获得更高的吞吐量和性能。在这篇文章中,我将讨论另一种重要的技术,去重,它可以用于在使用定义的 primary key 的最终一致性用例中替代事务。

时间序列数据库(如 InfluxDB)为客户端提供了易用性,并接受多次摄取相同的数据。例如,边缘设备可以在重新连接时直接发送其数据,而无需记住之前成功传输了哪些部分。为了在这种情况下返回正确的结果,时间序列数据库通常应用去重来实现 数据 的最终一致性视图。对于经典事务系统,去重技术可能不明显适用,但实际上是适用的。让我们逐步了解一些示例,以了解其工作原理。

理解事务

数据插入和更新通常在原子提交中执行,原子提交是一种将一组不同的更改作为单个操作应用的操作。这些更改要么全部成功,要么全部中止,没有中间状态。数据库中的原子提交称为事务。

实现事务需要包括恢复活动,这些活动会重做和/或撤销更改,以确保事务在事务中间发生意外事件时完成或完全中止。事务的典型示例是两个账户之间的资金转移,其中要么成功地从一个账户中提取资金并存入另一个账户,要么根本没有资金转移。

在分布式数据库中,由于需要在节点之间进行通信并容忍各种通信问题,因此实现事务甚至更加复杂。PaxosRaft 是用于在分布式系统中实现事务的常用技术,并且以其复杂性而闻名。

图 1 显示了使用事务数据库的资金转移系统的示例。当客户使用银行系统将 100 美元从账户 A 转账到账户 B 时,银行会启动一个转账作业,该作业会启动一个包含两个更改的事务:从 A 账户提取 100 美元,并将 100 美元存入 B 账户。如果这两个更改都成功,则该过程将完成并且作业完成。如果由于某种原因无法执行提款和/或存款,则系统中的所有更改都将被中止,并且将向作业发送信号,告知它重新启动事务。只有在流程成功完成时,A 和 B 才会分别看到提款和存款。否则,他们的账户将不会发生任何变化。

Transactional flow

图 1. 事务流程。

非事务流程

显然,事务流程的构建和维护都很复杂。但是,该系统可以简化,如图 2 所示。在这里,在“非事务流程”中,作业也会发出提款和存款。如果这两个更改都成功,则作业完成。如果两个更改都不成功或只有一个更改成功,或者如果发生错误或超时,则数据将处于“中间状态”,并且将要求作业重复提款和存款。

Non-transactional flow

图 2. 非事务流程。

对于同一笔转账,在“中间状态”中的数据结果可能因多次重启而异,但只要最终会发生正确的完成状态,它们就可以被系统接受。让我们看一个示例来展示这些结果,并解释为什么它们是可以接受的。表 1 显示了如果事务成功,则预期的两个更改。每个更改包括四个字段

  1. AccountID,唯一标识一个账户。
  2. Activity,表示提款或存款。
  3. Amount,表示要提取或存入的金额。
  4. BankJobID,唯一标识系统中的作业。

表 1:资金转移事务的两个更改。

AccountID Activity Amount BankJobID
A 提款 100 543
B 存款 100 543

在图 2 所示的每次重复发出提款和存款时,有四种可能的结果

  1. 无更改。
  2. 仅提取 A 账户。
  3. 仅存入 B 账户。
  4. A 账户被提取,B 账户被存入。

继续我们的示例,假设作业尝试了四次才成功,并且发送了成功的确认。第一次尝试产生“仅存入 B 账户”,因此系统只有一个更改,如表 2 所示。第二次尝试没有产生任何结果。第三次尝试产生“仅提取 A 账户”,因此系统现在有两行,如表 3 所示。第四次尝试产生“A 账户被提取,B 账户被存入”,因此完成状态下的数据如表 4 所示。

表 2:第一次和第二次尝试后系统中的数据。

AccountID Activity Amount BankJobID
B 存款 100 543

表 3:第三次尝试后系统中的数据。

AccountID Activity Amount BankJobID
B 存款 100 543
A 提款 100 543

表 4:第四次尝试后系统中的数据,现在处于完成状态。

AccountID Activity Amount BankJobID
B 存款 100 543
A 提款 100 543
A 提款 100 543
B 存款 100 543

数据去重以实现最终一致性

上面的四次尝试示例在系统中创建了三个不同的数据集,如表 2、3 和 4 所示。为什么我们说这是可以接受的?答案是,只要我们可以有效地管理系统中的数据,系统中的数据就可以是冗余的。如果我们可以识别冗余数据并在读取时消除该数据,我们将能够产生预期的结果。

在本示例中,我们说 AccountID、Activity 和 BankJobID 的组合唯一标识一个更改,称为键。如果存在与同一键关联的多个更改,则在读取时只会返回其中一个。消除冗余信息的过程称为去重。因此,当我们从表 3 和表 4 中读取和去重数据时,我们将获得与表 1 中所示的预期结果相同的返回值。

在表 2 的情况下,它仅包含一个更改,返回的值将仅是表 1 的预期结果的一部分。这意味着我们没有获得强大的事务保证,但是如果我们愿意等待协调账户,我们最终将获得预期的结果。在现实生活中,即使我们在账户中看到转移的资金,银行也不会立即让我们使用转移的资金。换句话说,如果银行在一天或两天后才允许使用转移的资金,则表 2 所代表的部分更改是可以接受的。由于我们的事务过程会重复执行直到成功,因此一天的时间足以协调账户。

图 2 所示的非事务插入过程与读取时的数据去重的结合不会立即提供预期的结果,但最终结果将与预期相同。这称为最终一致性系统。相比之下,图 1 所示的事务系统始终产生一致的结果。但是,由于保证一致性所需的复杂通信,事务确实需要时间才能完成,因此每秒事务数将受到限制。

实践中的去重

如今,大多数数据库都将更新实现为删除,然后再插入,以避免代价高昂的就地数据修改。但是,如果系统支持去重,则如果我们在表中添加“Sequence”字段以标识数据进入系统的顺序,则更新可以仅作为插入来完成。

例如,在如表 5 所示成功完成资金转移后,假设我们发现金额应为 200 美元。可以通过使用相同的 BankJobID 但更高的 Sequence 编号进行新的转账来修复此问题,如表 6 所示。在读取时,去重将仅返回序列号最高的行。因此,金额为 100 美元的行将永远不会返回。

表 5:“更新”前的数据

AccountID Activity Amount BankJobID Sequence
B 存款 100 543 1
A 提款 100 543 1

表 6:“更新”后的数据

AccountID Activity Amount BankJobID Sequence
B 存款 100 543 1
A 提款 100 543 1
A 提款 200 543 2
B 存款 200 543 2

由于去重必须比较数据以查找具有相同键的行,因此正确组织数据和实现正确的去重算法至关重要。常用的技术是在键上对数据插入进行排序,并使用合并算法来查找重复项并对其进行去重。数据的组织和合并方式的详细信息将取决于数据的性质、其大小以及系统中的可用内存。例如,Apache Arrow 实现了 多列排序合并,这对于执行有效的去重至关重要。

在读取时执行去重将增加查询数据所需的时间。为了提高查询性能,去重可以作为后台任务完成,以提前删除冗余数据。大多数系统已经运行后台作业来重组数据,例如删除先前标记为要删除的数据。去重非常适合该模型,该模型读取数据、去重或删除冗余数据,然后将结果写回。

为了避免与数据加载和读取共享 CPU 和内存资源,这些后台作业通常在称为压缩器的单独服务器中执行,这是另一个值得单独撰写文章的庞大主题。