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です。
| PK | SK | play_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で構築してコピー できます。
ADDはplay_countがまだ存在しないときでも動きます。DynamoDBは存在しない数値属性を
0として扱うので、最初の再生がそれを1で作成します。別のシード書き込みは不要です。
(AWS: Using update expressions)
ADD vs SET +:どちらか一方を選ぶ
2つの式は同じ算術をします。AWSは一般的な用途にSETを推奨しています。他のSET
アクションと組み合わさり、より明示的に読めるからです。(AWS: Using update
expressions)
ADD play_count :one | SET play_count = play_count + :one | |
|---|---|---|
| 属性が存在しない | 0から始めて作成 | エラー — if_not_existsが必要 |
| データ型 | 数値とセットのみ | 数値(およびそれ以上)をSET経由で |
SETとの併用 | 別の句 | 1つのSET句、カンマ区切り |
| AWSのガイダンス | カウンターには問題なし | 推奨されるデフォルト |
属性が存在しないかもしれず、SETを使いたいなら、ガードします。
SET play_count = if_not_exists(play_count, :zero) + :one。ADDならそれをスキップ
できます — 0から無料でシードします。
インクリメント1回あたりの書き込みコスト
us-east-1のオンデマンドでは、ADDを伴う各UpdateItemは書き込み後の項目サイズの
1KBあたり1 WCU(切り上げ)を課金します。900バイトの統計行なら、記録される
再生1件につき1 WCUです。10件の並行再生は合計で10 WCUになり、1 WCUには
なりません。カウンターをパーティションにまたいでシャード化するとスループットの上限は
動きますが、項目ごとのWCUの計算は変わりません。行のサイズは
項目サイズ計算ツールで見積もり、ホットな
経路のレートは料金計算ツールで見積もりましょう。
DynoTable でやってみる
統計項目を開いてライブのカウンターを確認し、それからSQL WorkbenchでSUMと
GROUP BYを使ってシャード化されたカウンターを集計し、すべての
STATS#TOTAL#0..N行にまたがる合計を見ます。インクリメントそのものを下書きするには、
Web版のDynamoDB Expression Builderを使って、
名前と値を含めてADDのUpdateItem式を組み立てます。
罠:カウンターは冪等ではない
ここが本番でチームに刺さる部分です。アトミックカウンターはUpdateItemが実行される
たびにインクリメントします。(AWS: Working with items)
ネットワークの瞬断を思い浮かべてください。インクリメントを送り、レスポンスが 戻ってくる前に接続が切れて、それが届いたかどうか分かりません。あなたは再試行します。 最初の呼び出しが_成功していた_なら、あなたは今その再生を2回数えたことになります。
動画のビュー数ならそれで問題ありません — 100万再生の中の数件の二重カウントは 誰も傷つけませんし、AWSはこの「訪問者を追跡する」まさにこのケースをアトミック カウンターの典型的な用途と呼んでいます。(AWS: Working with items)
正確でなければならないもの — 売り越せる在庫、二重に使えるクレジット、壊せる残高 — にとってはそれは問題です。そこでは条件付き更新に手を伸ばしましょう。
正確さが必要なとき:条件付き更新
条件付き更新は、_変更している属性そのものを条件にすれば_冪等です。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に動かしていれば、
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を試すと、自分のテーブルに対して アトミックな更新を実行して、カウントが動くのを見られます。