Database design 如何在DynamoDb中对手工排序的待办事项列表建模?

Database design 如何在DynamoDb中对手工排序的待办事项列表建模?,database-design,nosql,amazon-dynamodb,data-modeling,Database Design,Nosql,Amazon Dynamodb,Data Modeling,排序就像你的每日待办事项应用程序一样,用户可以手动设置项目的顺序 到目前为止,我的想法是: 如果我们在单个Dynamo记录中使用列表来存储数据,则排序很简单,但是: 更新项目意味着更新整个项目列表。 列表大小被绑定为400KB 如果我们在DynamoDb中将每个项目保存为一条记录,那么我们需要在每次排序或保存项目时指定一个权重值,这可能会导致竞争条件。在存储新项目时会想到: 阅读最新的项目索引-答案:21 在索引21处编写一个项目,但从另一个并发过程编写 在索引21处写下新项目-错误:取21 请

排序就像你的每日待办事项应用程序一样,用户可以手动设置项目的顺序

到目前为止,我的想法是:

如果我们在单个Dynamo记录中使用列表来存储数据,则排序很简单,但是:

更新项目意味着更新整个项目列表。 列表大小被绑定为400KB 如果我们在DynamoDb中将每个项目保存为一条记录,那么我们需要在每次排序或保存项目时指定一个权重值,这可能会导致竞争条件。在存储新项目时会想到:

阅读最新的项目索引-答案:21 在索引21处编写一个项目,但从另一个并发过程编写 在索引21处写下新项目-错误:取21 请参阅下面的备选解决方案-回复OP注释,要求维护索引中的列表顺序

一个解决方案 按列表对表进行分区 将每个列表项存储为单独的项 存储具有版本号的单独排序器项 优点:

sortOrder项可以通过版本号充当列表锁,解决您的竞争条件 重新调用列表只需要更新一个项目,即sortOrder项目 在列表中插入或追加新项只需更新两个项—新项和排序器项 缺点:

查询列表项时,它们将不会以正确的排序顺序返回。每次都需要使用sortOrder项的order属性对返回的项进行排序。 这种排序可以在服务器端完成,例如在将查询结果返回到客户端或前端代码中的客户端之前,在lambda函数中完成。对一个相对较短的列表进行排序是非常有效的,您的用例听起来像这样可能不会引起问题。你应该考虑你的预期访问模式和性能需求。 示例表:

要创建或插入新项目,请执行以下操作:

获取列表的排序器项 将项目放入新项目 使用递增的版本和新的订单属性更新\u项排序器,条件是预期的版本号仍然存在 如果由于另一个客户端更改了列表而导致该条件失败,则可以删除插入的新项目,然后重新开始。 要读取列表,请列出列表中的项目

查询列表中的所有项目,包括排序器 使用sortOrder order属性对项目进行排序 注意:另一个客户端可能添加了尚未更新排序器的项。根据您的需要,您可以忽略新项目,也可以重试,直到订单中的项目与实际返回的项目匹配为止 更新列表中的项目的步骤

更新项目上的\u项目 删除列表中的项目的步骤

获取列表的排序器项 使用递增的版本和新的订单属性更新\u项排序器,条件是预期的版本号仍然存在 删除\u项要删除的项 对现有列表进行排序的步骤

根据预期的版本更新排序器项。预期版本应该是客户端上次查询列表项时的版本 另一种方法:TransactionWriteItems 如果项目按排序顺序存储很重要,您可以利用TransactionWriteItems,这将允许您在单个事务中批量写入多达25个项目。如果您的待办事项列表不超过25项,那么这将是一个更简单的解决方案,具有更高性能的列表读取,而代价是性能较差的插入和排序。25个项目的限制是这个项目的主要问题

替代溶液 根据您的评论,另一种解决方案是通过排序键对列表进行排序。您仍然需要为每个列表设置一个锁,但过程会略有不同

示例表:

要获取列表的锁,客户端应:

更新: TTL至现在+间隔,例如10秒 客户端提供的随机字符串的锁密钥 增加版本号 条件是: TTL<现在,即任何以前的锁已过期 要刷新客户端已获取的锁,即如果10秒的限制正在接近,请执行以下操作:

更新: TTL到现在+一个间隔,例如再10秒 增加版本 条件是: lock_key==预期的lock_key,即它没有过期,并且被另一个客户端获取 插入或追加项目或更新排序顺序时,需要获取锁。如果只是更新项的其他非排序键属性,则不一定需要锁

如果您正在读取列表,您可以在不获取锁的情况下进行乐观查询,但您应该在之前和之后读取锁,以确保在此期间未获取锁,因为这可能会导致列表不一致,因此在这种情况下,您将重试。这就是锁上的version属性的原因

因此:

插入、追加或更改排序顺序时:

获取列表的锁 写入项目,包括更新新的排序键 r正在移动的每个项目 如果需要更多时间,请刷新列表的锁 读书时

读锁 如果TTL>现在 更新正在进行中,请等待TTL后重试 否则,如果TTL<现在: 阅读列表项,即查询 再次读取锁。如果版本已更改,则查询的项目可能不一致,因此请重新开始。如果没有更改,则客户端可以信任返回的结果是一致的。 使用锁的问题是,在更新列表时,其他客户端无法一致地读取。根据您的用例,这可能是可以接受的。如果没有,您可以创建事务一致的缓存层。缓存的实现可能超出了您的问题范围:


所有这些都假设您的待办事项列表不限于25项,否则您可以使用前面提到的TransactionWriteItems。

我非常感谢您的努力。然而,我的第一个预感是,拥有这两个真相来源,我真的很不舒服。“我正在想办法把排序信息保存在索引本身中。”@Danielbowskypopeski,这很公平。基于此,我提供了另一种解决方案,该解决方案通过排序键对每个列表进行本机排序。最困难的部分是能够进行事务性写入,并且仍然允许其他客户一致地读取。您需要在多大程度上进行工程(即考虑读取副本/缓存)将取决于写入的健谈程度以及读取的并发程度。这是一个有趣的问题!我希望这次更新能有所帮助。再一次,非常感谢您的关注。我在你的最新建议中看到的问题是,当索引100中的项目移动到索引10时。我们需要更新介于两者之间的90个项目的索引。我认为,理想情况下,我们将只更改移动项目的排序键。这应该是可能的。客户机将在新索引之前知道该项的索引,因此新索引类似于增加前一项的索引。例如,对于索引K,下一个索引将是K0。也许我们可以使用整个Unicode表来实现这个目的?我明白你的意思,我同意当你在顶部附近插入时,必须重新为整个列表编制索引不是件好事。如果您尝试您的策略,您需要了解前后的元素,以确保不会覆盖后面的元素。为了最大限度地增加插入,你可以分割差异,但是如果你一直在同一个位置插入,你的空间很快就会用完。如果你的列表不是超长的,那么我认为第一个解决方案值得再次考虑,因为插入和重置是非常有效的,锁将保持所有内容的一致性。
partion key | sort key    | version | order                    | item_details
list#1      | sortOrder   | 2       | [item#UUID1, item#UUID2]
list#1      | item#UUID1  |         |                          | foo
list#1      | item#UUID2  |         |                          | bar
list#2      | sortOrder   | 1       | [item#UUID3]
list#2      | item#UUID3  |         |                          | baz
partition key | sort key | itemID  | version | item_details | TTL    | lock_key
list#1        | lock     |         | 2       |              | 888500 | xyz123
list#1        | 000001   | UUID1   |         | foo
list#1        | 000002   | UUID2   |         | bar
list#2        | lock     |         | 1       |              | 888001 | abc456
list#2        | 000001   | UUID3   |         | baz