如何在 DynamoDb 中模拟手工排序的 ToDo 列表?
How to model hand-sorted ToDo list in DynamoDb?
排序就像您的日常待办事项应用一样,用户可以在其中手动设置项目的顺序。
到目前为止我的想法:
如果我们在单个 Dynamo 记录中使用列表来存储数据,排序很简单,但是:
- 更新项目意味着更新整个项目列表。
- 列表大小限制为 400KB
如果我们将每个项目保存为 DynamoDb 中的一条记录,我们将需要在每次排序或保存项目时指定一个 "weight" 值,这可能会导致竞争条件。想到的一个是存储新项目时:
- 阅读最新项目索引-答案:21
- (在索引 21 处写入一个项目,但来自不同的并发过程)
- 在索引 21 处写入新项目。-错误:21 已被占用
请参阅下面的替代解决方案 - 响应要求在索引中维护列表顺序的 OP 评论。
一个解决方案
- 按列表
对您的 table 进行分区
- 将每个列表项存储为单独的项
- 使用版本号
存储一个单独的 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
。
排序就像您的日常待办事项应用一样,用户可以在其中手动设置项目的顺序。
到目前为止我的想法:
如果我们在单个 Dynamo 记录中使用列表来存储数据,排序很简单,但是:
- 更新项目意味着更新整个项目列表。
- 列表大小限制为 400KB
如果我们将每个项目保存为 DynamoDb 中的一条记录,我们将需要在每次排序或保存项目时指定一个 "weight" 值,这可能会导致竞争条件。想到的一个是存储新项目时:
- 阅读最新项目索引-答案:21
- (在索引 21 处写入一个项目,但来自不同的并发过程)
- 在索引 21 处写入新项目。-错误:21 已被占用
请参阅下面的替代解决方案 - 响应要求在索引中维护列表顺序的 OP 评论。
一个解决方案
- 按列表 对您的 table 进行分区
- 将每个列表项存储为单独的项
- 使用版本号 存储一个单独的
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
。