DynamoDB 原子計數器:ADD 如何工作,以及何時失效
原子計數器是一個數值屬性,你用單次
UpdateItem 呼叫就地增減它 —— 無需先讀取,也沒有讀-改-寫競爭。DynamoDB 按
到達順序應用每一次增量,絕不讓兩個寫入者互相覆蓋對方的
計數。
什麼是 DynamoDB 原子計數器?
DynamoDB 原子計數器是一個數值屬性,你用單次 UpdateItem 呼叫、透過 ADD(或 SET x = x + :n)更新運算式就地對它做增量。DynamoDB 在伺服器端讀取、相加並寫回該值,因此並行的寫入者會序列化而不會丟失更新 —— 但它不具冪等性,因此一次被重試的呼叫會增量兩次。
- 用
ADD(或SET x = x + :n)在一次呼叫中做增量。 DynamoDB 在 伺服器端讀取、相加並寫回 —— 並行呼叫者序列化,無丟失更新。 - 無需先讀取。 從 SQL 過來你會先
SELECT再UPDATE;這裡你完全跳過 讀取,且該操作在並行下依然安全。 - 原子計數器 不 具冪等性。 一次被重試的
UpdateItem會再次 增量。如果你無法容忍多計或少計,請使用。 ADD作用於一個不存在的屬性時從 0 開始,因此第一次增量 就能正常工作 —— 無需播種寫入。
讀-改-寫的問題
假設你追蹤一個影片的觀看次數。天真的直覺,直接來自 SQL,是:
GetItem,在你的應用里加一,把新的總數 PutItem 寫回去。
兩個觀眾同時點選播放。兩者都讀到 views = 41。兩者都寫入 42。你
只計了一次觀看,而不是兩次。那是一次丟失更新 —— 經典的並行
大坑,而且它直到你有了流量才會顯現。
在 SQL 裡你會用 UPDATE videos SET views = views + 1 來避開它,把
算術推進資料庫。DynamoDB 有同樣的招數,而且這正是
原子計數器的全部意義。
在一次呼叫中做增量
對每個影片的統計項目建模。分割區索引鍵 VID#<id>,排序索引鍵 STATS#TOTAL,
帶一個數值 play_count:
| PK | SK | play_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 :one | SET 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 的按需模式下,每次帶 ADD 的 UpdateItem 都按寫入後項目大小
每 KB 計 1 個 WCU(向上取整)計費。一行 900 位元組的統計資料,每登記
一次播放就要 1 個 WCU;十次併發播放照樣總共落成 10 個 WCU,而不是一個。
把計數器分片到多個分割區只會移動吞吐上限,不會改變每個項目的 WCU 算法。用
項目大小計算器量一量這行有多大,再用
定價計算器給熱路徑估個價。
在 DynoTable 中操作
開啟統計項目以檢視實時計數器,然後在 SQL Workbench 裡用 SUM 和
GROUP 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,對你自己的表執行原子更新並 看著計數變化。