模块 3:核心用途:用户画像和游戏
为 DynamoDB 表设计主键并实现核心访问模式
概述
在上一模块中,我们定义了游戏应用程序的所有访问模式。这个模块中,我们将设计 DynamoDB 表的主键和实现核心访问模式。
在为 DynamoDB 表设计主键时,请遵循以下最佳实践:
- 区分表中的不同实体。如果您要在单个表中存储多种不同类型的数据,例如员工、部门、客户和订单,请确保您设置的主键能明确标识每个实体,并能对某个单项进行核心操作。
- 使用前缀区分实体类型。使用前缀区分实体类型可以防止冲突,并有助于查询。例如,如果同一个表中有客户和员工信息,那么客户实体类型的主键可以是 CUSTOMER#,而员工实体类型的主键可以是 EMPLOYEE#。
- 对单数据项的操作是重点,并尽可能支持对多数据项的操作。设置主键时,需要保证您可以调用以下针对单项操作的 API 接口来实现单项上的读写操作:GetItem、PutItem、UpdateItem 和 DeleteItem。您也可以通过使用 Query API 接口,根据主键实现多项读取模式。如果主键设计不能满足以上需求,可以添加二级索引,通过二级索引来实现 Query 操作。
我们根据这些最佳实践,为游戏应用程序的表设计主键并执行一些基本操作。
时长
20 分钟
使用的服务
操作步骤
-
设计主键
我们需要按照前面的介绍来设计不同的实体。在示例游戏中,我们有以下实体:
- User
- Game
- UserGameMapping
UserGameMapping是表示用户加入游戏的记录。User 和 Game 之间存在多对多的关系。
多对多映射通常表示需要满足两种 Query 模式,这个游戏也不例外。我们有一种访问模式需要查找所有已加入游戏的用户,还有一种访问模式需要查找某个用户玩过的所有游戏。
如果您的数据模型中有多个实体,并且这些实体之间存在关系,您通常需要使用一个同时具有 HASH 和 RANGE 值的复合主键。复合主键为我们提供了基于 HASH 键的 Query 功能,满足我们所需的一种查询模式。在 DynamoDB 文档中,分区键被称为 HASH,排序键被称为 RANGE。在本指南中,我们交替使用 API 术语,尤其是在讨论代码或 DynamoDB JSON Wire 协议格式时。
另外两个数据实体 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。
但是,我们还希望 RANGE 在不同的用户实体上具有不同的值,以便在将此列用作索引的 HASH 键来实现均衡分区。为此,我们添加了 USERNAME。
Game 实体的主键设计与 User 实体的设计类似。Game 实体的主键使用了不同的前缀 GAME# 加 GAME_ID 而不是 USERNAME,但原理是一样的。
最后,UserGameMapping 实例使用了与 Game 实体相同的 HASH 键。这样,我们不仅能在一次查询中获取游戏的元数据,还能获取游戏中的所有用户信息。然后,我们将 User 实体用于 UserGameMapping 上的 RANGE 键,用于识别哪个用户加入了特定游戏。
下一步,我们将使用这种主键设计创建一个表。
-
创建 DynamoDB 表
现在我们已经设计好了主键,让我们创建一个表。
在模块 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 个示例数据项。这些数据项包括 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 中使用低端客户端,而是使用了更高级的 资源对象。资源对象为使用 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 表上支持更多访问模式。