中級読了 2 分

DynamoDB のアトミックカウンタ

アトミックカウンタは、単一の UpdateItem 呼び出しでその場で増やす数値属性です — 先に読まず、read-modify-write の競合もありません。DynamoDB は各インクリメントを到着順に適用し、2つの書き込み側が互いのカウントを上書きすることを決して許しません。

DynamoDB のアトミックカウンタとは?

DynamoDB のアトミックカウンタは、ADD(または SET x = x + :n)更新式を使った単一の UpdateItem 呼び出しでその場でインクリメントする数値属性です。DynamoDB がサーバー側で値を読み、加え、書き込むため、並行する書き込み側は更新を失うことなく直列化されます — ただし冪等ではないので、リトライされた呼び出しは2回インクリメントしてしまいます。

  • ADD(または SET x = x + :n)を使って1回の呼び出しでインクリメントする。 DynamoDB はサーバー側で読み、加え、書きます — 並行する呼び出し側は直列化され、更新の損失はありません。
  • 先に読まない。 SQL なら SELECT してから UPDATE しますが、ここでは読み取りを完全に省き、それでも操作は並行下で安全です。
  • アトミックカウンタは冪等ではない リトライされた UpdateItem は再びインクリメントします。過剰カウントや過少カウントを許容できないなら、条件付き更新を使いましょう。
  • 存在しない属性への ADD は0から始まるので、最初のインクリメントがそのまま動きます — シード書き込みは不要です。

read-modify-write の問題

動画の再生回数を追跡するとします。SQL からそのまま来た素朴な発想は: GetItem、アプリで1を加え、新しい合計を PutItem で書き戻す、です。

2人の視聴者が同時に再生を押します。両方が views = 41 を読みます。両方が 42 を書きます。あなたは2回ではなく1回の再生を数えたことになります。それが更新の損失 — 古典的な並行性の罠で、トラフィックが出るまで姿を現しません。

SQL なら UPDATE videos SET views = views + 1 でそれをかわし、算術をデータベースに押し込みます。DynamoDB にも同じ手があり、それがアトミックカウンタの肝そのものです。

1回の呼び出しでインクリメントする

動画ごとの統計アイテムをモデル化します。パーティションキー VID#<id>、ソートキー STATS#TOTAL、数値の play_count:

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

再生を記録するには、ADD 句を持つ1回の UpdateItem を送ります:

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

DynamoDB は play_count を読み、1 を加え、単一のサーバー側操作の中で結果を書きます。別の書き込み側が割り込む窓はありません。10回の並行再生は毎回 +10 を生みます — それが「アトミック」が買うものです。

この正確な式 — 名前、値、4種類すべての句タイプ — は DynamoDB Expression Builder で構築してコピーできます。

ADDplay_count がまだ存在しなくても動きます: DynamoDB は欠けている数値属性を 0 として扱うので、最初の再生がそれを 1 で作成します。別途のシード書き込みはありません。(AWS: 更新式の使用

ADD vs SET +: どちらか選ぶ

2つの式が同じ算術を行います。AWS は一般的な用途には SET を推奨します。他の SET アクションと合成でき、より明示的に読めるからです。(AWS: 更新式の使用

ADD play_count :oneSET play_count = play_count + :one
欠けた属性0 から始めて作成エラー — if_not_exists が必要
データ型数値とセットのみSET 経由で数値(とそれ以上)
SET との結合別個の句1つの SET 句、カンマ区切り
AWS のガイダンスカウンタには問題なし推奨デフォルト

属性が存在しないかもしれず、かつ SET を使いたい場合はガードします: SET play_count = if_not_exists(play_count, :zero) + :oneADD ならそれを省けます — 0 から無料でシードします。

DynoTable でやってみる

アイテムを開き、play_count を編集すると、JSON を手書きせずにアトミックなインクリメントが着地する様子を観察できます — 更新パネルが ADD 式を出力し、コミットした瞬間に新しい値を表示します。

罠: カウンタは冪等ではない

ここが本番でチームを噛む部分です。アトミックカウンタは UpdateItem が実行されるたびにインクリメントします。(AWS: アイテムの操作

ネットワークの瞬断を想像してください: インクリメントを送り、レスポンスが返る前に接続が切れ、それが着地したかどうかわかりません。リトライします。最初の呼び出しが実際に成功していたら、あなたはその再生を二重に数えたことになります。

動画再生回数ならそれで問題ありません — 百万回の再生で数件の二重カウントが誰かを傷つけることはなく、AWS はまさにこの「訪問者を追跡する」ケースをアトミックカウンタの正典的な用途と呼んでいます。(AWS: アイテムの操作

正確でなければならないものには 問題ありです: 売り過ぎ得る在庫、二重消費し得るクレジット、破損し得る残高。そこでは条件付き更新に手を伸ばしましょう。

正確さが必要なとき: 条件付き更新

条件付き更新は、変更する属性と同じ属性を条件にすれば冪等です。play_count42 にインクリメントしますが、現在 41 の場合に限ります:

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

これでリトライは安全です: 最初の書き込みが既に play_count42 に動かしていたら、2回目には条件 play_count = 41 が失敗し、何も変わりません。(AWS: アイテムの操作

コストは並行性です。同じ条件で競う2つの書き込み側は、一方が勝ち、もう一方はリトライ用の ConditionalCheckFailedException を受け取ります — 無条件カウンタのスループットを正確さと引き換えにしたのです。正確で競合の多いカウンタには、それが正しいトレードです。再生回数にはやり過ぎです。

落とし穴

  • 1つのホットアイテム。 単一のカウンタ行は1つのパーティションキーです。VID#9f3a / STATS#TOTAL を叩くバズった動画は、パーティション単位の書き込み上限に達し得ます。シャーディングしましょう: 書き込みを STATS#TOTAL#0..N に分散させ、読み取り時に合計します。
  • バッチインクリメントはない。 BatchWriteItem は put/delete のみで — 更新式を実行できません。カウンタは UpdateItem を通り、呼び出しごとに1アイテムです。
  • ADD は数値とセットのみ。 文字列やブール値には触れません。それは SET です。完全な属性モデルは DynamoDB のデータ型を参照してください。

次のステップ

アトミックカウンタは書き込みパターンです。集計をどう読み戻すかはモデリングの問題です — 統計アイテムを親の隣に保つにはシングルテーブル設計を、シャーディングされたカウンタの集約が Query のままであり続けるには Query と Scan の比較を参照してください。

DynamoDB Expression Builder でインクリメントを下書きしてコピーし、DynoTable を試して、自分のテーブルに対してアトミックな更新を実行し、カウントが動く様子を観察しましょう。

更新日