在上一个模块中,我们定义了游戏应用程序的访问模式。在本模块中,我们将设计 DynamoDB 表的主键并启用核心访问模式。

完成模块所需时间:20 分钟


设计 DynamoDB 表的主键时,请牢记以下最佳实践:

  • 从表中的不同实体开始。如果您要在一个表中存储多种不同类型的数据(例如员工、部门、客户和订单),请确保您的主键能够清楚地区分每个实体并对各个项目启用核心操作。
  • 使用前缀来区分实体类型。使用前缀来区分实体类型可以防止冲突,同时有助于查询。例如,如果“客户”和“员工”位于同一个表中,则“客户”的主键可以是 CUSTOMER#<CUSTOMERID>,“员工”的主键可以是 EMPLOYEE#<EMPLOYEEID>。
  • 首先重点关注单项目操作,然后添加多项目操作(如果可能的话)。对于主键,重要的是您可以使用单项目 API(GetItem、PutItem、UpdateItem 和 DeleteItem)满足单个项目的读写选项要求。通过使用 Query,您还可以使用主键满足您的多项目读取模式要求。如果没有多项目,您可以添加二级索引来处理 Query 使用案例。

请牢记这些最佳实践,接下来我们开始设计游戏应用程序表的主键并执行一些基本操作。


  • 第 1 步:设计主键

    我们来考虑一下先前简介中建议的不同实体。我们的游戏中包含以下实体:

    • User
    • Game
    • UserGameMapping

    UserGameMapping 是用于指示用户已加入游戏的记录。User 和 Game 之间存在多对多关系。

    通常,具有多对多映射表示您需要满足两种 Query 模式,这一点对于游戏也不例外。 我们有两种访问模式:一种模式需要查找所有已加入游戏的用户,另一种模式用于查找用户玩过的所有游戏。

    如果您的数据模型具有多个存在映射关系的实体,您通常要使用具有 HASH 和 RANGE 值的复合主键。复合主键提供了对 HASH 键的 Query 功能,可以满足我们所需的其中一种查询模式的需要。在 DynamoDB 文档中,分区键称为 HASH,排序键称为 RANGE。在本指南中,尤其是在讨论代码或 DynamoDB JSON 线路协议格式时,我们使用的 API 术语与之类似。

    其他两个数据实体(User 和 Game)没有 RANGE 值的自然属性,因为 User 或 Game 的访问模式是键值查找。由于 RANGE 值为必填项,因此我们可以为 RANGE 键提供填充值。

    为此,我们对每种实体类型的 HASH 和 RANGE 值使用以下模式。

    实体 HASH RANGE
    User USER#<USERNAME> #METADATA#<USERNAME>
    Game GAME#<GAME_ID> #METADATA#<GAME_ID>
    UserGameMapping GAME#<GAME_ID> USER#<USERNAME>

    下面来详细解释一下上表。

    对于 User 实体,HASH 值是 USER#<USERNAME>。请注意,此处使用前缀来标识实体,并防止实体类型之间发生任何可能的冲突。

    对于 User 实体上的 RANGE 值,我们使用 #METADATA# 的静态前缀,后跟 USERNAME 值。对于 RANGE 值,切记需要有一个已知值,例如 USERNAME。这样可以允许单项目操作,例如 GetItem、PutItem 和 DeleteItem。

    但是,我们还希望有一个具有来自不同 User 实体的不同值的 RANGE 值,用于实现均匀分区。前提是我们需要使用此列作为索引的 HASH 键。为此,我们附加了 USERNAME。

    Game 实体的主键设计类似于 User 实体的设计。它使用不同的前缀 (GAME#),并且使用 GAME_ID 代替 USERNAME,但是二者的原理是相同的。

    最后,UserGameMapping 使用与 Game 实体相同的 HASH 键。如此一来,我们可以在单个查询中一并获取 Game 的元数据和 Game 中的所有用户。然后,我们对 UserGameMapping 上的 RANGE 键使用 User 实体,用以识别哪个用户已加入特定游戏。

    在下一步中,我们使用此主键设计来创建表。 

  • 第 2 步:创建表

    现在我们已经设计了主键,接下来我们来创建表。

    您在模块 1 的第 3 步中下载的代码在 scripts/ 目录中包含一个名为 create_table.py 的 Python 脚本。Python 脚本内容如下。

    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    try:
        dynamodb.create_table(
            TableName='battle-royale',
            AttributeDefinitions=[
                {
                    "AttributeName": "PK",
                    "AttributeType": "S"
                },
                {
                    "AttributeName": "SK",
                    "AttributeType": "S"
                }
            ],
            KeySchema=[
                {
                    "AttributeName": "PK",
                    "KeyType": "HASH"
                },
                {
                    "AttributeName": "SK",
                    "KeyType": "RANGE"
                }
            ],
            ProvisionedThroughput={
                "ReadCapacityUnits": 1,
                "WriteCapacityUnits": 1
            }
        )
        print("Table created successfully.")
    except Exception as e:
        print("Could not create table. Error:")
        print(e)
    

    上述脚本使用 CreateTable 操作,该操作使用了 Boto 3(适用于 Python 的 AWS 开发工具包)。该操作会声明两个属性定义,它们是要用于主键的类型化属性。虽然 DynamoDB 是无架构的,但您必须声明用于主键的属性的名称和类型。属性必须包含在写入表的每个项目中,因此,必须在创建表时指定属性。

    由于我们将不同的实体存储在一个表中,因此无法使用主键属性名称(例如 UserId)。该属性因存储的实体类型而异。例如,用户的主键可能是其 USERNAME,而游戏的主键可能是其 GAMEID。相应地,我们对属性使用通用名称,例如 PK(用于分区键)和 SK(用于排序键)。

    在键架构中配置属性后,我们将为表指定预置吞吐量。DynamoDB 有两种容量模式:预置和按需。在预置容量模式下,您可以精确指定所需的读写吞吐量。无论是否使用,都需要为此容量付费。

    在 DynamoDB 按需容量模式下,您可以按请求付费。与完全使用预置吞吐量相比,采用此方式后每个请求的成本略高,但是您无需再花费时间进行容量规划或者担心受限。按需模式非常适合峰值工作负载或不可预测的工作负载。我们在本实验中使用预置容量模式,因为它适用于 DynamoDB 免费套餐

    要创建表,请使用以下命令运行 Python 脚本。

    python scripts/create_table.py

    该脚本应返回以下消息:“表已成功创建。”

    在下一步中,我们会将一些示例数据批量加载到表中。 

  • 第 3 步:将数据批量加载到表中

    在此步骤中,我们会将一些数据批量加载到在上一步中创建的 DynamoDB 中。这是为了在后续步骤中使用示例数据而做得准备。

    在 scripts/ 目录中,找到名为 items.json 的文件。该文件包含为本实验随机生成的 835 个示例项目。这些项目包括 User、Game 和 UserGameMapping 实体。如需查看某些示例项目,请打开该文件。

    scripts/ 目录下还有一个名为 bulk_load_table.py 的文件,该文件用于读取 items.json 文件中的项目,并将它们批量写入 DynamoDB 表。该文件的内容如下。

    import json
    
    import boto3
    
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('battle-royale')
    
    items = []
    
    with open('scripts/items.json', 'r') as f:
        for row in f:
            items.append(json.loads(row))
    
    with table.batch_writer() as batch:
        for item in items:
            batch.put_item(Item=item)
    

    在此脚本中,我们没有使用 Boto 3 中的低级别客户端,而是使用了更高级别的 Resource 对象。Resource 对象为使用 AWS API 提供了更简单的接口。Resource 对象可以批量处理请求,因此非常适合用于当前情况。BatchWriteItem 操作能够接受在单个请求中最多包含 25 个项目。Resource 对象能够通过批处理方式完成此操作,而无需人工将数据拆分为最多包含 25 个项目的多个请求。

    运行 bulk_load_table.py 脚本,并通过在终端中运行以下命令来向表中加载数据。

    python scripts/bulk_load_table.py

    您可以通过运行 Scan 操作并获取计数来确保已将所有数据加载到表中。

    aws dynamodb scan \
     --table-name battle-royale \
     --select COUNT

    这应显示以下结果。

    {
        "Count": 835, 
        "ScannedCount": 835, 
        "ConsumedCapacity": null
    }
    

    您应该会看到 Count 为 835,这表示已成功加载所有项目。

    在下一步中,我们将展示如何在单个请求中检索多种实体类型。这可以减少您在应用程序中发出的总网络请求数,并提高应用程序性能。

  • 第 4 步:在单个请求中检索多种实体类型

    正如之前模块中所述,您应该针对 DynamoDB 表接收的请求数量进行优化。此外我们还提到,关系数据库具有联接,而 DynamoDB 没有。作为替代,您可以设计表以允许您的请求中具有类似联接的行为。

    在此步骤中,我们会在单个请求中检索多种实体类型。在游戏中,我们有时需要获取有关游戏会话的详细信息。这些详细信息包括有关游戏本身的信息(例如开始时间、结束时间、放置者)以及有关参与游戏的用户的详细信息。

    该请求跨两种实体类型:Game 实体和 UserGameMapping 实体。但是,这并不意味着我们需要提出多个请求。

    在您下载的代码中,可在 application/ 目录中找到 fetch_game_and_players.py 脚本。该脚本展示了如何构造代码以实现在单个请求中一并检索游戏的 Game 实体和 UserGameMapping 实体。

    fetch_game_and_players.py 脚本的代码组成情况如下。

    import boto3
    
    from entities import Game, UserGameMapping
    
    dynamodb = boto3.client('dynamodb')
    
    GAME_ID = "3d4285f0-e52b-401a-a59b-112b38c4a26b"
    
    
    def fetch_game_and_users(game_id):
        resp = dynamodb.query(
            TableName='battle-royale',
            KeyConditionExpression="PK = :pk AND SK BETWEEN :metadata AND :users",
            ExpressionAttributeValues={
                ":pk": { "S": "GAME#{}".format(game_id) },
                ":metadata": { "S": "#METADATA#{}".format(game_id) },
                ":users": { "S": "USER$" },
            },
            ScanIndexForward=True
        )
    
        game = Game(resp['Items'][0])
        game.users = [UserGameMapping(item) for item in resp['Items'][1:]]
    
        return game
    
    
    game = fetch_game_and_users(GAME_ID)
    
    print(game)
    for user in game.users:
        print(user)
    

    在此脚本的开头,我们导入 Boto 3 库和一些简单的类,用来表示应用程序代码中的对象。您可以在 application/entities.py 文件中查看这些实体的定义。

    脚本的实际工作发生在已在模块中定义的 fetch_game_and_users 函数。该函数类似于您在应用程序中定义的函数,供需要此数据的所有终端节点使用。

    fetch_game_and_users 函数可以执行一些操作。首先,它向 DynamoDB 发出 Query 请求。该 Query 使用 PK 作为 GAME#<GameId>。然后,它请求排序键位于 #METADATA#<GameId> 和 USER$ 之间的所有实体。这一步将获取排序键为 #METADATA#<GameId> 的 Game 实体,以及键以 USER# 开头的所有 UserGameMappings 实体。字符串类型的排序键按 ASCII 字符代码的顺序进行排序。在 ASCII 中,美元符号 ($) 的位置在井字符号 (#) 之后,因此可确保我们能够在 UserGameMapping 实体中获取所有映射。

    收到响应后,我们要将数据实体组装到应用程序已知的对象中。我们知道返回的第一个实体是 Game 实体,因此从该实体创建一个 Game 对象。对于其余实体,我们为每个实体创建一个 UserGameMapping 对象,然后将用户数组附加到 Game 对象。

    脚本的末尾会展示该函数的使用情况,然后打印生成的对象。您可以使用以下命令在终端中运行该脚本。

    python application/fetch_game_and_players.py

    该脚本应将 Game 对象和所有 UserGameMapping 对象打印到控制台。

    Game<3d4285f0-e52b-401a-a59b-112b38c4a26b -- Green Grasslands>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- branchmichael>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- deanmcclure>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- emccoy>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- emma83>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- iherrera>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- jeremyjohnson>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- lisabaker>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- maryharris>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- mayrebecca>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- meghanhernandez>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- nruiz>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- pboyd>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- richardbowman>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- roberthill>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- robertwood>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- victoriapatrick>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- waltervargas>
    

    该脚本会展示如何在单个 DynamoDB 请求中对表进行建模,以及如何编写查询以检索多种实体类型。在关系数据库中,您可以使用联接在单个请求中从不同的表中检索多种实体类型。使用 DynamoDB,您可以对数据进行专门建模,以便让您应一起访问的实体在单个表中彼此相邻。这种方法无需使用典型关系数据库中的联接,并能够在您扩展应用程序时保持高性能。


    在本模块中,我们设计了主键并创建了表。然后,我们将数据批量加载到表中,并展示了如何在单个请求中查询多种实体类型。

    当前的主键设计可以满足以下访问模式:

    • 创建用户资料(写入)
    • 更新用户资料(写入)
    • 获取用户资料(读取)
    • 创建游戏(写入)
    • 查看游戏(读取)
    • 将用户加入游戏(写入)
    • 开始游戏(写入)
    • 为用户更新游戏(写入)
    • 更新游戏(写入)

    在下一模块中,我们将添加二级索引,并了解稀疏索引技术。使用二级索引,您可以在 DynamoDB 表上支持其他访问模式。