如何在 DynamoDb 中模拟手工排序的 ToDo 列表?

How to model hand-sorted ToDo list in DynamoDb?

排序就像您的日常待办事项应用一样,用户可以在其中手动设置项目的顺序。

到目前为止我的想法:

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

如果我们将每个项目保存为 DynamoDb 中的一条记录,我们将需要在每次排序或保存项目时指定一个 "weight" 值,这可能会导致竞争条件。想到的一个是存储新项目时:

请参阅下面的替代解决方案 - 响应要求在索引中维护列表顺序的 OP 评论。

一个解决方案

  1. 按列表
  2. 对您的 table 进行分区
  3. 将每个列表项存储为单独的项
  4. 使用版本号
  5. 存储一个单独的 sortOrder 项目

优点:

  • sortOrder项可以作为列表锁(通过版本号),解决你的竞争条件
  • 重新排序列表只需要更新一项(sortOrder 项)
  • 将新项目插入或附加到您的列表只需要更新两个项目(新项目和 sortOrder 项目)

缺点:

  • 查询列表项时,将不会以正确的排序顺序返回它们。您需要使用 sortOrder 项目的 order 属性来对每次返回的项目进行排序。

这种排序可以在服务器端完成(例如,在将查询结果返回给您的客户端之前在 lambda 函数中)或客户端(在您的前端代码中)。对相对较短的列表进行排序是非常高效的,您的用例听起来可能不会引起问题。不过,您应该考虑预期的访问模式和性能需求。

示例table:

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

创建(插入)新项目:

  • get_item 列表的 sortOrder
  • put_item新项目
  • update_item sortOrder 具有递增的版本和新的 order 属性,条件是预期的版本号仍然存在
  • 如果条件失败(因为另一个客户端在您执行原始操作后更改了列表 get_item),您可以删除插入的新项目,然后重新开始。

读取(列出)列表中的项目

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

更新列表中的项目

  • update_item 项目

删除列表中的项目

  • get_item 列表的 sortOrder
  • update_item sortOrder 具有递增的版本和新的 order 属性,条件是预期的版本号仍然存在
  • delete_item要删除的项目

对现有列表进行排序

  • update_item sortOrder 项,以预期的 version 为条件。预期的 version 是从客户端上次查询列表项开始的

另一种方法:TransactWriteItems

如果项目按排序顺序存储很重要,您可以利用 TransactWriteItems,这将允许您在单个事务中批量写入最多 25 个项目。如果您的待办事项列表不超过 25 个项目,这将是一个更简单的解决方案,以性能更差的插入和排序为代价来提高列表读取的性能。 25 个项目限制是这个的主要问题。

替代解决方案

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

示例table:

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

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

  • 更新:
    • TTL 到现在 + 间隔(例如 10 秒)
    • lock_key(客户端提供的随机字符串)
    • 增加 version 个数
  • 条件是:
    • TTL < 现在(即任何先前的锁都已过期)

刷新客户端已获取的锁(即如果接近 10 秒限制):

  • 更新:
    • TTL 到现在 + 一个间隔(例如再过 10 秒)
    • 增加 version
  • 条件是:
    • lock_key == 预期 lock_key(即它没有过期并被另一个客户端获取)

插入或附加项目或更新排序顺序时需要获取锁。如果您只是更新项目的其他(非排序键)属性,则不一定需要锁定。

如果你正在读取列表,你可以在不获取锁的情况下乐观地查询,但是你应该在之前和之后读取锁以确保它没有同时被获取,因为这可能会导致列表不一致,所以在这种情况下你会重试。这就是 lock.

上的 version 属性的原因

所以:

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

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

阅读时

  • 读锁
  • 如果 TTL > 现在
    • 正在进行更新,请等到 TTL 后重试
  • 否则如果 TTL < 现在:
    • 读取列表项(即查询)
    • 再次读锁。如果版本发生变化,则查询的项目可能不一致,因此重新开始。如果它没有改变,那么客户端可以相信返回的结果是一致的。

使用锁的问题是,在更新列表时,其他客户端将无法一致地读取。根据您的用例,这可能是 acceptable。如果没有,您可以创建一个事务一致的缓存层。缓存的实现可能超出了你的问题范围:)

所有这些都假设您的待办事项列表不限于 25 个项目,否则您可以使用上述 TransactWriteItems