使用亚马逊 DynamoDB 实现自动增量

使用 Amazon Dynam oDB 开发应用程序时,有时您希望插入到表中的新项目的序列号不断增加。一些数据库称之为 自动增量 , 并在插入时自动填充该值。示例用例可以为客户订单或支持票证提供数字标识符。

DynamoDB 不提供自动增量作为属性类型,但是有几种方法可以使用 DynamoDB 实现不断增加的序列号。在这篇文章中,我们演示了两种简单且低成本的方法。

解决方案概述

在我们开始之前,请考虑一下您是否真的需要一个不断增加的序列号。随机生成的标识符往往可以更好地扩展,因为它们不需要中央协调点。适宜使用 DynamoDB 模拟自动增量的情况往往分为两类:

  • 从关系数据库迁移时,在关系数据库中,人们或系统已经习惯了先前存在的自动增量式行为
  • 应用程序何时必须为新项目(例如员工编号或票号)提供人性化的增长数字标识符

在以下部分中,我们将介绍如何使用计数器或排序键来获得不断增加的序列号。

用计数器实现

生成不断增加的序列号的第一种方法使用 原子计 数器。这是一个两步过程。首先,请求增加计数器并在响应中接收新值。其次,在随后的写入中使用该新值。

以下 Python 示例更新原子计数器以获取订单 ID 的下一个值,然后插入以 ID 作为分区键的订单。通过这种方法,可以为分区键使用不同的值并将该 ID 存储在另一个属性中。

import boto3

table = boto3.resource('dynamodb').Table('orders')

# Add one to the counter and ask for the new value to be returned
response = table.update_item(
    Key={'pk': 'orderCounter'},
    UpdateExpression="ADD #cnt :val",
    ExpressionAttributeNames={'#cnt': 'count'},
    ExpressionAttributeValues={':val': 1},
    ReturnValues="UPDATED_NEW"
)

# Retrieve the new value
nextOrderId = response['Attributes']['count']

# Use the new value
table.put_item(
    Item={'pk': str(nextOrderId), 'deliveryMethod' : 'expedited'}
)

这种设计没有竞争条件,因为对 DynamoDB 中单个项目的所有写入都是串行应用的。这可确保每个计数器值的返回次数不会超过一次。

这种方法的成本是更新计数器物品需要写入 1 次,再加上存储新物品的通常写入成本。这种方法的最大吞吐量受计数器物品的限制。DynamoDB 中单个小项目的最大吞吐量与分区 的最大吞吐量 相同。

如果在更新计数器和写入新项目之间出现故障,则序列中可能会出现空白。例如,客户端应用程序可能会在这两个步骤之间停止,或者如果第一个值在返回时出现网络故障,亚马逊云科技 SDK 中的自动重试功能可能会多次增加计数器。请注意,自动增量列也可能出现间隙。

如果您的表需要多个序列值,则可以同时维护多个计数器。

使用排序键实现

第二种方法使用项目集合中排序键的最大值来跟踪该项目集合的最大序列值。

存储在 DynamoDB 表中的项目可以将分区键和可选排序键作为其主键的一部分。项目集合中的项目具有相同的分区键,但排序键不同。DynamoDB 查询可以将项目集合作为目标来检索集合中的所有项目,也可以提供排序键条件来检索子集。

通过设计排序键来表示序列中项目的值,可以有效地使用查询来检索序列的最大值。下表包含项目及其问题。项目标识符是分区键。问题编号是排序键(确保在创建表时将排序键声明为数字类型。或者,声明为字符串并使用零填充来确保字典排序符合预期)。每个项目的问题编号会单独增加。任何项目发行编号的最高值是物品集合中的最高值。

Partition Key (a Project ID) Sort Key (an Issue Number) Priority
projectA 1 low
projectA 2 medium
projectB 1 low
projectB 2 high
projectB 3 low

使用下一个序列值向项目集合中添加新项目需要两个步骤。首先,执行查询以检索该项目集合的最高排序键值。其次,尝试使用最高值加 1 写入新项目。写入操作必须包含 条件表达式 ,该表达式要求表中尚未存在该项才能成功写入。这样可以避免出现与大约在同一时间读取相同值并尝试插入具有相同主键的项目的客户端出现任何竞争情况。

如果条件失败(因为另一个客户端首先到达那里并使用了该值),则可以选择两种方法继续:返回起始处再次查询使用的最大值,或者将排序键值增加 1 后重试。

以下 Python 示例演示了查询项目集合(代表项目)中迄今使用的最大值,然后使用下一个值作为排序键编写项目。该示例以递增的排序键值不断重试,直到成功为止。

import boto3
from boto3.dynamodb.conditions import Key

PROJECT_ID = 'projectA'

dynamo = boto3.resource('dynamodb')
client = dynamo.Table('projects')
highestIssueId = 0
saved = False

# Query for the last sorted value in the given item collection
response = client.query(
    KeyConditionExpression=Key('pk').eq(PROJECT_ID),
    ScanIndexForward=False,
    Limit=1
)

# Retrieve the sort key value
if response['Count'] > 0:
    highestIssueId = int(response['Items'][0]['sk'])

while not saved:
    try:
        # Write using the next value in the sequence, but only if the item doesn’t exist
        response = client.put_item(
            Item={
                'pk': PROJECT_ID, 
                'sk' : highestIssueId + 1, 
                'priority' : 'low'
            },
            ConditionExpression='attribute_not_exists(pk)'
        )
        saved = True
    # An exception indicates we lost a race condition, so increment the value and loop again
    except dynamo.meta.client.exceptions.ConditionalCheckFailedException as e:
         highestIssueId = highestIssueId + 1

这种方法的成本是 0.5 个读取单位,用于查询迄今为止使用的最大值,加上存储新项目的通常写入成本。如果尝试写入的内容由于条件已确定该项目已经存在而被拒绝,则将向您收取费用,因此这种方法的成本会随着争用和重试而增加。如果您预计会出现争用,则可以将读取转换为强一致性读取,这会为查询花费 1.0 个读取单位,但始终读取最新值。

此方法的最大吞吐量是每个项目集合 分区 的最大吞吐量 (假设您的项目大小低于 1 KB)。此吞吐量包括任何由于条件而被拒绝的写入尝试。

即使出现意外客户端停止或临时网络问题,这种方法也不会出现计数间隔。

结论

对于无法使用随机生成的 ID 值的情况,您可以使用本文中介绍的技术在 DynamoDB 表中生成不断增加的序列值。

如果你喜欢阅读这篇文章,请查看 使用 DynamoDB 实现资源计数器 扩展 DynamoDB:分区、热键和拆分如何 影响热性能。

要了解如何开始使用 DynamoDB,请参阅我们的 开发者 指南。


作者简介

Chris Gillespie 克里斯·吉莱斯皮 是一位驻 英国的高级解决方案架构师。他的大部分工作时间都花在快速变化的 “云端” 客户身上。工作之余,他与家人共度时光,努力锻炼身体。

Jason Hunter 杰森·亨特 是加州的首席解决方案架构师,专门研究亚马逊 DynamoDB。自 2003 年以来,他一直在使用 NoSQL 数据库。他以对 Java、开源和 XML 的贡献而闻名。


*前述特定亚马逊云科技生成式人工智能相关的服务仅在亚马逊云科技海外区域可用,亚马逊云科技中国仅为帮助您发展海外业务和/或了解行业前沿技术选择推荐该服务。