中級読了 2 分

DynamoDB のアトミックカウンター:ADD の仕組みと限界

アトミックカウンターは、1回のUpdateItem呼び出しでその場で増やす数値属性です — 先読みなし、read-modify-writeの競合なし。DynamoDBは各インクリメントを到着順に 適用し、2つのライターが互いのカウントを踏み潰すことを決して許しません。

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

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

  • 1回の呼び出しでインクリメントするにはADD(またはSET x = x + :n)を使います。 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句を付けたUpdateItemを1回送ります。

# 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: Using update expressions

ADD vs SET +:どちらか一方を選ぶ

2つの式は同じ算術をします。AWSは一般的な用途にSETを推奨しています。他のSET アクションと組み合わさり、より明示的に読めるからです。(AWS: Using update expressions

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から無料でシードします。

インクリメント1回あたりの書き込みコスト

us-east-1のオンデマンドでは、ADDを伴う各UpdateItemは書き込み後の項目サイズの 1KBあたり1 WCU(切り上げ)を課金します。900バイトの統計行なら、記録される 再生1件につき1 WCUです。10件の並行再生は合計で10 WCUになり、1 WCUには なりません。カウンターをパーティションにまたいでシャード化するとスループットの上限は 動きますが、項目ごとのWCUの計算は変わりません。行のサイズは 項目サイズ計算ツールで見積もり、ホットな 経路のレートは料金計算ツールで見積もりましょう。

DynoTable でやってみる

統計項目を開いてライブのカウンターを確認し、それからSQL WorkbenchでSUMGROUP BYを使ってシャード化されたカウンターを集計し、すべての STATS#TOTAL#0..N行にまたがる合計を見ます。インクリメントそのものを下書きするには、 Web版のDynamoDB Expression Builderを使って、 名前と値を含めてADDUpdateItem式を組み立てます。

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

ここが本番でチームに刺さる部分です。アトミックカウンターはUpdateItemが実行される たびにインクリメントします。(AWS: Working with items

ネットワークの瞬断を思い浮かべてください。インクリメントを送り、レスポンスが 戻ってくる前に接続が切れて、それが届いたかどうか分かりません。あなたは再試行します。 最初の呼び出しが_成功していた_なら、あなたは今その再生を2回数えたことになります。

動画のビュー数ならそれで問題ありません — 100万再生の中の数件の二重カウントは 誰も傷つけませんし、AWSはこの「訪問者を追跡する」まさにこのケースをアトミック カウンターの典型的な用途と呼んでいます。(AWS: Working with items

正確でなければならないもの — 売り越せる在庫、二重に使えるクレジット、壊せる残高 — にとってはそれは問題です。そこでは条件付き更新に手を伸ばしましょう。

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

条件付き更新は、_変更している属性そのものを条件にすれば_冪等です。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: Working with items

代償は並行性です。2つのライターが同じ条件で競うと、片方が勝ち、片方は再試行の ためにConditionalCheckFailedExceptionを受け取ります — 無条件カウンターの スループットを正確さと交換したのです。正確で競合の多いカウンターには、それが 正しいトレードです。ビュー数にはやりすぎです。

落とし穴

  • 1つの 単一のカウンター行は1つのパーティション キーです。バズった動画がVID#9f3a / STATS#TOTALを叩き続けると、パーティション ごとの書き込み上限にぶつかることがあります。シャード化しましょう。書き込みを STATS#TOTAL#0..Nにまたいで分散させ、読み込みで合計します。
  • バッチインクリメントはありません。 BatchWriteItemはput/deleteのみで、 は実行できません。カウンターはUpdateItemを通り、 1呼び出しにつき1項目です。複数のカウンターをアトミックに増やす必要があるなら、 TransactWriteItemsが最大100項目に対してUpdateアクションを1リクエストで実行 します。書き込みコストはおよそ2倍です。
  • ADDは数値とセットのみ。 文字列やブール値には触れません — それはSETです。 完全な属性モデルについてはDynamoDBのデータ型を参照して ください。

次のステップ

アトミックカウンターは書き込みパターンです。集計をどう_読み_戻すかはモデリングの 問題です — 統計項目を親の隣に保つことについてはシングルテーブル設計を、 シャード化されたカウンターの集計がQueryのままであるように Query vs Scanを参照してください。

DynamoDB Expression Builderでインクリメントを 下書きしてコピーし、それからDynoTableを試すと、自分のテーブルに対して アトミックな更新を実行して、カウントが動くのを見られます。

更新日