进阶阅读约 2 分钟

DynamoDB 原子计数器:ADD 如何工作,以及何时失效

原子计数器是一个数值属性,你用单次 UpdateItem 调用就地增减它 —— 无需先读取,也没有读-改-写竞争。DynamoDB 按 到达顺序应用每一次增量,绝不让两个写入者互相覆盖对方的 计数。

什么是 DynamoDB 原子计数器?

DynamoDB 原子计数器是一个数值属性,你用单次 UpdateItem 调用、通过 ADD(或 SET x = x + :n)更新表达式就地对它做增量。DynamoDB 在服务器端读取、相加并写回该值,因此并发的写入者会串行化而不会丢失更新 —— 但它不具幂等性,因此一次被重试的调用会增量两次。

  • ADD(或 SET x = x + :n)在一次调用中做增量。 DynamoDB 在 服务器端读取、相加并写回 —— 并发调用者串行化,无丢失更新。
  • 无需先读取。 从 SQL 过来你会先 SELECTUPDATE;这里你完全跳过 读取,且该操作在并发下依然安全。
  • 原子计数器 具幂等性。 一次被重试的 UpdateItem 会再次 增量。如果你无法容忍多计或少计,请使用
  • ADD 作用于一个不存在的属性时从 0 开始,因此第一次增量 就能正常工作 —— 无需播种写入。

读-改-写的问题

假设你追踪一个视频的观看次数。天真的直觉,直接来自 SQL,是: GetItem,在你的应用里加一,把新的总数 PutItem 写回去。

两个观众同时点击播放。两者都读到 views = 41。两者都写入 42。你 只计了一次观看,而不是两次。那是一次丢失更新 —— 经典的并发 大坑,而且它直到你有了流量才会显现。

在 SQL 里你会用 UPDATE videos SET views = views + 1 来避开它,把 算术推进数据库。DynamoDB 有同样的招数,而且这正是 原子计数器的全部意义。

在一次调用中做增量

对每个视频的统计项目建模。分区键 VID#<id>,排序键 STATS#TOTAL, 带一个数值 play_count

PKSKplay_count
"VID#9f3a""STATS#TOTAL"41

要记录一次播放,发送一次带 ADD 子句的 UpdateItem

# UpdateItem
Key               PK = "VID#9f3a", SK = "STATS#TOTAL"
UpdateExpression  ADD play_count :one
Values            :one = 1

DynamoDB 读取 play_count,加上 1,并在单次服务器端操作内写回 结果。没有让另一个写入者插进来的窗口。十次 并发播放每次都产生 +10 —— 这就是“原子”为你买来的东西。

你可以用 DynamoDB 表达式构建器构建并复制这个确切的表达式 —— 名称、值以及全部四种 子句类型。

ADD 即便在 play_count 尚不存在时也能工作:DynamoDB 把一个缺失的 数值属性当作 0,因此第一次播放会以 1 创建它。无需单独的播种 写入。(AWS:使用更新表达式

ADD 与 SET +:选一个

两个表达式做同样的算术。AWS 推荐通用情况下用 SET, 因为它能与其他 SET 动作组合,且读起来更明确。(AWS: 使用更新表达式

ADD play_count :oneSET play_count = play_count + :one
缺失的属性创建它,从 0 开始报错 —— 需要 if_not_exists
数据类型仅数字和集合数字(及更多)经由 SET
SET 组合单独的子句一个 SET 子句,逗号分隔
AWS 建议对计数器没问题推荐的默认选择

如果属性可能不存在而你想用 SET,请守卫它: SET play_count = if_not_exists(play_count, :zero) + :one。用 ADD 你就能 跳过那一步 —— 它免费从 0 播种。

每次增量的写入成本

us-east-1 的按需模式下,每一次带 ADDUpdateItem 都按写入后项大小 每 KB 1 WCU 计费(向上取整)。一行 900 字节的统计行,每登记一次播放要花 1 WCU;十次并发播放合起来仍然落成 10 WCU,而不是一次。把计数器分片到多个 分区上,挪动的是吞吐上限,并不改变按项计算的 WCU 账。用 项大小计算器给这一行定尺寸,再用 定价计算器按行费率算热路径。

在 DynoTable 中操作

打开统计项目以查看实时计数器,然后在 SQL Workbench 里用 SUMGROUP BY 汇总一个分片计数器,看看跨每一个 STATS#TOTAL#0..N 行的总数。要草拟增量本身,用 Web 版的 DynamoDB 表达式构建器来组合 ADD UpdateItem 表达式,名称和值都包括在内。

陷阱:计数器不具幂等性

这里是在生产环境中会咬伤团队的部分。原子计数器每次 UpdateItem 运行时都会增量。(AWS:使用项目

设想一次网络抖动:你发送了增量,连接在响应 返回之前断掉了,而你不知道它是否落地了。你重试。如果 第一次调用 确实 成功了,你现在就把那次播放计了两次。

对于视频观看次数那没问题 —— 百万次播放里几次重复计数不会伤害 任何人,且 AWS 把这个确切的“追踪访客”场景称为原子计数器的 典型用法。(AWS:使用项目

对于任何必须精确的东西,它就行了:可能被超卖的库存、 可能被重复消费的积分、可能被破坏的余额。那里,请动用一个 条件更新。

当你需要精确性时:条件更新

如果你以正在更改的同一个属性作为条件,条件更新就是 幂等的。把 play_count 增量到 42,但仅当它当前为 41 时:

# UpdateItem
Key                  PK = "VID#9f3a", SK = "STATS#TOTAL"
UpdateExpression     SET play_count = :next
ConditionExpression  play_count = :current
Values               :next = 42, :current = 41

现在一次重试是安全的:如果第一次写入已经把 play_count 移到了 42,第二次时条件 play_count = 41 就会失败,什么都不会改变。(AWS: 使用项目

代价是并发性。两个写入者在同一个条件上竞争,意味着一个胜出, 一个得到一个 ConditionalCheckFailedException 去重试 —— 你用无条件 计数器的吞吐量换取了正确性。对于精确、有争用的 计数器,那是正确的取舍。对于观看次数,它是杀鸡用牛刀。

陷阱

  • 单个 单个计数器行就是一个分区键。一个疯传的视频 猛击 VID#9f3a / STATS#TOTAL 会撞上每分区的写入上限。 给它分片:把写入分散到 STATS#TOTAL#0..N,并在读取时求和。
  • 无批量增量。 BatchWriteItem 只做 put/delete —— 它无法运行 。计数器要走 UpdateItem, 每次调用一个项目。如果你必须原子地增减若干计数器, TransactWriteItems 可以在一个请求中对最多 100 个项目运行 Update 动作, 写入成本约为双倍。
  • ADD 仅限数字和集合。 它不会碰字符串或布尔值; 那是 SET 的活。参见 DynamoDB 数据类型了解完整的 属性模型。

后续步骤

原子计数器是一种写入模式;你如何 读回 聚合是一个建模 问题 —— 参见单表设计以把统计项目 保持在其父项目旁边,以及 Query 与 Scan,好让 汇总一个分片计数器仍是一次 Query

DynamoDB 表达式构建器里草拟并复制增量,然后 试用 DynoTable,对你自己的表运行原子更新并 看着计数变化。

更新于