【问题标题】:Indexing notifications table in DynamoDBDynamoDB 中的索引通知表
【发布时间】:2015-07-09 04:24:45
【问题描述】:

我将实现一个通知系统,并且我正在尝试找出一种在数据库中存储通知的好方法。我有一个使用 PostgreSQL 数据库的 Web 应用程序,但关系数据库似乎不适合这个用例;我想支持各种类型的通知,每种都包含不同的数据,尽管数据的一个子集对于所有类型的通知都是通用的。因此,我认为 NoSQL 数据库可能比尝试规范化关系数据库中的模式更好,因为这会非常棘手。

我的应用程序托管在 Amazon Web Services (AWS) 中,我一直在研究 DynamoDB 来存储通知。这是因为它是托管的,所以我不必处理它的操作。理想情况下,我想使用 MongoDB,但我真的不想自己处理数据库的操作。我一直在尝试想出一种在 DynamoDB 中做我想做的事情的方法,但我一直在苦苦挣扎,因此我有几个问题。

假设我要为每个通知存储以下数据:

  • 身份证
  • 通知接收者的用户 ID
  • 通知类型
  • 时间戳
  • 是否已阅读/看到
  • 关于通知/事件的元数据(无需查询)

现在,我希望能够查询给定用户的最新 X 通知。另外,在另一个查询中,我想获取特定用户的未读通知数。我正在尝试找出一种方法来索引我的表,以便能够有效地做到这一点。

我可以排除简单地使用散列主键,因为我不会仅通过散列键进行查找。我不知道“哈希和范围主键”是否对我有帮助,因为我不知道将哪个属性作为范围键。我可以有一个唯一的通知 ID 作为哈希键和用户 ID 作为范围键吗?这是否允许我仅通过范围键进行查找,即不提供哈希键?如果可能的话,也许二级索引可以帮助我按时间戳排序。

我还查看了全局二级索引,但问题在于,在查询索引时,DynamoDB 只能返回投影到索引中的属性 - 因为我希望返回所有属性,所以我会有效必须复制我所有的数据,这似乎很荒谬。

如何索引通知表以支持我的用例?是否有可能,或者您有其他建议吗?

【问题讨论】:

    标签: amazon-web-services notifications amazon-dynamodb


    【解决方案1】:

    动机说明:在使用像 DynamoDB 这样的云存储时,我们必须了解存储模型,因为这会直接影响 您的性能、可扩展性和财务成本。它是不同的 而不是使用本地数据库,因为您不仅要为 您存储的数据以及您对其执行的操作 数据。例如,删除记录是一个 WRITE 操作,所以如果 你没有一个有效的清理计划(你的情况是 时间序列数据特别需要一个),您将付出代价。您的 数据模型处理小数据量时不会出现问题 但是当您需要扩展时,肯定会破坏您的计划。那个存在 说,像创建(或不)索引,定义适当的 您的键的属性,创建表分段等将 让整个过程变得与众不同。选择 DynamoDB(或更多 一般而言,键值存储)与任何其他架构一样 决定是有取舍的,你需要清楚地了解 有关存储模型的某些概念,以便能够使用该工具 有效地,选择正确的键确实很重要,但只有 冰山一角。例如,如果你忽略了你是 处理时间序列数据,不管是什么主键或索引 您定义,您的预置吞吐量将不会被优化,因为 它分布在整个表(及其分区)中,而不是 只有经常访问的数据,这意味着未使用的数据是 直接影响您的吞吐量,因为它是相同的一部分 桌子。这会导致以下情况 ProvisionedThroughputExceededException 被“意外”抛出时 您确定您的预置吞吐量应该足以满足您的需求 但是,需要不均匀访问的 TABLE PARTITION 已达到极限(更多详情here)。

    下面的帖子有更多细节,但我想给你一些阅读它的动力并理解虽然你现在肯定可以找到一个更简单的解决方案,但这可能意味着在不久的将来你从头开始一堵墙(“墙”可能是高昂的财务成本、性能和可扩展性的限制,或所有这些的组合)。

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

    答: DynamoDB 是 Key-Value 存储,这意味着最有效的查询使用整个 Key(Hash 或 Hash-Range)。仅仅因为您没有密钥而使用Scan 操作来实际执行查询,这绝对是您的数据模型在您的要求方面存在缺陷的迹象。有几件事情需要考虑,并且有许多选项可以避免这个问题(更多详情见下文)。

    现在在继续之前,我建议您阅读这篇快速文章以清楚地了解 Hash Key 和 Hash+Range Key 之间的区别:

    DynamoDB: When to use what PK type?

    您的案例是典型的时间序列数据场景,您的记录会随着时间的推移而过时。您需要注意两个主要因素:

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

    如果您将所有通知放在一个表中并且更频繁地访问最近的通知,则您的预置吞吐量将不会得到有效使用。 您应该将访问次数最多的项目分组到一个表中,以便可以针对所需访问正确调整预置吞吐量。此外,请确保正确定义 Hash Key that will allow even distribution of your data across multiple partitions。

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

    文档建议将数据分段到不同的表中,以便在记录过时后删除或备份整个表(请参阅下面的更多详细信息)。

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

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

    为您创建的每个表指定吞吐量 要求。 DynamoDB 分配和预留资源来处理您的 具有持续低延迟的吞吐量要求。当你设计 您的应用程序和表格,您应该考虑您的应用程序的 访问模式以最有效地利用您的表 资源。

    假设您设计了一个表格来跟踪您网站上的客户行为, 例如他们点击的 URL。您可以使用散列和 以客户 ID 作为散列属性的范围类型主键和 日期/时间作为范围属性。在此应用程序中,客户数据 随着时间的推移无限增长;但是,应用程序可能会显示 表中所有项目的访问模式不均匀 最新的客户数据更相关,您的应用程序可能 更频繁地访问最新项目并且随着时间的推移这些项目 访问较少,最终很少访问较旧的项目。如果 这是一种已知的访问模式,您可以考虑一下 在设计表架构时。而不是将所有项目存储在一个 单个表,您可以使用多个表来存储这些项目。为了 例如,您可以创建表来存储每月或每周的数据。为了 存储最近一个月或一周的数据的表,其中数据 访问率高,请求更高的吞吐量和表存储 旧数据,您可以降低吞吐量并节省资源。

    您可以通过将“热门”项目存储在一个表中来节省资源 更高的吞吐量设置,以及另一个表中的“冷”项目 较低的吞吐量设置。您只需删除即可删除旧项目 桌子。您可以选择将这些表备份到其他存储 Amazon Simple Storage Service (Amazon S3) 等选项。 删除一个 整个表比删除项目效率高得多 一个接一个,这基本上使您的写入吞吐量翻倍 删除操作与放置操作一样多。

    来源:

    http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GuidelinesForTables.html#GuidelinesForTables.TimeSeriesDataAccessPatterns

    例如,您可以按月份对表格进行分段:

    Notifications_April, Notifications_May, etc
    

    问:我希望能够查询给定用户的最新 X 通知。

    答:我建议使用Query 操作并仅使用具有Range Key 的Hash Key (UserId) 来按Timestamp(日期和时间)。

    Hash Key: UserId
    Range Key: Timestamp
    

    注意:更好的解决方案是 Hash Key 不仅有 UserId,而且还有另一个连接信息,您可以在查询之前计算这些信息以确保您的 Hash Key 授予您甚至访问您的数据的模式。例如,如果来自特定用户的通知比其他用户更容易访问,则您可以开始使用 热分区...在 Hash Key 中添加额外信息可以降低这种风险。

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

    答:创建一个Global Secondary Index 作为稀疏索引,其中UserId 作为Hash Key,Unread 作为Range Key。 p>

    例子:

    Index Name: Notifications_April_Unread
    Hash Key: UserId
    Range Key : Unuread
    

    当您通过哈希键 (UserId) 查询此索引时,您将自动获得所有未读通知,而无需通过与此案例无关的通知进行不必要的扫描。请记住,表中的原始主键会自动投影到索引中,因此如果您需要获取有关通知的更多信息,您可以随时使用这些属性对原始主键执行 GetItem 或 BatchGetItem表。

    注意:您可以探索使用除“未读”标志之外的不同属性的想法,重要的是要记住稀疏索引可以帮助您处理此用例(更多详情如下)。

    详细解释:

    我将有一个稀疏索引来确保您可以查询减少的数据集来进行计数。在您的情况下,您可以使用“未读”属性来标记通知是否已被读取,并使用该属性来创建稀疏索引。当用户阅读通知时,您只需从通知中删除该属性,使其不再显示在索引中。以下是文档中明确适用于您的场景的一些指南:

    利用稀疏索引

    对于表中的任何一项,DynamoDB 只会写一个对应的 索引条目,如果是索引范围键 属性值存在于项目中。如果范围键属性 没有出现在每个表项中,索引被认为是稀疏的。 [...]

    要跟踪未结订单,您可以在 CustomerId(哈希)和 IsOpen(范围)。只有表中定义了 IsOpen 的那些订单 将出现在索引中。然后,您的应用程序可以快速且 通过查询索引有效地找到仍然打开的订单。 例如,如果您有数千个订单,但数量很少 打开后,应用程序可以查询索引并返回 每个未结订单的 OrderId。您的应用程序将执行 读取次数明显少于扫描整个文件所需的次数 客户订单表。 [...]

    您无需将任意值写入 IsOpen 属性,而是 可以使用不同的属性来产生有用的排序顺序 在索引中。为此,您可以创建一个 OrderOpenDate 属性 并将其设置为下订单的日期(仍然删除 订单完成后的属性),并创建 OpenOrders 具有架构 CustomerId (hash) 和 OrderOpenDate (range) 的索引。 这样,当您查询索引时,项目将以 更有用的排序顺序。[...]

    这样的查询可以非常高效,因为 索引将显着小于中的项目数 桌子。此外,您投射到 索引,您将从索引中消耗的读取容量单位越少。

    来源: 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

    【讨论】:

    • 哇,这是一个很棒的答案 - 非常感谢!一个问题,不过。专门使用关系数据库工作后,我想知道为什么我必须将我的数据分割成多个表。您提到我的“预置吞吐量”将无法有效使用。对于关系数据库表,索引可确保选择查询即使有数百万行也是高效的。如果我在单个 DynamoDB 表中保留相同数量的记录,为什么会有问题?我了解更新项目的访问频率更高,但这是性能问题吗?谢谢! :-)
    • 不客气。关于时间序列数据、表分割和供应吞吐量,我会给你一个过于简单的例子,这样你就可以更好地理解这个问题。假设您将所有数据放在一个表中,随着数据量的增加,DynamoDB 会将您的数据划分为多个分区(这是可扩展性的要求)。但是,预置吞吐量平均分配给每个分区,因为 DynamoDB 期望您的数据被平均访问。
    • 现在让我们假设 DynamoDB 将您的表数据划分为两个名为 A 和 B 的分区,为了更容易让我们假设 4 月的通知存储在分区 A 中,5 月的通知存储在分区 B 中。在给定时间内,此表可用读取的总预置吞吐量为 4 个容量单位(导致每个分区有 2 个上限单位)。如果 4 月的通知比 5 月更容易访问,那么来自该分区的预置吞吐量将被更快地使用,并且未来的请求将受到限制...
    • ...即使该表仍然具有可用的预置吞吐量(但仅适用于其他分区)。同样,这过于简单化了,因为 DynamoDB 不会将 5 月和 4 月的数据完全均匀地分开在两个分区中,但是,该场景解释了当您有热分区时会发生什么。表分段使您有机会根据您的时间序列数据要求尽可能细化您的访问模式。
    • 当您专门为具有更高访问需求的最近通知定义表时,您可以相应地定义您的预置吞吐量,从而避免热分区。同样,您可以在数据过时时降低旧表的预置吞吐量。该表是预置吞吐量的粒度单位,因此,您必须确保放入同一个表中的数据具有相同的吞吐量要求。
    【解决方案2】:

    我是 DynamoDB 的活跃用户,这就是我要做的... 首先,我假设您需要单独访问通知(例如,将它们标记为已读/已查看),除了获取user_id 的最新通知。

    餐桌设计:

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

    当您queryUserNotificationsIndex 时,您将您想要通知的用户的user_id 和ScanIndexForward 设置为false,DynamoDB 将按时间倒序返回该用户的通知ID。您可以选择设置 limit 来指定要返回的结果数量,或获取最大 1 MB。

    关于投影属性,您必须将所需的属性投影到索引中,或者您可以简单地投影id,然后在您的代码中编写“水合物”功能来查找每个属性ID 并返回您需要的特定字段。

    如果您真的不喜欢这样,这里有一个替代解决方案...将您的id 设置为您的timestamp。例如,我将使用自自定义纪元以来的毫秒数(例如 2015 年 1 月 1 日)。这是另一种表格设计:

    NotificationsTable
    user_id - Hash key
    id/timestamp - Range key
    

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

    【讨论】:

    • 这很有帮助!我可能会做出妥协并在我返回的 ID 上执行查找以“水合”通知。我不太喜欢这种方法,但不幸的是,DynamoDB 似乎有必要这样做。但这是我可以生存的东西。我会尽快实现这一点,如果我能让这种方法在实践中发挥作用,我会回复你并接受。非常感谢!
    • 顺便说一下,@spin 下面关于使用稀疏索引的建议也是一个有价值的建议,特别是如果您只想查询 UNREAD 通知。对于未读通知,将此属性跟踪为unread: true 或unread: "x" 并完全删除已读通知的属性,这一点很重要。这样,只有具有该属性的通知才会在该索引中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多