DynamoDB 强一致读取与最终一致读取
你更新一个项,立刻把它读回来,却拿到了旧值。写入是成功的——片刻之后同一次读取就返回了新值。没有任何东西坏掉:你撞上了 DynamoDB 默认的最终一致读取,而你可以按请求选择退出它。
这是 DynamoDB 直接交到你手里的少数几个正确性旋钮之一,而它带着一个实实在在的代价。用对它意味着要懂每种模式保证什么、它花费多少,以及在哪里根本没有强一致读取可用。
DynamoDB 中的强一致读取与最终一致读取有什么区别?
最终一致读取(默认)由任意一个副本提供服务,因此在一次写入之后它可能短暂返回陈旧数据,但成本只有一半。强一致读取用 ConsistentRead=true 按请求选择启用,会被路由到分区的主导节点,并且总是反映每一次已提交的写入——代价是 2 倍的读取容量。
- 最终一致(默认)——在一次写入之后可能短暂返回陈旧数据。最便宜的读取模式。
- 强一致——总是反映读取之前已提交的每一次写入。用
ConsistentRead=true按请求选择启用。 - 强一致读取的成本是最终一致的 2 倍。 对于相同的数据,一次强一致读取消耗的读取容量是一次最终一致读取的两倍。
- 并非处处可用。 你能在基础表和本地二级索引上拿到强一致读取。全局二级索引只支持最终一致——无法选择启用。
- 默认用最终一致。 只有在读取你自己刚刚写入的数据、且差一瞬间的陈旧就会出错时,才去用强一致。
问题:一次看不到最新写入的读取
假设你运营用户账户。一个用户修改了他的通知邮箱,你的应用写入了这次更新,确认页面立刻重新读取资料以显示新地址。在默认读取模式下,那次重新读取可能落在一个还没收到变更的副本上——于是用户看到的是他的旧邮箱,并以为保存失败了。
这个窗口很小(通常远不到一秒)而且会自行关闭。但对于一次写后立即读的确认来说,"通常正确"还不够好。这正是强一致存在的理由。
为什么会出现最终一致
DynamoDB 把每个分区存储在三个存储节点上——一个主节点和两个副本——分布在不同的可用区。一次写入在落到主节点和一个副本上时就被确认;随后它异步地传播到第三个节点。
为了分摊负载,读取可以由这三个节点中的任意一个来服务。一次最终一致读取可能命中一个还没收到你最近写入的节点——于是它返回一个略微陈旧的值。一次强一致读取会被路由到该分区的主导节点,那里总是持有最新已提交的数据,因此它绝不会返回陈旧结果。
那个复制延迟就是全部差别所在。它也解释了 2 倍成本:强一致读取无法像最终一致读取那样在副本间做负载均衡,所以 DynamoDB 按两倍的容量给它定价。
把成本说具体
读取以读取容量单元(RCU)计量,每个 RCU 覆盖最大 4 KB。对于一个 4 KB 的项,一个 RCU 可买一次强一致读取或两次最终一致读取。所以在一个热读取路径上翻开 ConsistentRead=true 会让它的读取成本翻倍——在一个高流量端点上,那是一笔你会注意到的开销条目。
在把强一致读取设为默认之前,用 DynamoDB 定价计算器针对你自己的项大小和请求速率把差别建模出来——全面付双倍代价很少值得。
强一致读取在哪里可用(在哪里不可用)
| 读取对象 | 是否强一致? |
|---|---|
| 基础表 | 是——用 ConsistentRead=true 选择启用 |
| 本地二级索引(LSI) | 是——与基础表相同的选择启用方式 |
| 全局二级索引(GSI) | 否——仅最终一致,无法覆盖 |
一个 GSI 维护它自己的一份数据副本,从基础表异步复制而来,所以它永远无法提供强一致读取。如果某个访问模式确实需要写后立即读,而你原本打算用一个 GSI 来服务它,那就是一个信号——应该改用基础表或一个 LSI 来服务它。
陷阱与下一步
- 别把强一致读取设为默认。 大多数读取能容忍一个亚秒级的陈旧窗口;处处付 2 倍是浪费的开销。
- 别指望从 GSI 做写后立即读。 它在设计上就是最终一致的——参见为什么 GSI 是最终一致的。
- 事务是强一致读取。
TransactGetItems始终是强一致的——参见 DynamoDB 事务。 - 一致性与容量相互影响。 那个 2 倍乘数直接关联到按需与预置的成本规划。
想在不写 API 调用的情况下探索你的 DynamoDB 表和索引?下载 DynoTable 直接检视你的数据。