Python(boto3)中的 DynamoDB TransactWriteItems

事务是 boto3 两套 API 分歧最大的地方之一:transact_write_items 只存在于低层 client 上,所以 Table 带给你的那种原生 Python 类型的便利在这里没有。而当事务失败时,你需要的东西藏在异常的一个角落里,大多数 boto3 代码从来不去看那里。(事务能给你买到什么在每个 SDK 里都是一样的。)

代码

import boto3

client = boto3.client("dynamodb")

# Move one award between two songs — atomically. If the first song has no
# award to give, NEITHER update happens.
try:
    client.transact_write_items(
        TransactItems=[
            {
                "Update": {
                    "TableName": "Music",
                    "Key": {"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "Cubano Chant"}},
                    "UpdateExpression": "SET #upd0 = #upd0 - :one",
                    "ConditionExpression": "#upd0 >= :one",
                    "ExpressionAttributeNames": {"#upd0": "Awards"},
                    "ExpressionAttributeValues": {":one": {"N": "1"}},
                }
            },
            {
                "Update": {
                    "TableName": "Music",
                    "Key": {"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "A Mis Abuelos"}},
                    "UpdateExpression": "SET #upd0 = if_not_exists(#upd0, :zero) + :one",
                    "ExpressionAttributeNames": {"#upd0": "Awards"},
                    "ExpressionAttributeValues": {":one": {"N": "1"}, ":zero": {"N": "0"}},
                }
            },
        ]
    )
    print("Transaction committed")
except client.exceptions.TransactionCanceledException as e:
    # One reason per action, in TransactItems order. Code "None" means that
    # action was fine — some OTHER action sank the transaction.
    codes = [reason["Code"] for reason in e.response["CancellationReasons"]]
    print(f"Transaction canceled: {codes}")  # e.g. ['ConditionalCheckFailed', 'None']

说明

  • TransactItems——一个由 PutUpdateDeleteConditionCheck 字典组成的列表,每个值都必须是 DynamoDB JSON,没有例外。这是唯一一次带类型形式不可选的 boto3 调用,也正是本页末尾那一节存在的原因。上限在 CLI 那一页
  • CancellationReasons 不在 Error 里面。botocore 会把建模出来的错误字段提到响应字典的顶层,所以捕获到的异常带着的 e.response 里,CancellationReasonsErrorMessageResponseMetadata 是并列的。到 e.response["Error"] 底下去找它什么也找不到,而 e.response["Error"] 里只有概要性的码和消息。
  • None 条目上没有 "Message"——一个成功动作的原因是只有一个键的字典 {"Code": "None"},所以那句自然而然的 [r["Message"] for r in reasons] 恰恰会在那些成功了的动作上抛出 KeyError: 'Message'。用 r.get("Message")
  • 一个生成出来的异常类——botocore 在运行时从服务模型构建出 client.exceptions.TransactionCanceledException,所以它挂在 client 实例上,也因此你没法 from botocore.exceptions import ... 它。在一个拿不到 client 的辅助函数里,捕获 botocore.exceptions.ClientError 并按 e.response["Error"]["Code"] 分支;生成的那个类是它的子类。
  • 结构性错误不会以取消的形式到达,所以代码片段里的 except 子句永远看不到它们。两个动作瞄准同一个项目会抛出一个裸的 ClientError,其码为 ValidationException,而它的 e.response 里没有 CancellationReasons 键,因为事务在任何动作运行之前就被拒绝了。如果你想让这些也带着同样的上下文被记录下来,就在外层捕获 ClientError
  • 动作上的 ReturnValuesOnConditionCheckFailure: "ALL_OLD" 会把落败的那个项目以 DynamoDB JSON 放进该动作的原因里的 Item 键下,省掉你在已经输掉这场竞争之后再补一次 get_item
  • boto3 会替你填 ClientRequestToken。在线上抓包看到,两次一模一样的 transact_write_items 调用发出的是两个不同的 UUID,所以这个 token 覆盖的是单次调用,而不是你自己的捕获-重试循环。如果重试可能比进程活得更久,就自己传一个稳定的。
  • TransactionConflict 上重试,在 ConditionalCheckFailed 上永远不要重试——前者说的是别人短暂地占住了那个项目;后者说的是你的前置条件为假,而且下次还会为假。大多数处理逻辑只需要区分这两个码,完整集合在 TransactionCanceledException 页面上解码
  • 成本——一次事务写入的计费大约是同样的写入在事务之外的两倍,在 CLI 那一页测量过。如果你只需要单个项目上的原子性,条件写入以一半的价格就能买到。

这件事没有资源 API 的版本

boto3.resource("dynamodb").Table(...) 没有 transact_write_items 这个属性;只有 resource.meta.client 有。所以一个已经定型于 Table 和原生 Python 类型的代码库,做事务时只能退回带类型的 DynamoDB JSON,或者用 boto3.dynamodb.types.TypeSerializer 手工序列化:

from boto3.dynamodb.types import TypeSerializer

serialize = TypeSerializer().serialize
values = {k: serialize(v) for k, v in {":one": 1, ":zero": 0}.items()}

TypeSerializer 应用的规则和资源 API 相同,也就是说它拒绝 float,任何带小数的东西都要 decimal.Decimal。当你只是想把一个字面量粘进脚本时,DynamoDB JSON 转换器会在浏览器里做同样的转换。要在不手写这两种形式中任何一种的情况下编辑事务碰到的项目,请下载 DynoTable

相关示例

参考资料

最后核实于 2026-07-28,依据上方链接的 AWS 官方文档。

无需控制台即可使用 DynamoDB

一款快速的 DynamoDB 桌面客户端,可运行 DynamoDB 无法执行的真正 SQL——JOINs、GROUP BY、聚合——并支持可视化编辑和运行在你自己的 Bedrock 密钥上的 AI agent。

30 天免费试用,无需信用卡 — 之后为无时间限制的免费版。