DynamoDB 作为一个数据库,其特性也落在 ACID 的框架之内。我们先来复习一下什么是 ACID1。
- 原子性(Atomicity):不可分割性,一个具有原子性的操作要么都成功,要么都失败,不存在只有开头成功,结尾却失败;不存在只有前几个语句成功,后几句却失败。
- 一致性(Consistency):任何操作都需保持数据库中的数据合法、有效2,包括数据完整、不违背写入规则等。对于 DynamoDB 这样的分布式数据库来说,可能还包括主、从节点的数据相同,不发生分歧3。
- 隔离性(Isolation):多个进程、线程同时写入数据库时,可能造成某些数据的冲突、覆盖,一定的隔离性防止冲突发生。
- 持久性(Durability):数据一旦提交,就会存在数据库中,不能丢失。
增、改、删
写入操作都很忌讳意外覆盖,导致数据丢失。因此要着重考察数据原子性、一致性、隔离性,防止冲突地修改。
如果你的使用场景中确定没有并发的写入,比如个人随便记录东西的数据库,那设计就很简单。
- 可以顺序执行
PutItem
,UpdateItem
,DeleteItem
等各种操作,直至把他们组合起来形成批量写入BatchWriteItem
但只要有多个不同的程序操作同一个 item,尤其是同一个 item 的同一个 attribute,那冲突就有可能发生了。比如,有些进程可能读取了旧数据,隔一段时间后还在基于旧数据来更新 item,从而导致在那段时间中别人的更新被覆盖,成为 lost update4。
此时可以尽量避免冲突:
- 使用原子性操作,将 read-modify-write[https://en.wikipedia.org/wiki/Read%E2%80%93modify%E2%80%93write] 在一个事物内完成,缩短读取数据和更改数据之间的窗口时间,比如 atomic counter(类似于
#attr = #attr + :val
),不取回数据,只在数据库本身中使用数据。 - 如果必须取回数据、计算一番之后再修改数据,那写入时,就需要再检查一次,确保自己刚刚的计算所基于的数据是最新的,这就是
ConditionExpression
的作用。
可以基于 ConditionExpression
来检查某些特殊的字段,比如数据的版本号(version),就实现了乐观锁。
DynamoDB 中的 request 都是具有原子性的5(BatchWriteItem
除外),但是原子性不意味着隔离性。一个原子性操作的过程中,别的线程并未被隔离,也在修改数据,还是有可能造成冲突。我不确定 DynamoDB 中,对同一个 item 执行多个原子性写入,是否会序列化隔离6;而且没人明确提到,带上 ConditionExpression 后,原子性的 request 就能阻止并发覆盖。但基本可以确定的是,写入都基于 leader node7,提交过的数据会立刻在后续写入中应用
在 2018 年之前,我们可能找不到 DynamoDB 对于 ACID 的完整支持,据说 DynamoDB 更关注写入的一致性和持久性,而没那么关注隔离性的实现。但是现在 DynamoDB 有了自己的 transaction 操作,可以实现序列化级别的隔离8。这种序列化包括 transcation 之间的序列化和 transaction 和普通写入的序列化9。
因此
- 当你不允许某个 item 被冲突修改,那么所有对这个 item 的修改都用
TransactWriteItems
。 - 除此之外,你可以在一个 transaction 中修改多个 item,这些修改具有一个原子性,统一和其他请求隔离。
相比之下,之前的 DynamoDB 是无法在一个原子操作内修改多个 item 的。如果你要修改 2 个 item,就只能创建两个写入请求,结果可能是一个成功,另一个却在执行时发生冲突。以及还有更多种子情况需要你处理,你可以想象那意味着什么……
但 DynamoDB 的序列化隔离不会锁住正要修改的 item,而是发现 item 被别的 transaction 修改后让当前的修改失败(这种不基于锁的序列化,被称为“快照隔离”10),因此你需要处理相关的失败,重试或放弃。不过请注意,冲突只发生在以下情况9:
- 当 transaction 遇到正在进行的 transaction,后来的 transaction 失败
- 当后来的普通写入遇到正在进行的 transaction,后来的普通写入失败
这是否间接证明对单一 item 的普通原子性写入本来就是可序列化的?因为没有关于 transaction 遇到正在进行的普通写入后会失败的描述;也就是说,他们有可能相遇,但不会失败。
总之,由简单到复杂,由一个 PutItem
,到多个请求合并成一个原子性的、transaction 之间序列化的 TransactWriteItems
,这是一个大致连续的变化。情况确定了,复杂度就确定了,操作方式也就确定了。
当然,你可能也发现了:DynamoDB 中目前没有标准意义上的范围写入(WHERE
一个大范围),每一个写入都需要明确 primary key。如果你要大批量、甚至全表改数据,不是一件容易的事情,所以设计表结构的时候要谨慎。
最后的最后,纵观各种写入限制11,
- 最容易被突破的是一个 item 的数据大小不能超过 400kb(如果 item 包含 Local Secondary Index,则 index 大小包含在内),不要把 attributes 当成一个小 table,存一些总是在增长的数据(比如用户历史订单),迟早会撑爆。
- 如果遵守了这个限制,其他的限制都“留有余地”的:
BatchWriteItem
和TransactWriteItems
可容纳的 25 个请求加起来也不会超过他们分别的大小限制 16 mb12 和 4 mb。
查
AWS 提示,DynamoDB 这种 NoSQL 和关系型数据库的一个重要区别是:你需要先想好查表的方式,然后再设计表13。因为不是每一个 attribute(类似于关系型数据库中的 column)都可以当作查询的条件;没有事先建立 index 的查询,在 DynamoDB 中非常耗费资源。
所以,我们先来简单介绍一下常用的查询模式对应到 DynamoDB 中应该怎样建表:
- 1 对 1:
- 利用 composite key14 建表或建立 index,一个 PK(partition key,下同) 下留一个 SK(sort key,下同) 来存一对一关系
- 1 对多:
- 同理,在 table 或 index 的一个 PK 下,存多个 SK,就构成了一对多关系;
- 同时,一对多经常需要排序,比如一个用户的多个订单按时间排序,那么你可以把时间当作 SK,或者是多个字段聚合在 SK 中15,比如时间和订单号的聚合,
2020-04-01T12:34:56.000Z|order12345
,这样数据会按顺序存储,返回时也按时间顺序返回
- 多对 1:
- 可以把“1”存在多个 items 的 attribute 中
- 多对多:
- 首先建立一个 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 种查询指令:GetItem
、BatchGetItem
、Query
、Scan
、TransactGetItems
。我们对他们的查询能力做一下分类。
- 扫描所有 partition:
Scan
,唯一一个可以全表扫描的接口,和关系型数据库可以相对轻松的全表扫描不同,Scan
在多个 partition 之间扫描,非常耗费资源。我猜,这种情况主要发生在你没有对某些 attribute 建 index 时。 - 扫描一个 partition:
Query
,扫描顺序和 SK 顺序相同(或完全倒序),返回此 PK 下的多个 items。 - 查找确定的 primary key:
GetItem
、BatchGetItem
、TransactGetItems
,他们都只能利用 primary key 做查找,无法扫描。
大致可以看出,这几个接口并不是可以随意选择的,使用场景很确定。
也大致可以看出,我们总是要把已知数据当作 PK,如果不知道的话,就只能 Scan
来检查所有 PK;知道了 PK,就可以查询由此引出的其他数据。
以上就是“查”的主要操作。但基于 DynamoDB 的特性,“查”的结果还受一致性和隔离性的影响。
- 可序列化读:
TransactGetItems
实现了可序列化读,一个 transaction 中读取同一批 item,数据一定是一样的(但好像没必要读取同一批 item);如果读取过程中相关的 item 被其他 transaction 或普通写入修改,会抛出异常9</small> - 提交读、不可重复读:DynamoDB 没有“脏读”,只能读到提交过的数据;但同时,其他线程的提交可能使
GetItem
、BatchGetItem
、Query
、Scan
两次读到的数据不同 - “提交也暂时不能读”: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 ↩