中級読了 2 分

DynamoDB の条件式

条件式は、DynamoDB が書き込みをコミットする に、既存のアイテムに対して評価する 述語です。述語が偽なら、書き込みは拒否され、何も変わりません。それは DynamoDB が持つ、 書き込みに対する WHERE 句に最も近いもの — そして不変条件を強制する唯一の安全な方法です。

DynamoDB の条件式はどのように機能するか?

条件式は、書き込みをコミットする前に、DynamoDB がサーバー側で現在のアイテムに対して評価する述語です。真であれば書き込みは進み、偽であれば書き込みは ConditionalCheckFailedException で拒否され、何も変わりません。チェックと変更を1つのアトミック操作に畳み込むため、同時実行の呼び出し元が古い読み取りで競合することはありません。

  • それはフィルタではなくガード。 ConditionExpression は現在のアイテムに対して サーバー側で動き、偽の結果は ConditionalCheckFailedException で書き込みを失敗させます。
  • それは read-then-write を置き換える。 SELECT してから UPDATE するラウンド トリップなし — チェックと変更が1つのアトミック操作なので、2人の呼び出し元が競合できません。
  • 拒否は無料だが、実行は無料ではない。 失敗した条件付き書き込みでも書き込みキャパシティ を消費します。その保証は、それがブロックする書き込みと同じだけのコストです。

SQL から来ると、行を読み、アプリのコードでチェックし、それから更新するでしょう。 DynamoDB では、その読み取りと書き込みの間のギャップが、同時実行の呼び出し元を待ち受ける データ破損のバグです。条件式がそのギャップを閉じます。

どこに適用されるか

ConditionExpressionPutItemUpdateItemDeleteItem、そして TransactWriteItems 内の各アクションに付けられます。QueryScan の一部では ありません — それらは読み取り経路上の別物である FilterExpression を使います。

その区別が人々を混乱させるので、正確に言いましょう。

ConditionExpressionFilterExpression
経路書き込み(Put/Update/Delete読み取り(Query/Scan
失敗時の効果書き込み全体を拒否アイテムを結果から落とす
見るもの現在のアイテム、書き込み前各候補アイテム、読み取り後
コスト失敗した書き込みも課金されるフィルタされたアイテムも読み取り分は課金される

どちらもサーバー側で動きます。違いは「偽」が何をするかです。条件は変更を中止し、フィルタは すでに読み取り分を支払った行を隠すだけです。 (AWS: Condition Expressions

実際に使う関数

条件言語は小さいです。働き者たちは。

  • attribute_exists(path) / attribute_not_exists(path) — この属性はアイテムに存在 するか? 「なければ作成のみ」/「あれば更新のみ」の古典的なイディオムです。
  • 比較演算子 — =<><<=>>= — 値や別の属性に対して。
  • attribute_typebegins_withcontainssize — 型と文字列/セットのチェック。
  • BETWEEN … AND …IN (…) — 範囲とメンバーシップ。
  • ANDORNOT、括弧 — 上記を組み合わせるため。

パーティションキーに対する attribute_not_exists は、既存のアイテムを潰さない挿入のように PutItem を振る舞わせる定石です — DynamoDB には別個の「挿入」操作がないため、条件 挿入のセマンティクスそのものです。 (AWS: Comparison Operator and Function Reference

具体例:残高不足から元帳を守る

銀行の元帳を取り上げます。各口座は1つのアイテムです。

PK = "ACCT#a7f3"
SK = "BALANCE"
clearedCents = 50000
holdCents    = 0

不変条件:引き落としは利用可能残高を決してゼロ未満に押し下げてはならず、存在しない口座を 決して引き落としてはなりません。2つのルール、どちらも書き込みそのものの中で強制できます。

間違ったやり方(落とし穴)

GetItem ACCT#a7f3 / BALANCE     → clearedCents = 50000
if (50000 >= 30000) ...         ← アプリ側のチェック
UpdateItem  SET clearedCents = 20000

GetItemUpdateItem の間で、2つ目の引き落としが同じ 50000 を読み、自身のチェックを 通過し、書き込みできます。両方が成功し、口座はマイナスになります。これは read-modify-write の競合であり、どれだけアプリ側で検証しても直りません — チェックと書き込みが別々の操作 だからです。

正しいやり方

チェックを書き込みに畳み込みます。口座が存在し かつ 十分に保持していることを条件に、 30000 セントを引き落とします。

UpdateItem  ACCT#a7f3 / BALANCE
  SET clearedCents = clearedCents - :amt
  ConditionExpression:
    attribute_exists(PK) AND clearedCents >= :amt

:amt = 30000 で。残高が低すぎるか、アイテムが作成されていなければ、DynamoDB は ConditionalCheckFailedException で書き込みを拒否し、残高は手つかずです。同時実行の 引き落としは、元の残高を見てそれに対してチェックされるか、更新後の残高を見るかのどちらかで — 作用した古い読み取りには決してなりません。

ExpressionAttributeValues マップを手組みする代わりに、その正確な式 — 名前も値も すべて — を DynamoDB Expression Builder で構築して コピーできます。

DynoTable でガードを検査する

条件付き書き込みが失敗したとき、推測するのではなくアイテムの本当の状態を見たいものです。 口座アイテムを呼び出して clearedCents を直接読みましょう。

DynoTable に表示した元帳コレクション — BALANCE アイテムが口座のトランザクションアイテムの上に clearedCents を表示している。
DynoTable に表示した元帳コレクション — BALANCE アイテムが口座のトランザクションアイテムの上に clearedCents を表示している。

拒否を読み、闇雲にリトライしない

ConditionalCheckFailedException は一時的なエラーではありません — 同じ書き込みを リトライしても何も変わりません。それはビジネスルールが発火したことを意味します。残高不足、 重複作成、古いバージョン。インフラの一過性の不調としてではなく、ドメインの結果として 表面化させましょう。

2つのことが失敗をデバッグ可能にします。

  • ReturnValuesOnConditionCheckFailure: ALL_OLD — DynamoDB が失敗とともに現在の アイテムを返すので、2回目の読み取りなしに「残高は 20000 でしたが、30000 を要求しました」 と表示できます。 (AWS: Working with Items
  • 2つの失敗理由を区別する。 attribute_exists(PK) AND clearedCents >= :amt は 「口座なし」と「資金なし」を1つの例外に畳み込みます。呼び出し元がそれらを区別する必要が あるなら、2つの書き込みに分けるか、返されたアイテムを検査します。

楽観的ロックも同じトリック

バージョン番号パターンは、別の帽子をかぶった条件式にすぎません。version 属性を保存し、 すべての書き込みが、読んだバージョンをアサートしてそれをインクリメントします。

UpdateItem  ACCT#a7f3 / BALANCE
  SET clearedCents = :new, version = :next
  ConditionExpression: version = :seen

別の書き手が先に動いたなら、version = :seen は偽で、書き込みは拒否され、あなたは再読 してリトライします。これが、ロックなしで DynamoDB が同時実行制御を行う方法です — 見た ものをアサートし、動いていたら失敗する。(AWS: Optimistic Locking with Version Number

落とし穴と次のステップ

  • 予約語と衝突する名前。 statussizename、そして他およそ570語が予約されて います。ExpressionAttributeNames#s = status)でエイリアスを付けないと、式が静かに パースに失敗します。
  • 条件は別のアイテムを参照できない。 書き込まれているアイテムだけを見ます。アイテムを またぐ不変条件には、アクションごとの ConditionExpression を持つ TransactWriteItems か、番兵アイテムに対する ConditionCheck が必要です。
  • 失敗した書き込みも WCU を消費する。 90% の確率で拒否するガードでも、それらの拒否分を 課金されます。安い保険ですが、無料ではありません。

これらのガードが対象とするキーのモデリングについては、 シングルテーブル設計Query と Scan を参照してください。実データに対して条件付き書き込みを発行する準備ができたら、 DynoTable をダウンロード して、自分のテーブルに対してそれらを実行しましょう。

更新日