使用 Amazon DynamoDB 对游戏玩家数据建模

模块 3:核心用途:用户画像和游戏

为 DynamoDB 表设计主键并实现核心访问模式

概述

在上一模块中,我们定义了游戏应用程序的所有访问模式。这个模块中,我们将设计 DynamoDB 表的主键和实现核心访问模式。

在为 DynamoDB 表设计主键时,请遵循以下最佳实践:

  • 区分表中的不同实体。如果您要在单个表中存储多种不同类型的数据,例如员工、部门、客户和订单,请确保您设置的主键能明确标识每个实体,并能对某个单项进行核心操作。
  • 使用前缀区分实体类型。使用前缀区分实体类型可以防止冲突,并有助于查询。例如,如果同一个表中有客户和员工信息,那么客户实体类型的主键可以是 CUSTOMER#,而员工实体类型的主键可以是 EMPLOYEE#。
  • 对单数据项的操作是重点,并尽可能支持对多数据项的操作。设置主键时,需要保证您可以调用以下针对单项操作的 API 接口来实现单项上的读写操作:GetItemPutItemUpdateItemDeleteItem。您也可以通过使用 Query API 接口,根据主键实现多项读取模式。如果主键设计不能满足以上需求,可以添加二级索引,通过二级索引来实现 Query 操作。

我们根据这些最佳实践,为游戏应用程序的表设计主键并执行一些基本操作。

 时长

20 分钟

 使用的服务

操作步骤

  • 我们需要按照前面的介绍来设计不同的实体。在示例游戏中,我们有以下实体:

    • User
    • Game
    • UserGameMapping

    UserGameMapping是表示用户加入游戏的记录。UserGame 之间存在多对多的关系。

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

    如果您的数据模型中有多个实体,并且这些实体之间存在关系,您通常需要使用一个同时具有 HASH RANGE 值的复合主键。复合主键为我们提供了基于 HASH 键的 Query 功能,满足我们所需的一种查询模式。在 DynamoDB 文档中,分区键被称为 HASH,排序键被称为 RANGE。在本指南中,我们交替使用 API 术语,尤其是在讨论代码或 DynamoDB JSON Wire 协议格式时。

    另外两个数据实体 UserGame 没有 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。这样就可以执行单项操作,如 GetItemPutItemDeleteItem

    但是,我们还希望 RANGE 在不同的用户实体上具有不同的值,以便在将此列用作索引的 HASH 键来实现均衡分区。为此,我们添加了 USERNAME

    Game 实体的主键设计与 User 实体的设计类似。Game 实体的主键使用了不同的前缀 GAME# 加 GAME_ID 而不是 USERNAME,但原理是一样的。

    最后,UserGameMapping 实例使用了与 Game 实体相同的 HASH 键。这样,我们不仅能在一次查询中获取游戏的元数据,还能获取游戏中的所有用户信息。然后,我们将 User 实体用于 UserGameMapping 上的 RANGE 键,用于识别哪个用户加入了特定游戏。

    下一步,我们将使用这种主键设计创建一个表。

  • 现在我们已经设计好了主键,让我们创建一个表。

    在模块 1 的步骤 3 中下载的代码中,包含一个 Python 脚本,位于 scripts/ 目录中,名为 create_table.py。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)

    以上脚本用于通过 AWS Python SDK Boto 3 调用 CreateTable 操作。该操作声明了两个属性定义,它们是将用于主键的特定类型属性。虽然 DynamoDB 是无模式,但您必须声明用于主键的属性名称和类型。属性必须包含在写入表的每个数据项中,因此必须在创建表格时指定。

    由于我们要在一个表中存储不同的实体,因此不能使用主键属性名称,如 UserId。根据所存储实体的类型,属性的含义会有所不同。例如,用户的主键可能是 USERNAME,而游戏的主键可能是 GAMEID。因此,我们为属性设置通用名称,如 PK (表示分区键)和 SK (表示排序键)。

    在键模式中配置属性后,我们需要为表指定预置吞吐量。DynamoDB 有两种容量配置模式:预置和按需。采用预置容量模式时,您需要声明想预置的读写吞吐量。无论使用与否,您都要为预置的容量付费。

    在 DynamoDB 按需容量模式下,您可以按请求付费。每个请求的成本略高于完全使用预配吞吐量的成本,但你不必花时间去做容量规划,也不必担心被节流。按需模式非常适合突发性或不可预测的工作负载。我们在本实验室中使用预配容量模式,因为 DynamoDB 免费资源额度 可用于预配容量。

    使用以下命令运行 Python 脚本创建表格。

    python scripts/create_table.py

    创建成功,则返回:“Table created successfully.” (表创建成功)。

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

  • 在这一步中,我们将把一些数据批量加载到上一步创建的 DynamoDB 表中。在后续步骤中,我们将使用这些示例数据。

    scripts/ 目录中,找到名为 items.json 的文件。该文件中包含为本实验室随机生成的 835 个示例数据项。这些数据项包括 UserGameUserGameMapping 实体。打开该文件便可查看示例数据项。

    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 中使用低端客户端,而是使用了更高级的 资源对象资源对象为使用 AWS API 提供了一个更简单的接口。资源对象在这种情况下非常有用,因为它可以批量处理我们的请求。BatchWriteItem 操作可在单个请求中写入多达 25 个数据项。资源对象会帮助我们处理批量请求,我们无需将数据分成 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,表示所有数据项都已成功加载。

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

  • 正如我们在上一个模块中所说的,您应该根据 DynamoDB 表收到的请求数量对其进行优化。我们还介绍了,DynamoDB 不支持关系型数据库的连接 (join) 功能。因此,在设计表时应考虑到请求中所需的类似连接的行为。

    在这一步中,我们将展示如何通过单个请求检索多个实体类型。在游戏中,我们需要获取游戏会话的详细信息。这些详细信息包括游戏本身的信息,如开始时间、结束时间、游戏开局者以及参与游戏的所有玩家的详细信息。

    该请求跨越两个实体类型: 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 请求中,使用 GAME#<GameId>PK。然后,它会检索排序键在 #METADATA#<GameId> 和 USER$ 之间的实体。这将抓取排序键为 #METADATA#<GameId> 的 Game 实体,以及键以 USER# 开头的所有 UserGameMapping 实体。字符串类型的排序键按 ASCII 字符编码排序。在 ASCII 中,美元符号 ($) 直接位于磅号 (#) 之后,因此这确保了我们将获得 UserGameMappings 实体中的所有映射。

    收到响应后,我们会将数据实体组合成应用程序已知的对象。我们知道返回的第一个实体是 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 表上支持更多访问模式。

设计游戏访问模式