PAYATHON 2026

DynamoDB 能否根据分区内最大的 Sort Key 条件插入?

数据小陈

无法仅用一次 PutItem 的 ConditionExpression 判断新记录的 update_time 是否大于同一文档已有记录的所有 Sort Key。要在并发写入时保证这一规则,可以为每个文档维护一条最新时间记录,并通过 TransactWriteItems 原子地完成条件检查和历史记录插入。

为什么单次 Conditional Put 无法实现?

题目中的完整主键是 (document_id, update_time)。PutItem 的条件表达式只检查这个完整主键对应的已有 Item,不会遍历相同 document_id 下的其他 Item,也不能在表达式中计算整个分区的最大值。AWS PutItem 文档

例如,文档 A 已有两条记录:

(document_id=A, update_time=100)
(document_id=A, update_time=200)

现在尝试插入 (A, 150)。条件表达式检查的是 (A, 150) 对应的位置,不会读取 (A, 200)。

因此,attribute_not_exists(document_id) 可以防止覆盖相同完整主键的记录,却不能证明 150 大于该文档已有的所有时间戳。

使用最新时间记录和事务写入

保留原来的 document 表,另外创建 document_latest 表:

表主键用途
documentdocument_id + update_time保存每次被接受的更新
document_latestdocument_id保存该文档的 latest_update_time

每次插入更新时,在同一个事务中执行两个操作:

  1. 更新 document_latest:要求最新时间不存在,或已有的 latest_update_time 严格小于本次时间。
  2. 向 document 插入历史记录,并要求该完整主键尚不存在。

事务中的操作全部成功才会提交;任何条件失败,两个操作都不会生效。两张表需要位于同一个 AWS 账号和 Region。AWS TransactWriteItems 文档

下面是 Python / Boto3 示例。假设两张表已创建,update_time 传入整数时间戳,value_json 传入已序列化的 JSON 字符串:

import boto3

client = boto3.client("dynamodb")


def insert_document_update(document_id, update_time, value_json):
    timestamp = {"N": str(update_time)}

    return client.transact_write_items(
        TransactItems=[
            {
                "Update": {
                    "TableName": "document_latest",
                    "Key": {
                        "document_id": {"S": document_id}
                    },
                    "UpdateExpression": "SET #latest = :t",
                    "ConditionExpression": (
                        "attribute_not_exists(#latest) OR #latest < :t"
                    ),
                    "ExpressionAttributeNames": {
                        "#latest": "latest_update_time"
                    },
                    "ExpressionAttributeValues": {
                        ":t": timestamp
                    }
                }
            },
            {
                "Put": {
                    "TableName": "document",
                    "Item": {
                        "document_id": {"S": document_id},
                        "update_time": timestamp,
                        "value": {"S": value_json}
                    },
                    "ConditionExpression": "attribute_not_exists(#pk)",
                    "ExpressionAttributeNames": {
                        "#pk": "document_id"
                    }
                }
            }
        ]
    )

条件可以直接写在 Update 操作中。事务不允许两个操作指向同一个 Item,因此不能在同一个事务中再对这条最新时间记录执行 ConditionCheck。AWS 事务说明

假设最新时间是 200,提交 150 或 200 都会因条件不满足而失败。提交 250 时,如果事务成功,就会同时更新最新时间并写入历史记录。

为什么不能先 Query 最大值,再 Put?

查询原表时,指定 document_id,并设置 ScanIndexForward=False 和 Limit=1,可以获取最大的 Sort Key。Number 类型的 Sort Key 按数值排序,查询原表时也可以使用 ConsistentRead=True。AWS Query 文档

但查询和写入之间仍有并发窗口:

当前最大时间:100
请求 A 查询到 100,准备插入 150
请求 B 插入 200
请求 A 随后插入 150

即使 A 使用强一致性查询,B 仍可能在 A 查询完成后写入。事务方案让所有更新共同修改同一条最新时间记录,以此建立原子检查点。

实施时需要注意什么?

  • 如果 document 已有历史记录,必须先为每个文档建立正确的最大时间记录,并协调迁移期间的写入。否则,最新时间记录缺失时,较旧的更新也可能被接受。
  • 所有写入入口都必须遵守同一规则。绕过事务直接写入历史表,会破坏最新时间与历史记录之间的一致性。最新时间记录也不应被随意删除或过期清理。
  • TransactionCanceledException 可能由条件不满足、事务冲突或其他原因引起,应检查取消原因后再处理。过旧时间戳无需原样重试;事务冲突等暂时性失败可以按退避策略重试。AWS 事务错误说明
  • 所有写入方必须使用相同的时间戳单位。严格使用 < 意味着同一时间戳只能接受一条更新;如果业务允许同一时刻多条更新,需要重新定义排序和唯一性规则。

在上述前提下,这个方案保证每次成功提交的新记录的时间戳都严格大于此前已接受的更新。它无法判断尚未到达系统的未来更新,也无法纠正写入方的时钟偏差。

备注:内容仅供参考。