亚马逊AWS官方博客

用 DynamoDB 实现多级人物关系之数据结构与建模

业务场景

客户业务当前已部署在亚马逊云科技上,主要面向 2C 以及 2B 的客户,期望建立基于用户之间关系,并将构建好的人物关系用于游戏推荐,广告投递等场景。

在数据库选型之初,首选并非是 DynamoDB,因传统上 DynamoDB 被视为非关系型数据库的代表,在一个需要存储和处理关系的场景下,很容易被忽视,但在调研了不同数据库之后,首先对于可扩展性,当业务需要扩展到 TB 级以上的数据,DynamoDB 不会降低性能,DynamoDB 可以在水平扩展中实现任何规模下提供一致的性能。例如一些 DynamoDB 用户在生产环境下数据量不断扩展,超过 100TB 时,性能与建表之初依然可以保持一致,因此,DynamoDB 能满足业务方对于数据库端个位数 ms 响应的核心诉求;其次,DynamoDB 是一款完全托管的无服务器 NoSQL 数据库,无需用户处理服务器都管理、运维、升级等任务。基于以上两点诉求,客户开始尝试使用 DynamoDB 并最终应用在生产环境。

用户的业务场景主要是基于人物关系,进行相关的游戏,以及广告推荐,且需要实时在线查询。该项目的场景,DynamoDB 作为下游数据库,只需要针对上游的请求,返回至多到 1-2 级人物关系,例如人物 A-B-C 之间的关系即可。对于 DynamoDB 来说,需要将人物关系 A-B-C 拆分成 A-B 的关系以及 B-C 的关系,分别写入 DynamoDB 作为一级任务关系写入。在某些场景下,需要查询 A 的朋友的朋友有哪些,只需要查询 A 的好友,在获取 A 好友列表之后,通过 batch 查询获取 B 的好友列表返回给业务方即可。 本文重点在于使用 DynamoDB 进行一级好友关系建模,我们以 A-B 关系的建立为例,设计 DynamoDB 数据模型。B-C 关系以此类推。

ER Modeling 设计

在 SQL 传统数据库建模时,针对关系类,会通过创建 ER 图,来创建以下几个表格,其中涉及的实体包括:

  1. 用户表:用户(user_id),例如上文提到的人物 A 以及 A 的属性,包括 A 的用户名、性别、地点、用户等级(是否是会员等)。
  2. 关系表:Relevant(Relevantuser_id),例如代表人物 A 的人物关系,包括 Type(type_id)关系类型、create_time 关系创建的时间、update_time 关系更新时间、status 关系状态。
  3. RelationType 表:用户关系类型,用户可以与另一个关系之间有不同种类之间的关系。
  4. Level 表:用户等级,例如订阅用户、免费用户。

ER Modeling 转换成 DynamoDB 建模

在对关系型数据库建模时,会根据 ER Modeling 的实体关系创建多个表格,但是在创建 DynamoDB 的数据库表结构时,通常针对一项业务,只创建一张表,需要将上述不同实体、属性,设计到一张表内。本次多级人物关系,使用 DynamoDB  来说,需要将人物关系 A-B-C 拆分成 A-B 的关系以及 B-C 的关系,分别写入 DynamoDB。在 DynamoDB 数据库的设计中,主要包含 Base 表(基表)与本地索引以及全局二级索引。

DynamoDB 主键与核心属性如下

  • Partition Key:Userid 需要具有唯一性,也是 DynamoDB 中最重要的分区键,会通过分区键,将数据分散在不同的分区。
  • Sort key:Sort key 可以通过合并一些属性来提高 query 快速检索的速度,如在 relevant_id 前添加了 prefix type_id 这样代表关系类型 +user_id 来组成。例如可以通过搜索 user01 的全部好友并通过 prefix 来排序筛选出 user01 用户代表#1 好友的关系人。
  • 属性 Attributes:type_id 关系类型,例如 1 代表好友,2 代表点赞,3 代表拉黑。

在设计之初需要规划好并在未来的生产环境可进一步规划,其中注意到以下几点:

  1. 基表与本地索引共用同一个存储分区。因此本地索引只可以在表格创建之初创建,并且最多创建 5 个本地索引。
  2. 全局二级索引可以在表格创建之后,根据需要来创建,最多可以创建 20 个全局二级索引。
  3. DynamoDB 的主键有两种:a)简单主键只有分区键来作为主键;b)复合主键,由分区键+排序键构成。在较为复杂的查询情况下,使用复合主键,本文使用复合主键,这样可以通过 query 进行范围查询,一次返回某个用户的全部关系。
  4. 可以在排序键中加入前缀以 # 开头,通过 ScanIndexForward 的属性将排序键通过正排或者倒排的方式进行排列,例如 PK = UserId,SK = #Metadata#UserID,这样实现 UserId 的 Metadata 保持在 Query 查询的第一条记录,也可以通过 Limit 1 的方式获取到这条记录。

访问模式实现设计(Access Patten Design)

在 DynamoDB 中,访问模式(Access Patterns)指对数据库进行读取和写入操作的方式和模式,它描述了应用程序如何访问和操作 DynamoDB 中存储的数据。访问模式决定了如何组织和设计 DynamoDB 表格以支持特定的查询和数据访问需求。根据应用程序的需求,可以选择不同的访问模式来优化数据访问和查询性能。

  1. 创建关系-createRelevant
  2. 过滤查找指定用户在指定区域/指定性别的好友列表-getFilteredRelevant
  3. 更新关系-updateRelevent
  4. 删除关系-deleteRelevant

使用 Workbench for Amazon DynamoDB 进行数据建模与创建表格

对于初次使用 DynamoDB 的用户,推荐大家使用 NoSQL Workbench 来进行数据建模,可以选择左侧直接构建新数据模型,根据现有模型设计符合应用程序数据访问模式的模型,您还可以在过程结束时导入和导出设计的数据模型。

可以从官方下载安装:https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.settingup.html

选择左侧创建新模型,并输入新模型名字,选择 Create 进行创建。

进入到模型创建界面,可以在此页面,快速清晰地填写表格名称、Partition Key 分区键、Sort Key 主键(不是必须),并通过点击添加属性来创建 DynamoDB 的属性值。

根据以上信息,创建如下表格,基表名字(base table):UserRelevantTable

全局二级索引:typeIndex

除了使用 Workbench 建模之外,还可以通过可视化的方式,查看已经创立的 DynamoDB 表格,并可以点击 Commit to Amazon DynamoDB 的方式,将创建的模型以及 GSI 表直接一键传输到您在亚马逊云科技的账号内。选择想要部署的区域,以及填写账号的密钥即可通过一键部署,创建出 DynamoDB 表格。

此时,可以到亚马逊云科技的控制台进行查看。以上可以通过使用 NoSQL Workbench 来快速建模,此外,NoSQL Workbench 可以帮助用户快速生成示例数据,在可视化看板中展示,并能够在接近生产环境的条件下测试应用程序访问模式,推荐大家使用。

访问模式实现设计(Access Patten Design)

在创建好数据库模型之后,开始考虑使用 API 的方式来修改数据以及查询数据。DynamoDB 数据库有三种主要的 API 查询方式:

  1. Single item API(单个项目查询):针对单个数据进行修改, GetItem,PutItem,UpdateItem,DeleteItem 这四种,下文创建关系就是使用单个 API 的方式。
  2. Query API(检索 API 查询):主要是通过获取同一个分区键的方式,来返回数据,每次返回数据量最大为 1MB。
  3. Scan API(扫描 API 查询):这种方式在 DynamoDB 的查询场景下,建议尽量避免使用,因每次查询会扫描整张表格,适用于非常小的表格,以及表格迁移的场景。

1. 创建关系-createRelevant 

创建关系,是通过前文提到的单个项目查询 API 方式,通过 PutItem 的方式,插入数据。

item = dynamodbclient.put_item(
    TableName='UserRelevantTable',
    Item = {
            "update_time": {"S": cur_time},
            "create_time": {"S": cur_time},
            "delete_status": {"BOOL": False},
            "tag":{'L':insertinfo['tag']},
            "relevantContent":{'S':insertinfo['relevantContent']},
            "relevant_gender":{'S':insertinfo['relevant_gender']},
            "type_id":{'S':insertinfo['type_id']},
            "user_id":{'S':user_id},
            "relevant_id":{'S':relevant_id}
    }
)

2. 查找指定用户在指定性别的关系列表

items=dynamodbclient.query(
       TableName='UserRelevantTable',
       KeyConditionExpression="#u=:u",
       ConditionExpression:"delete_status=false" 
      FilterExpression="#relevant_gender=:relevant_gender",
       ExpressionAttributeNames={
          "#u":"user_id",
          "#relevant_gender":"relevant_gender"
       },
       ExpressionAttributeValues={
          ":u":{"S":user01},
          ":relevant_gender":{"S":male}
       }
) 

 

查询使用的是上述第二种 Query API, 并使用了三种表达式(参考下图):

  • 通过使用 KeyCondition 表达式来查询哪一条数据合集被返回(one item collection)作为返回值。下图中,黄线的部分,为表达式数值为 user01 的一条 item collection。KeyCondtion 的作用区紧在主键中。
  • 使用 Filter 表达式,通过下图,可以清晰地看到,Filter 的查询功能主要作用在属性值内,通过 Query 查询一条数据返回中,过滤出需要最终返回的数值。查询用户 user01 的所有男性用户用户。
  • ConditionExpression 作为条件筛选出 delete_status 为否的关系并未删除的数据。例代码中井号 # 为属性名字,冒号 : 后边代表属性数值。

3. 更新关系-updateRelevent 

是通过单个项目查询 API 方式,通过 UpdateItem 的方式,插入数据。基于主键更新 ReleventContent 相关内容的属性值。

result = dynamodbclient.update_item(
       TableName='UserRelevantTable',
       Key={
             "user_id":{"S":user_id},
              "relevant_id":{"S":relevant_id}
            },
            
       UpdateExpression="SET #relevantContent=:relevantContent,#update_time=:update_time",
       
       ExpressionAttributeNames={
                    "#relevantContent":'relevantContent',
                    "#update_time":'update_time',
       },
       ExpressionAttributeValues={
                    ":relevantContent":{"S":designer},
                    ":update_time":{"S":update_time}
                    
       }
)

4. 删除关系-deleteRelevant

result = dynamodbclient.update_item(
       TableName='UserRelevantTable',
       Key={
             "user_id":{"S":user_id},
              "relevant_id":{"S":relevant_id}
            },
            
       UpdateExpression="SET #delete_status=:delete_status,#update_time=:update_time",
                Key={
                    "user_id":{"S":user_id},
                    "relevant_id":{"S":relevant_id}
                },
       ExpressionAttributeNames={
                    "#delete_status":'delete_status',
                    "#update_time":'update_time',
       },
       ExpressionAttributeValues={
                    ":delete_status":{"BOOL":True},
                    ":update_time":{"S":update_time}
                    
       }
)

 

参考链接

Lambda Python 代码:https://github.com/Summer1208/Samplecode/blob/main/DynamoDB_UserRelevant

总结

本文通过使用 DynamoDB 来创建人物关系,并实现毫秒级响应速度,满足客户业务需求,对响应速度有要求的客户,可以参考 Lambda github 实例代码进行快速验证。

本篇作者

窦锦涄

亚马逊云科技跨国企业解决方案架构师,负责基于亚马逊云科技云平台的解决方案咨询和设计,在计算,存储,安全,数据分析,DevOps 等领域有丰富的架构设计及实践经验。