在DynamoDB中为通知表创建索引

5
我将要实现一个通知系统,我正在尝试找到一种良好的方法来存储数据库中的通知。我有一个使用PostgreSQL数据库的Web应用程序,但关系型数据库似乎不是这种情况下的理想选择;我想支持各种类型的通知,每种通知包括不同的数据,尽管所有类型的通知都有一部分数据是共同的。因此,我认为NoSQL数据库可能比在关系性数据库中规范化模式更好,因为这会相当棘手。
我的应用程序托管在Amazon Web Services(AWS)中,我一直在研究DynamoDB来存储通知。这是因为它是托管的,所以我不必处理它的操作。理想情况下,我希望使用MongoDB,但我真的不想自己处理数据库的操作。我一直在努力想出在DynamoDB中实现我想要的功能的方法,但我一直在挣扎,因此我有几个问题。
假设我想为每个通知存储以下数据:
- ID - 接收通知的用户ID - 通知类型 - 时间戳 - 是否已读/已看 - 关于通知/事件的元数据(不需要查询)
现在,我想能够查询给定用户最近的X条通知。另外,在另一个查询中,我想获取特定用户的未读通知数量。我正在尝试找出一种方法,可以有效地对我的表进行索引。
我可以排除仅具有哈希主键的方案,因为我不会通过简单的哈希键进行查找。我不知道“哈希和范围主键”是否能够帮助我,在这里我不知道将哪个属性作为范围键。我能否将唯一的通知ID作为哈希键,将用户ID作为范围键?这样是否允许我仅通过范围键进行查找,即不提供哈希键?然后,也许一个二级索引可以帮助我按时间戳排序,如果这是可能的话。
我还看了全局二级索引,但这些的问题在于当查询索引时,DynamoDB只能返回投影到索引中的属性 - 由于我想要返回所有属性,那么实际上我必须复制所有数据,这似乎相当荒谬。
我如何为我的通知表建立索引以支持我的用例?这是可能的吗,或者您有其他建议吗?
2个回答

10

动机说明:当使用云存储,如DynamoDB时,我们必须意识到存储模型的影响,因为它将直接影响性能、可扩展性和财务成本。与使用本地数据库不同,您不仅支付存储的数据,还支付对数据执行的操作。例如,删除记录是一项写操作,因此如果您没有有效的清理计划(尤其是在处理时间序列数据的情况下),您将付出代价。您的数据模型在处理小数据量时不会出现问题,但在需要扩展时肯定会破坏您的计划。话虽如此,像创建(或不创建)索引、为键定义适当的属性、创建表分割等决策将产生整体巨大的差异。选择DynamoDB(或更通用地说,选择键值存储)作为任何其他架构决策都伴随着一个权衡,您需要清楚地了解存储模型的某些概念,才能有效地使用该工具。选择正确的键确实很重要,但这只是冰山一角。例如,如果您忽略了您正在处理时间序列数据的事实,那么无论您定义什么主键或索引,您的预配吞吐量都不会得到优化,因为它分布在整个表格(及其分区)上,而不仅仅是经常访问的数据,这意味着未使用的数据直接影响吞吐量,因为它是同一张表的一部分。这导致了当您知道您的预配吞吐量应该足够满足您的需求时,“意外”抛出“ProvisionedThroughputExceededException”异常的情况,但正在不均匀地访问表格分区已达到其极限(更多详细信息在这里)。

以下帖子有更多细节,但我想给你一些动力来阅读并理解,尽管你现在可以找到一个更简单的解决方案,但在不久的将来,这可能意味着从头开始解决问题(“墙”可能会表现为高昂的财务成本、性能和可扩展性限制,或三者的组合)。

问:我能以唯一的通知ID作为哈希键,用户ID作为范围键吗?这样做是否只允许我通过范围键进行查找,即不提供哈希键?

答:DynamoDB是一个键值存储,这意味着最有效的查询使用整个键(哈希或哈希范围)。使用Scan操作实际上执行查询只是因为您没有您的键,这绝对是您的数据模型在满足您的要求方面存在不足的迹象。有几件事情需要考虑,并且有许多选项可以避免这个问题(下面有更多详细信息)。

在继续之前,我建议您阅读这篇快速文章,以清楚地了解哈希键和哈希+范围键之间的区别:

DynamoDB:何时使用什么PK类型?

您的情况是典型的时间序列数据场景,在这种情况下,随着时间的推移,您的记录将变得过时。有两个主要因素需要注意:

  • 确保您的表具有均匀的访问模式

如果您将所有通知放在一个表中,并且最近的通知被更频繁地访问,那么您的预配置吞吐量将无法有效使用。 您应该将最常访问的项目分组到一个表中,以便可以为所需的访问适当地调整预配置吞吐量。此外,请确保您正确定义哈希键,以使数据在多个分区中分布均匀

  • 以最高效(努力、性能和成本方面)的方式删除过时数据

文档建议将数据分割成不同的表,以便在记录变得过时时可以删除或备份整个表(有关更多详细信息,请参见下面的文档)。

以下是文档中解释与时间序列数据相关的最佳实践的部分:

了解时间序列数据的访问模式

对于每个创建的表,您需要指定吞吐量要求。 DynamoDB 分配并保留资源来处理您的吞吐量要求,并实现持续低延迟。在设计应用程序和表时,您应考虑应用程序的访问模式,以最有效地利用表的资源。
假设您设计了一个用于跟踪客户在您网站上行为的表,例如他们单击的 URL。您可以使用 Customer ID 作为哈希属性和日期/时间作为范围属性来设计表。在此应用程序中,客户数据会随着时间的推移而不断增长;但是,应用程序可能会显示出对表中所有项目的不均匀访问模式,其中最新的客户数据更相关,您的应用程序可能更频繁地访问最新的项目,并且随着时间的推移这些项目将被较少访问,最终较旧的项目很少被访问。如果这是已知的访问模式,则可以在设计表架构时考虑它。您可以使用多个表存储这些项目,而不是在单个表中存储所有项目。例如,您可以创建用于存储每月或每周数据的表。对于存储来自最近一个月或一周数据的表,其中数据访问率很高,请请求更高的吞吐量,并为存储旧数据的表降低吞吐量并节省资源。
您可以通过在一个具有较高吞吐量设置的表中存储“热”项目和在另一个具有较低吞吐量设置的表中存储“冷”项目来节省资源。您可以通过简单地删除表来删除旧项目。您还可以选择将这些表备份到其他存储选项(例如 Amazon Simple Storage Service(Amazon S3))。删除整个表比逐个删除项要高效得多,因为这样做会使写入吞吐量增加一倍,需要进行与放置操作同样多的删除操作。
来源:http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GuidelinesForTables.html#GuidelinesForTables.TimeSeriesDataAccessPatterns 例如,您可以按月对表进行分段:
Notifications_April, Notifications_May, etc

Q: 我希望能够查询给定用户的最近 X 条通知。

A: 我建议使用Query操作,并仅使用Hash KeyUserId)进行查询,Range Key用于按时间戳(日期和时间)对通知进行排序。

Hash Key: UserId
Range Key: Timestamp

注意: 更好的解决方案是在Hash Key中不仅包含UserId,而且还包含另一个可以在查询前计算的连接信息,以确保您的Hash Key授予您对数据的访问权限。例如,如果特定用户的通知比其他用户更受欢迎,则可以开始使用热分区...在Hash Key中添加其他信息将减轻这种风险。

问: 我想获取特定用户未读通知的数量。

答: 创建一个稀疏索引作为全局二级索引(GSI),将UserId作为Hash Key,将Unread作为Range Key

例如:

Index Name: Notifications_April_Unread
Hash Key: UserId
Range Key : Unuread
当您通过哈希键(UserId)查询此索引时,您将自动获得所有未读通知,无需扫描与此情况不相关的通知。请记住,表中的原始主键会自动投影到索引中,因此如果您需要获取有关通知的更多信息,您可以始终借助这些属性在原始表上执行GetItemBatchGetItem操作。 注意:您可以探索使用不同的属性而不是“未读”标志的想法,重要的是要记住,稀疏索引可以帮助您解决此用例(下面有更多详细信息)。
详细说明:
我会建立一个稀疏索引以确保您可以查询减少的数据集来进行计数。在您的案例中,您可以使用“未读”属性标记通知是否已读,并使用该属性创建稀疏索引。当用户阅读通知时,您只需从通知中删除该属性,以使其不再显示在索引中。以下是文档中清楚适用于您场景的一些指南:

利用稀疏索引的优点

对于表中的任何一项,在索引范围键属性值存在于该项时,DynamoDB将只写入相应的索引条目。如果在每个表项中都没有出现范围键属性,则称该索引为稀疏索引。

为了跟踪未完成订单,您可以在CustomerId(哈希)和IsOpen(范围)上创建索引。只有在表中定义IsOpen的订单才会出现在索引中。然后,您的应用程序可以通过查询该索引快速有效地查找仍处于打开状态的订单。例如,如果您有数千个订单,但只有少量订单是打开状态,应用程序可以查询该索引并返回每个打开订单的OrderId。与扫描整个CustomerOrders表相比,您的应用程序将执行显着较少的读取操作。

而不是将任意值写入IsOpen属性,您可以使用另一个属性,该属性将在索引中产生有用的排序顺序。为此,您可以创建OrderOpenDate属性,并将其设置为下订单的日期(并在订单被履行后删除该属性),并使用CustomerId(哈希)和OrderOpenDate(范围)架构创建OpenOrders索引。这样,当您查询索引时,将以更有用的排序顺序返回项目。

这样的查询非常高效,因为索引中的项目数量要少得多,而不是表中的项目数量。此外,您从索引中投影的表属性越少,则消耗的读取容量单位就越少。

参考文献: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GuidelinesForGSI.html#GuidelinesForGSI.SparseIndexes

以下是一些编程创建和删除表所需的操作的参考:

创建表 http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_CreateTable.html

删除表 http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteTable.html


1
哇,太棒了,非常感谢您的回答!不过我有一个问题。由于我一直在使用关系型数据库,我想知道为什么我需要将我的数据分段到多个表中。您提到我的“预配吞吐量”将无法有效利用。对于关系型数据库表,索引可确保选择查询即使有许多百万行也很高效。如果我在单个DynamoDB表中保留相同数量的记录,这将成为问题吗?我知道新项目更频繁地被访问,但那是性能问题吗?谢谢! :-) - ba0708
2
欢迎。关于时间序列数据、表格分割和预配吞吐量,我将给您一个过度简化的例子,以便您更好地理解这个问题。假设您的所有数据都在一个单一的表中,随着数据量的增加,DynamoDB 将您的数据分成多个分区(这是可扩展性的要求)。然而,由于 DynamoDB 期望您的数据被均匀访问,因此预配吞吐量会均匀地分配到每个分区。 - b-s-d
2
现在假设DynamoDB将您的表数据分成两个名为A和B的分区,为了更容易理解,让我们假设4月份的通知存储在分区A中,而5月份的通知存储在分区B中。给定时间内用于此表的读取可用的总预留吞吐量为4个容量单位(每个分区结果为2个容量单位)。如果4月份的通知比5月份的通知更常被访问,则该分区的预留吞吐量将更快地被使用,并且未来的请求将被限制... - b-s-d
2
即使表仍然具有可用的预留吞吐量(但仅适用于其他分区),也会出现这种情况。再次说明,这是一种过度简化,因为DynamoDB不会将5月和4月的数据完全均匀地分成两个分区,但是,该场景解释了当您有热分区时会发生什么。表分段使您有机会在尊重时序数据要求的同时尽可能细粒度地处理访问模式。 - b-s-d
2
当您专门为最近的通知定义一个表,并具有更高的访问需求时,您可以相应地定义所提供的吞吐量,从而避免热分区。同样,随着数据变得过时,您可以减少旧表格的所提供的吞吐量。表是所提供的吞吐量的粒度单位,因此,您必须确保放入同一表中的数据具有相同的吞吐量要求。 - b-s-d
显示剩余3条评论

1
我是 DynamoDB 的活跃用户,以下是我的建议... 首先,我假设你需要单独访问通知(例如标记为已读/已看),除了按 user_id 获取最新通知。
表设计:
NotificationsTable
id - Hash key
user_id
timestamp
...

UserNotificationsIndex (Global Secondary Index)
user_id - Hash key
timestamp - Range key
id

当您查询UserNotificationsIndex时,您需要设置用户的user_idScanIndexForwardfalse,DynamoDB将以时间戳的倒序返回该用户的通知ID。您还可以选择设置limit来限制返回结果的数量,或获取最多1 MB。

关于投影属性,您可以将所需的属性投影到索引中,或者仅投影id,然后在代码中编写“hydrate”功能,对每个ID进行查找并返回所需的特定字段。

如果您真的不喜欢这个方法,这里有一个替代方案... 将您的id设置为timestamp。例如,您可以使用自定义纪元(例如2015年1月1日)以来的毫秒数作为时间戳。这是一个备选的表设计:

NotificationsTable
user_id - Hash key
id/timestamp - Range key

现在,您可以直接查询NotificationsTable,适当设置user_id并将Range键的ScanIndexForward设置为false。当然,这假定您不会出现用户在同一毫秒内收到2条通知的冲突。这应该不太可能,但我不知道您的系统规模。

您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - ba0708
顺便说一下,@spin在下面提出使用稀疏索引的建议也是非常有价值的,特别是如果您只想查询未读通知。重要的是,您要将此属性跟踪为“unread: true”或“unread:“x””,以标记未读通知,并删除已读通知的属性。这样,只有具有该属性的通知才会在该索引中。 - readyornot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接