中階閱讀時間 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,對你自己的表執行原子更新並 看著計數變化。

已更新