DynamoDB 增删改查要点

DynamoDB 作为一个数据库,其特性也落在 ACID 的框架之内。我们先来复习一下什么是 ACID1

  1. 原子性(Atomicity):不可分割性,一个具有原子性的操作要么都成功,要么都失败,不存在只有开头成功,结尾却失败;不存在只有前几个语句成功,后几句却失败。
  2. 一致性(Consistency):任何操作都需保持数据库中的数据合法、有效2,包括数据完整、不违背写入规则等。对于 DynamoDB 这样的分布式数据库来说,可能还包括主、从节点的数据相同,不发生分歧3
  3. 隔离性(Isolation):多个进程、线程同时写入数据库时,可能造成某些数据的冲突、覆盖,一定的隔离性防止冲突发生。
  4. 持久性(Durability):数据一旦提交,就会存在数据库中,不能丢失。

增、改、删

写入操作都很忌讳意外覆盖,导致数据丢失。因此要着重考察数据原子性、一致性、隔离性,防止冲突地修改。

如果你的使用场景中确定没有并发的写入,比如个人随便记录东西的数据库,那设计就很简单。

  1. 可以顺序执行 PutItemUpdateItemDeleteItem 等各种操作,直至把他们组合起来形成批量写入 BatchWriteItem

但只要有多个不同的程序操作同一个 item,尤其是同一个 item 的同一个 attribute,那冲突就有可能发生了。比如,有些进程可能读取了旧数据,隔一段时间后还在基于旧数据来更新 item,从而导致在那段时间中别人的更新被覆盖,成为 lost update4

此时可以尽量避免冲突:

  1. 使用原子性操作,将 read-modify-write[https://en.wikipedia.org/wiki/Read%E2%80%93modify%E2%80%93write] 在一个事物内完成,缩短读取数据和更改数据之间的窗口时间,比如 atomic counter(类似于 #attr = #attr + :val),不取回数据,只在数据库本身中使用数据。
  2. 如果必须取回数据、计算一番之后再修改数据,那写入时,就需要再检查一次,确保自己刚刚的计算所基于的数据是最新的,这就是 ConditionExpression 的作用。

可以基于 ConditionExpression 来检查某些特殊的字段,比如数据的版本号(version),就实现了乐观锁。

DynamoDB 中的 request 都是具有原子性的5BatchWriteItem 除外),但是原子性不意味着隔离性。一个原子性操作的过程中,别的线程并未被隔离,也在修改数据,还是有可能造成冲突。我不确定 DynamoDB 中,对同一个 item 执行多个原子性写入,是否会序列化隔离6;而且没人明确提到,带上 ConditionExpression 后,原子性的 request 就能阻止并发覆盖。但基本可以确定的是,写入都基于 leader node7,提交过的数据会立刻在后续写入中应用

在 2018 年之前,我们可能找不到 DynamoDB 对于 ACID 的完整支持,据说 DynamoDB 更关注写入的一致性和持久性,而没那么关注隔离性的实现。但是现在 DynamoDB 有了自己的 transaction 操作,可以实现序列化级别的隔离8。这种序列化包括 transcation 之间的序列化和 transaction 和普通写入的序列化9

因此

  1. 当你不允许某个 item 被冲突修改,那么所有对这个 item 的修改都用 TransactWriteItems
  2. 除此之外,你可以在一个 transaction 中修改多个 item,这些修改具有一个原子性,统一和其他请求隔离。

相比之下,之前的 DynamoDB 是无法在一个原子操作内修改多个 item 的。如果你要修改 2 个 item,就只能创建两个写入请求,结果可能是一个成功,另一个却在执行时发生冲突。以及还有更多种子情况需要你处理,你可以想象那意味着什么……

但 DynamoDB 的序列化隔离不会锁住正要修改的 item,而是发现 item 被别的 transaction 修改后让当前的修改失败(这种不基于锁的序列化,被称为“快照隔离”10),因此你需要处理相关的失败,重试或放弃。不过请注意,冲突只发生在以下情况9

  1. 当 transaction 遇到正在进行的 transaction,后来的 transaction 失败
  2. 当后来的普通写入遇到正在进行的 transaction,后来的普通写入失败

这是否间接证明对单一 item 的普通原子性写入本来就是可序列化的?因为没有关于 transaction 遇到正在进行的普通写入后会失败的描述;也就是说,他们有可能相遇,但不会失败。

总之,由简单到复杂,由一个 PutItem ,到多个请求合并成一个原子性的、transaction 之间序列化的 TransactWriteItems,这是一个大致连续的变化。情况确定了,复杂度就确定了,操作方式也就确定了。

当然,你可能也发现了:DynamoDB 中目前没有标准意义上的范围写入(WHERE一个大范围),每一个写入都需要明确 primary key。如果你要大批量、甚至全表改数据,不是一件容易的事情,所以设计表结构的时候要谨慎。

最后的最后,纵观各种写入限制11

  1. 最容易被突破的是一个 item 的数据大小不能超过 400kb(如果 item 包含 Local Secondary Index,则 index 大小包含在内),不要把 attributes 当成一个小 table,存一些总是在增长的数据(比如用户历史订单),迟早会撑爆。
  2. 如果遵守了这个限制,其他的限制都“留有余地”的:BatchWriteItemTransactWriteItems 可容纳的 25 个请求加起来也不会超过他们分别的大小限制 16 mb12 和 4 mb。

AWS 提示,DynamoDB 这种 NoSQL 和关系型数据库的一个重要区别是:你需要先想好查表的方式,然后再设计表13。因为不是每一个 attribute(类似于关系型数据库中的 column)都可以当作查询的条件;没有事先建立 index 的查询,在 DynamoDB 中非常耗费资源。

所以,我们先来简单介绍一下常用的查询模式对应到 DynamoDB 中应该怎样建表:

  1. 1 对 1:
    • 利用 composite key14 建表或建立 index,一个 PK(partition key,下同) 下留一个 SK(sort key,下同) 来存一对一关系
  2. 1 对多:
    • 同理,在 table 或 index 的一个 PK 下,存多个 SK,就构成了一对多关系;
    • 同时,一对多经常需要排序,比如一个用户的多个订单按时间排序,那么你可以把时间当作 SK,或者是多个字段聚合在 SK 中15,比如时间和订单号的聚合, 2020-04-01T12:34:56.000Z|order12345,这样数据会按顺序存储,返回时也按时间顺序返回
  3. 多对 1:
    • 可以把“1”存在多个 items 的 attribute 中
  4. 多对多:
    • 首先建立一个 1 对多,这样 1 个 PK 就对应多个 items,再把 items 中的一个 attribute,比如 project_id 作为新 PK’,建立一个新的 index’,这样 project_id 相同的 items 就会聚合在 index’ 的同一个 partition 中,实现通过 attribute 定位 items(不然 DynamoDB 是无法像关系型数据库那样,对任意 column 做查询来定位 rows 的)。这种在 attribute 上建 index 来查原始 items 的操作或可称为 inverted index18。可能的情景是:一个用户参与多个项目,一个项目中有多个用户。

按照文档的定义,这种设计方式可以被称为 Adjacency List16

另外,以上所有模式中,你都可以把数据存入 attributes 来构建数据间的关系,比如一个用户有 5 个收货地址,你可以把他们存入名为 address 的 attribute 中,但是如果用户有 1000 个收货地址,item 的总大小可能就会超过限制,就不能存在 attribute 中了。

接下来再看如何查询这些数据。

和 SQL 只用 SELECT 一个命令做查询不同,目前 DynamoDB 有 5 种查询指令:GetItemBatchGetItemQueryScanTransactGetItems。我们对他们的查询能力做一下分类。

  1. 扫描所有 partition:Scan,唯一一个可以全表扫描的接口,和关系型数据库可以相对轻松的全表扫描不同,Scan 在多个 partition 之间扫描,非常耗费资源。我猜,这种情况主要发生在你没有对某些 attribute 建 index 时。
  2. 扫描一个 partition:Query,扫描顺序和 SK 顺序相同(或完全倒序),返回此 PK 下的多个 items。
  3. 查找确定的 primary key:GetItemBatchGetItemTransactGetItems,他们都只能利用 primary key 做查找,无法扫描。

大致可以看出,这几个接口并不是可以随意选择的,使用场景很确定。

也大致可以看出,我们总是要把已知数据当作 PK,如果不知道的话,就只能 Scan 来检查所有 PK;知道了 PK,就可以查询由此引出的其他数据。

以上就是“查”的主要操作。但基于 DynamoDB 的特性,“查”的结果还受一致性和隔离性的影响。

  1. 可序列化读:TransactGetItems 实现了可序列化读,一个 transaction 中读取同一批 item,数据一定是一样的(但好像没必要读取同一批 item);如果读取过程中相关的 item 被其他 transaction 或普通写入修改,会抛出异常9</small>
  2. 提交读、不可重复读:DynamoDB 没有“脏读”,只能读到提交过的数据;但同时,其他线程的提交可能使 GetItemBatchGetItemQueryScan 两次读到的数据不同
  3. “提交也暂时不能读”:DynamoDB 是一种分布式数据库17,读取数据会访问“从节点”,而从节点和主节点的数据同步可能会有延迟7,因此提交过的数据也可能暂时读取不到。当然 AWS 自己管这叫“Eventually Consistent Read”3,是同一事物的另一面。如果要想获得强一致性的读取,可以使用“Strongly Consistent Read”。

以上,就是我对 DynamoDB 增删改查的一些总结。整理的大致思路是:需求决定操作。希望对您有帮助。

最后,如果我的文章展现了某些漏洞,请告诉我,但不要用 Hack 我的方式告诉我。好人一生平安。

1. Wikipedia Contributors. (2020, March 20). ACID. Retrieved March 25, 2020, from Wikipedia website: https://en.wikipedia.org/wiki/ACID‌
2. Wikipedia Contributors. (2019, June 19). Consistency (database systems). Retrieved March 25, 2020, from Wikipedia website: https://en.wikipedia.org/wiki/Consistency_(database_systems)‌
3. Read Consistency - Amazon DynamoDB. (2020). Retrieved April 5, 2020, from Amazon.com website: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html‌
4. Wikipedia Contributors. (2019, December 12). Concurrency control. Retrieved March 24, 2020, from Wikipedia website: https://en.wikipedia.org/wiki/Concurrency_control#Why_is_concurrency_control_needed.3F‌
5. AWS Developer Forums: Is “UpdateItem” an atomic operation wrt … (2015). Retrieved March 24, 2020, from Amazon.com website: https://forums.aws.amazon.com/thread.jspa?threadID=179999‌
6. Working with Items and Attributes - Amazon DynamoDB. (2020). Retrieved March 24, 2020, from Amazon.com website: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithItems.html#WorkingWithItems.AtomicCounters‌
7. Amazon Web Services. (2020). AWS re:Invent 2018: Amazon DynamoDB Under the Hood: How We Built a Hyper-Scale Database (DAT321) [YouTube Video]. Retrieved from https://www.youtube.com/watch?time_continue=160&v=yvBR71D0nAQ‌
8. New – Amazon DynamoDB Transactions | Amazon Web Services. (2018, November 27). Retrieved March 25, 2020, from Amazon Web Services website: https://aws.amazon.com/blogs/aws/new-amazon-dynamodb-transactions/‌
9. Amazon DynamoDB Transactions: How It Works - Amazon DynamoDB. (2020). Retrieved March 25, 2020, from Amazon.com website: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transaction-apis.html#transaction-isolation‌
10. Wikipedia Contributors. (2020, March 3). Isolation (database systems). Retrieved March 24, 2020, from Wikipedia website: https://en.wikipedia.org/wiki/Isolation_(database_systems)#Serializable‌
11. Limits in Amazon DynamoDB - Amazon DynamoDB. (2020). Retrieved March 25, 2020, from Amazon.com website: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html‌
12. Package dynamodb. (2012). Retrieved March 25, 2020, from Godoc.org website: https://godoc.org/github.com/aws/aws-sdk-go/service/dynamodb#DynamoDB.BatchWriteItem‌
13. NoSQL Design for DynamoDB - Amazon DynamoDB. (2020). Retrieved April 5, 2020, from Amazon.com website: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-general-nosql-design.html‌
14. Best Practices for Designing and Using Partition Keys Effectively - Amazon DynamoDB. (2020). Retrieved April 5, 2020, from Amazon.com website: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html‌
15. Best Practices for Using Sort Keys to Organize Data - Amazon DynamoDB. (2020). Retrieved April 5, 2020, from Amazon.com website: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-sort-keys.html‌
16. Best Practices for Managing Many-to-Many Relationships - Amazon DynamoDB. (2020). Retrieved April 5, 2020, from Amazon.com website: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-adjacency-graphs.html‌
17. What Is Amazon DynamoDB? - Amazon DynamoDB. (2020). Retrieved April 5, 2020, from Amazon.com website: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html‌
18. Wikipedia Contributors. (2020, February 29). Inverted index. Retrieved April 5, 2020, from Wikipedia website: https://en.wikipedia.org/wiki/Inverted_index‌