我们使用机器学习技术将英文博客翻译为简体中文。您可以点击导航栏中的“中文(简体)”切换到英文版本。
使用亚马逊 DynamoDB 实现资源计数器
在开发应用程序时,您通常需要实现计数器来准确跟踪诸如投票、电子商务商店中资源的可用数量或活动门票等操作。这些计数器必须随着资源数量的变化而更新。
在这篇文章中,我们探讨了使用
计数器的挑战
使用计数器时,即使有多个并发进程正在对其进行更新,您也希望它保持准确。失去准确性可能会很痛苦。例如,低估可能导致超额销售,从而无法完成所有客户订单。
在某些情况下,少计或超额计算是可以容忍的,只要设计时能理解并包括在内。例如,为了防止企业超额销售,超额计算订单数可能是可以接受的,因为以后可以使用物理资源数量来更正计数。
如果资源的可用量达到阈值(通常为零),则可能还需要停止消耗。如果这是一项要求,请务必避免任何可能导致某些进程在达到阈值后更新计数器的竞争条件。
在使用 DynamoDB 实现资源计数器时,应考虑以上所有内容。特别是,应该了解数据库在大规模故障情景下的行为方式。忽略失败发生时发生的事情、只关注成功的解决方案会增加生产中出现问题的可能性。
DynamoDB 中的故障处理
DynamoDB 是一个高度可用、持久的分布式数据库。与任何分布式系统一样,在运行期间可能会出现不频繁的间歇性故障,任何应用程序都必须考虑到这些故障。
更新计数器的操作可能会遇到服务故障(由 DynamoDB 返回的 500 系列 HTTP 响应代码识别)。出现故障的原因可能有多种,包括节点重启或被替换,或者暂时的网络问题。该问题可能在计数器更新之前或之后发生,而且客户端并不总是能够可靠地区分这两种情况。
在这篇文章中介绍的七种管理计数器的方法中,每一种都仔细考虑了这种类型的故障:
- 原子计数器
- 乐观的并发控制
- 使用历史进行乐观的并发控制
- 使用客户请求令牌进行交易
- 使用标记物品进行交易
- 用物品集合计数
- 用一组计数
方法 1 — 原子计数器
管理计数器的第一种方法并不复杂,成本最低,但与其他方法相比,精度较低。只有当可以容忍近似计数器时,它才适用。
updateItem
调用自然会在 DynamoDB 中进行序列化,因此同时进行多个调用不存在竞争条件问题。
以下
values.json 包含
:
如果返回 500 系列错误,则没有可靠的方法来确定错误是在更新之前还是之后发生。处理这个问题的一种策略是,如果可以容忍过度应用,则始终在失败后重试,如果可以容忍应用不足,则永远不要重试。
如果需要强制执行阈值,则 可以包含
: threshold 的值
为三(阈值加上变化):
values.json 包含
:
在处理下一个请求之前,DynamoDB 会按顺序应用这些信息,检查条件并应用更新。添加条件表达式不会增加原子计数器的成本,也不会消除 500 系列故障的不确定结果。
方法 2 — 乐观的并发控制
使用 OCC 时的总体设计是让客户端获取一个项目,然后更新该项目,但前提是特定属性(例如版本、时间戳、实体标签或资源计数器值)自获取该项目之时起保持不变。
让我们来看一个实际的例子。以下 亚马逊云科技 CLI 示例将计数器更新为 5,条件是仅当 Entit
yTag 属性的值当前为 a458fc3d(这是从获取该项目获得
的值)时才适用。该示例还将 Entit
yTag
更新 为新的随机值 781d45ac。
values.json 包含
:
如果出现故障,你可以再次获得该物品。如果你看到 Entit
yTag
的值 没有改变,你肯定知道更新没有发生,可以放心地重试。如果您看到 E
ntityTag
已更改为新值,则可以肯定地知道更新已发生,无需重试。但是,如果 Entit
yTag
是第三个值,则表示另一个客户端大约 在同一时间更改了 Entit
yTag
的值,目前尚不清楚您的更新是否适用。以下方法可以应对这一挑战。
要构造此 OCC 操作, 必须首先检索 Entit
yTag
的当前值。这将需要执行
getItem 操作
。
对于 OCC 来说,该过程必须完成
GetItem
的整个周期, 然后在任何其他竞争进程做出更改之前构建和应用更新(必要时重试)。进程进行更改后,任何其他写入过程的条件都将失败,他们必须从
GetIte
m 开始重新启动循环。
随着计数器并发性的增加,由于条件失败而不得不重复循环的进程数量也将增加。这种争用可能是自我毁灭性的;在高并发性下,更多的进程处于这种两步循环中,从而导致更多的争用,更多的进程进入重复循环。发生这种情况时,通过成功写入和继续操作,重复循环中的进程数量可能会超过退出的进程数。如果不给柜台一些喘息的机会,这将导致积压工作量不断增加。
如果对计数器强制设置了阈值,则该进程可以在执行
GetItem
操作后检查更改是否可接受。如果更改会突破阈值,则可以停止该过程。
与原子计数器相比,OCC 的成本影响是每个更新循环需要额外读取,再加上任何重复的循环,这使得 OCC 的成本略高。
方法 3 — 使用历史进行乐观的并发控制
可以增强 OCC 以保留哪些进程成功更新计数器的历史记录。这些附加信息将帮助流程确定何时执行重试。
在此示例中,我们创建了一个基本实现,我们没有将 Entit
yTag
值替换为新值,而是将新值附加到字符串中的现有值。这允许检查 En
tityTag
以检查最近有哪些写入成功以及按什么顺序成功写入。
values.json 包含
:
如果该进程遇到 500 系列故障,它可以在执行
GetItem 操作 后检查 Entit yT
ag
,并检查它尝试追加的随机值是否存在。如果是,则更新成功。否则,该过程应继续重试循环。
随着越来越多的进程成功更新计数器,Entit
yTag 的大
小 将增加。在我们的示例中,Entit
yTag
按顺序 存储成功的作家。为了限制增长,客户端应限制 En
t
ityTag 中单个值的最大数量。当达到此限制时,写入过程可以从 Entit
yTag
(最古老的成功写入器)的开头删除一个条目,并在最后添加自己的值。同样的方法可以使用列表而不是字符串来实现。
有了 En
tityTag
,这种方法更接近于成为精确的计数器。罢免历史最悠久的成功作家仍可能造成模棱两可的边缘情况。大规模地,许多作者都在进行更改,以至于当作者执行 GetIte
m 以检查其是否成功写入时,Entit yT
ag
中的每个条目都 可能已更改。那时这位作家处于模棱两可的境地。
这种方法并不能消除 OCC 由于争用和两步重复循环而导致积压工作量不断增加的可能性。这种方法的成本与基本的 OCC 相同。
交易
方法 4 — 使用客户请求令牌进行交易
DynamoDB 中的@@
t@@
ransact-
items.json 包含:
DynamoDB 保留了过去十分钟内成功通过的请求的 CRT 的内部记录。如果在 10 分钟内再次看到相同的 CRT,则 DynamoDB 将跳过最新的尝试并返回成功响应,从而提供完全的幂等性。如果客户端收到 500 系列故障,它可以安全地使用相同的 CRT 重试操作,并且知道在任何 10 分钟的时间段内,只有一个 CRT 请求会生效。
如果有阈值需要强制执行,transact-items.json 可以包含条件表达式。以下是之前的 CLI 示例,其中添加了一个条件,即数量不得降至零以下:
在 DynamoDB 中
方法 5 — 使用标记物品进行交易
如果幂等性必须超过十分钟的 CRT 窗口,则可以通过让写入器进程跟踪已处理的令牌并禁止其重复使用来手动实现等效功能,不受时间限制。
在交易中,客户必须采取两项行动:
- 更新计数器(只要这样做不会超过可选条件设定的阈值即可)。
- 将唯一标识此更新操作(实际上是 CRT)的标记项放入表中,以将操作标记为已完成。标记项目可以与计数器存储在同一个表中,也可以存储在单独的表中。
标记项的分区键等同于 CRT,对于所做的特定更改,分区键应该是唯一的。它还必须有一个条件来检查
以下示例使用阈值将计数器减少五个,并将标记项存储在单独的表中:
其中 transact-items.json 包含:
如果 500 系列出现故障,可以安全地重试此操作,因为它是幂等的。如果已突破阈值或此操作在之前的迭代中已经进行了此更新,则该交易将被明确拒绝,并发出 transactionc
an
celedException。要验证是什么原因导致了拒绝,请检查标记项是否存在。如果存在,则此进程成功进行了更改。如果不是,则阈值阻止了更改。
标记项可以包含用于记录为何对计数器进行此更改的属性。例如,如果要存储客户订单的详细信息,则可以将其作为附加属性添加到标记项目中,而不是存储在单独的项目中。
在此示例中,交易中写入了两项内容,使其成为最昂贵的方法,成本是使用 CRT 的两倍。为了避免表格中标记项的堆积, 可以
方法 6 — 使用物品集合进行计数
下一种方法以牺牲阅读为代价来简化写作,其独特之处在于它适用于
可以用物品集合替换单个计数器物品,事件集合可用于计算计数器值。这种设计通常是账本的实现方式,它基于 事件来源 的概念 。每次递增或递减都变成自己的项目,具有唯一的 ID,可以按等级编写。稍后可以在客户端对这些项目进行求和以计算计数器值。这种方法在阈值强制执行中效果不佳。
DynamoDB 中的
将 customer1- bal
ance 的计数器增加三个:
如果出现 500 系列故障,则可以安全地重试该进程,而不会出现任何过度应用的风险,因为如果该项目已经存在,则会被覆盖。此操作是幂等的,计数器将按比例保持精确。
在这种方法中,计数器的当前值并未明确存储。要获得该值,必须
查询计数器的时间和成本可能会降低这种方法的吸引力,但可以通过缓存计算出的值并在一段时间内使用缓存来缓解,但要权衡一下,该值会变得比其他方法更陈旧。
通过合并条目(如果不需要保留单个条目以保持可见性),也可以降低存储和查询计数器的成本。可以删除多个条目,然后用包含其净变化的单个条目替换。此数据修改应谨慎完成,并使用交易来确保在执行过程中不会出现任何错误。
如果按顺序获取计数器的更改(即显示更改日志)很有帮助,则可以在 SK 前面加上时间戳。重要的是保留一个随机元素作为后缀,以避免在完全同时执行的两个更改被混淆为单一更改。如果更改不必按顺序排列,那么使用真正随机的 SK 可以让更多的物品集合并发写入,因为这样可以
鉴于无法存储、检查和修改单个物品的当前计数器值,因此不可能可靠地强制执行阈值,尽管可以观察阈值的跨越情况并采取行动。
方法 7 — 用集合计数
如果可以限制计数器的更新总数,则可以使用一组来实现精确的计数器。例如,分配最大数量的老虎机并跟踪剩余的可用插槽数量(例如,在多人游戏中添加有限数量的玩家)。
写入过程生成一个唯一值(请求令牌)来表示正在进行的更新,并尝试在更新过程中插入到集合属性中。DynamoDB 中的原生集合功能可用于检查该令牌是否已存在于集合中,如果存在,则拒绝请求。
一个 CLI 示例,使用最大大小为 50 的字符串集、“a458fc3d” 的请求令牌并将计数器增加一:
values.json 包含:
如果出现 500 系列故障,则可以安全地重试该进程,而不会出现任何过度使用的风险,因为检查令牌在集合中是否存在可以防止重复应用。该计数器将在比例尺上保持精确。
如果更新因条件失败而被拒绝,则该进程必须通过执行
GetItem
操作并检查集合中的值来检查其尝试插入的令牌是否存在于集合中。如果该值存在,则拒绝是因为更新已成功执行,否则由于已达到集合的最大大小,无法进行更新。
如果计数器有一个需要强制执行的阈值,并且每个操作所做的更改为一个,则设置的最大大小为阈值。如果更改是可变的,则可以扩展条件表达式以包括阈值检查。
随着集合中条目数量的增加,物品的总大小也会增加。DynamoDB 写入按四舍五入到最接近的千字节计费。随着该项目的增长超过千字节边界,相关的写入成本将增加。如果更新总数(设定大小和增长)足够有限,那么这可能是大规模实施精确计数器的最具成本效益的方法。DynamoDB 中任何单个项目的 400 KB 大小限制都将是一个硬性限制。
一旦达到设定的最大值或 400 KB 的大小限制,就无法再更新计数器。逃避这种情况的一种可能方法是从集合中删除物品。集合自然是无序的,这使得这个过程比从列表或字符串中删除更加复杂。如果需要删除标记,则带有历史记录的 OCC(方法 3)可能是一种更简单、更合适的方法。
如果要存储更改计数器原因的更多细节,则允许通过交易收集物品的方法(方法 4 和 5)可能更合适。
缩放计数器
DynamoDB 可以通过上述所有方法实现几乎无限的流量。如果大规模使用交易,则 可能会发生
除了方法 6(使用项目集合计数)外,所有方法都写入单个项目。这些单项方法可以达到极限。如果进程达到这些
如果从仓库的垃圾箱中取出某件畅销产品的库存来配送订单,而只有一个箱子是一个瓶颈,那么合乎逻辑的解决方案是将库存分成多个箱子并行处理。从概念上讲,这与写入分片的方法相同。在写入分片中,计数器分成多个项目,并行处理这些项目。
举一个实际的例子,如果产品数量为 1,000,股票代码为
abc123
,则 会在表中的 N 个项目之间平均分配,并在每个项目的分区键末尾添加一个后缀。当 N 为 10 时,这将为计数器创建十个物品。
第一个分区键为
每个的初始数量为 100(1,000 除以 N)。
abc123-0 ,第二个分区键为 abc123-
1 (依此类推),最后一个是 abc123-9
。
在修改计数器的操作中,作者会随机选择其中一件物品进行更新。这将在各个项目之间分配更新,从而提高吞吐量。
实施写入分片可能意味着需要增强客户端,使其明白,如果某个项目由于阈值而无法更新,则另一个项目可能有能力接受该更新,或者更新数量可能必须分成几个项目。要了解计数器的总值,还需要读取所有物品并对各部分进行求和。
使用 亚马逊云科技 开发工具包
适用于 DynamoDB 的 亚马逊云科技 开发工具包可在某些情况下实现自动重试。这会对详细说明的非幂等或重试安全的方法产生影响。SDK 自动重试可能会导致过度应用更新。除非应用程序明确处理重试问题,否则使用这些方法禁用 SDK 重试可能会导致应用程序不足。
对于方法 2、3 和 7(乐观并发控制、带历史记录的 OCC 和使用集合计数),SDK 的自动重试可以成功应用更新,但随后重试返回条件失败,让应用程序来确定发生了什么以及接下来要采取的步骤。
对于方法 4 和方法 6(使用客户端请求令牌进行交易并使用项目集合进行计数),SDK 的重试将是透明的,因为 DynamoDB 将对成功应用的操作进行任何重试返回成功响应。对应用程序没有任何影响。SDK 还会自动生成和发送方法 5 的 CRT。这使得这些方法更易于使用。
对于方法 5(使用标记项进行交易),SDK 重试可能会导致 transactionCanceledException,让应用程序来决定发生了什么以及接下来要采取哪些步骤。
全球表
一个应用程序可能需要一个计数器才能存在于多个区域。在这种情况下, 可以使用
如果解决方案只写入单个区域,但从多个区域读取,则可以使用本文中详述的方法之一来更新计数器。计数器的精度由所选择的方法决定。任何从写入区域以外的区域读取的进程都可能获得比从写入区域读取更陈旧的计数器值,因为计数器更新会传播,但这种传播通常在一秒钟内完成。
如果解决方案写入多个区域,则只有方法 6(使用物品集合计数)才能保持准确的计数器。这是因为 DynamoDB 全局表使用最后写入者获胜的方法来解决对同一项目的写入冲突问题。让我们用一个例子来说明这个问题。区域 A、B 和 C 中存在一些计数器。它最初的值为 4。在区域 A 中写入会将其减少一(至三个)。几毫秒后,区域 B 中的写入操作会将其减少两毫秒(至两毫秒),然后再将区域 A 中的写入内容传播开来。计数器的正确计算值为一,但是最后一个写入者获胜的方法会看到这种冲突并在所有区域中将计数器设置为二。
方法 6 没有这个问题,因为对同一个项目的写入没有冲突。对计数器的每一次更改都是对独特物品的写入,最终会传播到每个区域。更改不会丢失。如果实现了合并项目的过程,则计数器可能会暂时不准确,因为各个写入和删除的传播速率可能不同,顺序也不同,但这最终会自行纠正。只要确保合并工作一次仅限于一个地区即可。
无论采用哪种方法,写入多个区域都会有一个计数器值,当写入操作在区域之间传播时,该计数器值在每个区域中都可能有些陈旧。
结论
在这篇文章中,我们探讨了如何使用亚马逊 DynamoDB 实现资源计数器。您了解了七种方法,包括每种方法的失效模式、成本和复杂性。下表总结了每种方法的特点:
| Approach | Summary |
| Atomic counter | Simple and low cost. Appropriate to use when a guaranteed accurate counter isn’t required. The SDK will automatically retry by default during 500-series failures, so might over apply. |
| Optimistic concurrency control (OCC) | Greater accuracy than atomic counters. Adds some cost and complexity. |
| Optimistic concurrency control with history | Greater accuracy than OCC, but with more complexity. |
| Transaction with a client request token | An accurate counter. Simple to implement but with a bit more cost. |
| Transaction with a marker item | An accurate counter. The best choice only if a retry window greater than ten minutes is required. |
| Counting with an item collection | The only option for global tables. Writing scales up without needing to implement write sharding. Obtaining the counter value is an application-side effort. |
| Counting with a set | The least expensive accurate counter. Requires that the total number of updates to the counter can be limited. |
要进一步探索这篇文章中涉及的一些主题,你可以更深入地研究
如果您对此帖子有任何疑问或反馈,请在下面发表评论。
作者简介
*前述特定亚马逊云科技生成式人工智能相关的服务仅在亚马逊云科技海外区域可用,亚马逊云科技中国仅为帮助您发展海外业务和/或了解行业前沿技术选择推荐该服务。