高级阅读约 3 分钟

DynamoDB 事务

一次 DynamoDB 事务把多个写入归为一次全有或全无的操作:要么每个动作都提交,要么一个都不 提交。这就是你在一次写入半途失败时,让两个相关的项不至于彼此偏离的方式。

在审计日志的场景中,你追加的每个事件都应同时让某个按租户的 eventCount 加一。如果事件落地 而计数器没有 —— 或反过来 —— 那么日志和计数就会永远不一致。一次事务让这种情况成为不可能。

DynamoDB 支持事务吗?

是的。DynamoDB 通过 TransactWriteItemsTransactGetItems 支持 ACID 事务,可将最多 100 个动作归集为一次跨一张或多张表的全有或全无操作。要么每个写入都提交,要么一个都不提交,相关项因此不会彼此偏离。事务写入消耗的容量是普通写入的两倍,且一旦条件失败或发生冲突,整个请求就会被取消。

  • TransactWriteItems 把最多 100 个写入动作归为一组,跨一张或多张表,全有或全无。这些项 的总大小不能超过 4 MB。
  • 动作是 PutUpdateDeleteConditionCheck ConditionCheck 对一个你并不写入 的项断言某事。
  • 它的成本翻倍。 一次事务写入消耗的容量是普通写入的两倍 —— DynamoDB 先准备后提交。
  • 冲突和失败的条件会取消整件事,并抛出 TransactionCanceledException;不会留下任何残缺的 部分。

问题所在:两个必须一致的写入

你希望每个新审计事件也让该租户的运行计数加一。作为两次独立调用来做,它们之间的任何失败都会 损坏你的数据:

  1. PutItem 写入新的 EVENT#… 项 —— 成功。
  2. UpdateItem 执行 ADD eventCount 1 —— 超时。

现在日志比计数器所声称的多了一行。盲目重试第 2 步有重复计数的风险;不重试又会让它们不一致。 没有安全的恢复办法,因为这两个写入从未被关联起来。

从 SQL 过来,你会把两者都裹进 BEGIN … COMMIT。DynamoDB 给出的答案是一次单一的事务请求, 它把两个写入一起携带。

TransactWriteItems 如何工作

AWS 开发者指南所述, TransactWriteItems “在一次全有或全无的操作中归集最多 100 个写入动作”,针对最多 100 个不同的项, 且“事务中各项的总大小不能超过 4 MB”。这些动作原子性地完成 —— 要么全部成功,要么一个都不成功。

你可以在一次事务中混用四种动作类型:

  • Put —— 创建或替换一个项。
  • Update —— 编辑属性(包括用 ADD 来处理我们的计数器)。
  • Delete —— 按键移除一个项。
  • ConditionCheck —— 对一个你并不另行写入的项断言一个条件(例如“此租户仍处于活跃状态”)。

实践中还有两条规则会起作用。第一,事务消耗的容量是等价非事务写入的两倍 —— DynamoDB 会做 一个准备阶段和一个提交阶段。第二,你不能在一次事务中两次针对同一个项,而且事务不能针对 索引执行。

"DynamoDB"App"DynamoDB"Appprepare both, checkconditions"TransactWriteItems [Put EVENT,Update counter]""both commit, orTransactionCanceledException"

一个实战示例:原子地追加 + 计数

回到审计日志。为租户 acme 追加一个事件并让它的计数器加一,是一次带两个动作的事务:

actionitemeffect
PutTENANT#acmeEVENT#2026-06-24T09:14Z#a1write the new audit row
UpdateTENANT#acmeCOUNTERADD eventCount 1

如果任一动作的条件失败 —— 比如一个断言该租户未被停用的 ConditionCheck —— 整个请求就会带着 TransactionCanceledException 被取消,且两个写入都不发生。日志和计数器永远不会不一致。

每个动作上的 ConditionExpression 就是那个杠杆。要断言事件行尚不存在(这样一次重试就不会 重复它)且该租户处于活跃状态,你可以这样组合条件:在 Put 上用 attribute_not_exists(SK), 并把 status = :active 作为一个 ConditionCheck

DynamoDB 表达式构建器中构建并复制那些带类型的条件 表达式,而不要手动拼凑 ExpressionAttributeNames:val 占位符 —— 它的条件写入模式会精确 地生成 TransactWriteItems 所要的结构。

对于在不稳定连接上的安全重试,附上一个 client token:在 10 分钟内用同一个 token 重复 TransactWriteItems 会返回成功而不重新应用那些写入 (幂等性)。

在 DynoTable 中操作

DynoTable 在底层用事务来完成它自己的写入:当你暂存若干项编辑并提交它们时,它会把它们作为带 乐观锁条件表达式的 TransactWriteItems 发送,于是你这一批编辑要么全有要么全无 —— 你绝不会 半途应用一次多项更改。

这意味着你可以在同一个暂存批次里编辑事件行和计数器、审阅差异,然后原子地一并提交,而无需 编写任何 SDK 代码。

在 DynoTable 中暂存新的审计事件和租户计数器编辑,然后把两者作为一次全有或全无的事务提交。
在 DynoTable 中暂存新的审计事件和租户计数器编辑,然后把两者作为一次全有或全无的事务提交。

坑与后续步骤

  • 为双倍容量做预算。 一次事务写入计费的 WCU 是普通写入的两倍 —— 对于偶尔的、对一致性至关 重要的成对操作没问题,但如果把每一个写入都裹进事务里就代价高昂。在原子性真正要紧的地方使用它。
  • 显式处理 TransactionCanceledException 它在条件失败与同一些项上另一个进行中的事务 发生冲突时被返回。取消原因会告诉你是哪个动作失败了 —— 去检查它们,不要盲目重试。
  • Stream 记录不感知事务。 一次事务带来的变更会逐步传播到 Streams,并可能与其他变更交错; 消费者不能假定原子性或顺序 —— 参见 DynamoDB Streams
  • 不适用于高吞吐计数器。 一个单独的热点计数器在繁重的并发事务负载下会被限流;对此,请看 原子计数器或对计数器做分片。

事务是用于“这些写入必须一致”的工具。一旦事件开始被一致地落地,下一个考量就是对它们做出反应 —— 那就是 DynamoDB Streams

下载 DynoTable,暂存多项编辑并把它们作为一次事务针对你自己的表提交。

更新于