条件付き書き込みで注文の重複を防ぐ

AWSBeginner
オンラインで実践に進む

はじめに

注文の送信を再試行しても、既存の金額を上書きしてはいけません。古い支払い更新も、新しいバージョンを上書きしてはいけません。このラボでは、実際の条件付き API リクエストを行い、重複と古いバージョンが拒否されることを観察します。さらに、有効な新規注文は引き続き成功することを確かめます。

最初に「注文用の DynamoDB テーブルを作成する」を完了し、型付きアイテムと更新式を学んでください。この新しい単元では、以前のテーブルや認証情報は再利用しません。/home/labex/project、公式 AWS CLI、AWS View を使用してください。

認定試験との関連

認定資格 試験タスク 演習内容
Developer – Associate (DVA-C02) タスク 1.3, タスク 1.1 DynamoDB の条件付き書き込みと楽観的同時実行制御。

ラボの概要

AWS4 アイコンを使い draw.io でエクスポートした概念図:条件付き書き込みで現在の注文状態を原子的に確認し、有効な作成と更新を許可します。重複や古いバージョンのリクエストは拒否し、受理済みデータを保持します。

最初の注文作成を保護する

このステップでは、プライマリキーが未使用の場合だけ注文を作成します。条件のない PutItem は、そのキーのアイテム全体を置き換えます。条件は、書き込みと不可分の操作として現在の状態を確認します。そのため、読み取ってから書き込む別々の確認は不要です。その方法では競合が発生する余地があります。

準備済みの使い捨ての接続を使い、注文 ID を String 型のパーティションキーとする独立したテーブルを作成します。

cd /home/labex/project
aws sts get-caller-identity
aws dynamodb create-table \
  --table-name labex-d04-orders \
  --attribute-definitions AttributeName=order_id,AttributeType=S \
  --key-schema AttributeName=order_id,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST
aws dynamodb wait table-exists --table-name labex-d04-orders

通常の API リクエストファイルを書き込みます。引用符付きのヒアドキュメントは JSON をそのまま書き込みます。file:// を使うと、大きなデータをシェルの引用符で囲まずに CLI から読み込めます。attribute_not_exists(order_id) は、そのキーを持つアイテムがない場合だけ書き込みを許可します。version: 1 フィールドはアプリケーションの値で、ステップ 3 で更新を保護するために使います。DynamoDB が自動的に増やす値ではありません。

cat > create-order.json <<'JSON'
{
  "TableName": "labex-d04-orders",
  "Item": {
    "order_id": {
      "S": "O100"
    },
    "amount": {
      "N": "145"
    },
    "status": {
      "S": "NEW"
    },
    "version": {
      "N": "1"
    }
  },
  "ConditionExpression": "attribute_not_exists(order_id)"
}
JSON
aws dynamodb put-item --cli-input-json file://create-order.json
aws dynamodb get-item \
  --table-name labex-d04-orders \
  --key '{"order_id":{"S":"O100"}}' \
  --consistent-read

O100 には金額 145、NEW、バージョン 1 が入っています。この新しいキーでは、存在しないことを確認する条件が成立します。上部の AWS View を確認し、O100 を展開して実際のアイテムを確認してください。

新規注文を妨げずに重複を拒否する

このステップでは、同じ注文 ID を異なる金額で再送します。安全でない上書きでは O100 が 999 に変わってしまいます。条件によってそれを拒否し、145 を保持する必要があります。重複の拒否はエラーレスポンスであり、冪等なアプリケーションの成功レスポンスに自動的に変わるわけではありません。アプリケーションが、そのエラーの扱いを決める必要があります。

作成リクエストを再試行しても、注文ですでに受け付けた金額を置き換えるべきではありません。項目が存在しないという条件が、書き込み時にその判断を保護します。読み取りのフィルターは返す項目を変えますが、書き込み条件は変更そのものを許可するか判断します。競合する各リクエストに条件が必要です。

同じキーと、存在しないことを確認する条件を持つ競合リクエストを書き込みます。

cat > duplicate-order.json <<'JSON'
{
  "TableName": "labex-d04-orders",
  "Item": {
    "order_id": {
      "S": "O100"
    },
    "amount": {
      "N": "999"
    },
    "status": {
      "S": "NEW"
    },
    "version": {
      "N": "1"
    }
  },
  "ConditionExpression": "attribute_not_exists(order_id)"
}
JSON

同じキーで書き込みを繰り返します。|| true により、想定した失敗の後もターミナルで処理を続けられます。ConditionalCheckFailedException が返ることを確認してください。接続や認可のエラーであれば、別の診断が必要です。

aws dynamodb put-item --cli-input-json file://duplicate-order.json || true
aws dynamodb get-item \
  --table-name labex-d04-orders \
  --key '{"order_id":{"S":"O100"}}' \
  --consistent-read

ConditionalCheckFailedException が返り、その後の読み取りでは金額 145 とバージョン 1 が変わっていないはずです。同じ保護条件がすべての書き込みを拒否するのではなく、異なる注文 ID は許可することを確かめます。

cat > second-order.json <<'JSON'
{
  "TableName": "labex-d04-orders",
  "Item": {
    "order_id": {
      "S": "O101"
    },
    "amount": {
      "N": "200"
    },
    "status": {
      "S": "NEW"
    },
    "version": {
      "N": "1"
    }
  },
  "ConditionExpression": "attribute_not_exists(order_id)"
}
JSON
aws dynamodb put-item --cli-input-json file://second-order.json
aws dynamodb get-item \
  --table-name labex-d04-orders \
  --key '{"order_id":{"S":"O101"}}' \
  --consistent-read

O101 は金額 200、NEW、バージョン 1 で作成されます。AWS View には両方の注文が表示され、O100 の金額は 145 のままです。これらの操作は、このキーで識別されるレコードの意図しない上書きを防ぎます。複数アイテムのトランザクションや支払いの重複排除を実装するものではありません。

期待するバージョンで更新を保護する

このステップでは、読み取ったバージョンにだけ状態遷移を適用します。楽観的なバージョン条件は、より新しい判断を黙って上書きせず、古いリクエストを拒否します。書き込みは、バージョン 1 の確認、PAID の設定、バージョン 2 への更新を不可分の操作として実行します。

最初の更新はバージョン 1 に一致し、アイテムがバージョン 2 になった後で同じ期待値を再送すると失敗します。

以下の UpdateItem リクエストを書き込み、実行します。式の属性名エイリアスで属性名を安全に扱い、型付きの値で期待するバージョンと次のバージョンを渡します。

cat > pay-order.json <<'JSON'
{
  "TableName": "labex-d04-orders",
  "Key": {
    "order_id": {
      "S": "O100"
    }
  },
  "UpdateExpression": "SET #state = :paid, #version = :next",
  "ConditionExpression": "#version = :expected",
  "ExpressionAttributeNames": {
    "#state": "status",
    "#version": "version"
  },
  "ExpressionAttributeValues": {
    ":paid": {
      "S": "PAID"
    },
    ":next": {
      "N": "2"
    },
    ":expected": {
      "N": "1"
    }
  },
  "ReturnValues": "ALL_NEW"
}
JSON
aws dynamodb update-item --cli-input-json file://pay-order.json

ALL_NEW には PAID、バージョン 2、金額 145 が表示されます。バージョン 1 を期待したままの、変更していないリクエストを再送します。

aws dynamodb update-item --cli-input-json file://pay-order.json || true
aws dynamodb get-item \
  --table-name labex-d04-orders \
  --key '{"order_id":{"S":"O100"}}' \
  --consistent-read

ConditionalCheckFailedException が返るはずです。最後の読み取りでは PAID、バージョン 2、金額 145 が保持され、O101 は NEW、バージョン 1 のままです。本番環境の呼び出し元は、古い期待値で無限に再試行するのではなく、現在の状態を読み直し、別の更新を行うのが適切か判断します。

実際のアイテムは、保護された作成後も金額を保持し、バージョンを管理した支払いの状態遷移を記録しています。

所有する注文テーブルだけを削除する

このステップでは、最終状態を確認し、所有するテーブルを削除します。保存した JSON ファイルはリクエストを記述したものです。実際の効果を示すのは、サービス内のアイテムです。テーブルを削除した証拠として、コマンド履歴やエラーログを使わないでください。

aws dynamodb scan --table-name labex-d04-orders --consistent-read

O100 は PAID、バージョン 2、金額 145 で、O101 は NEW、バージョン 1、金額 200 です。このテーブルだけを削除し、存在しなくなるまで待ちます。

aws dynamodb delete-table --table-name labex-d04-orders
aws dynamodb wait table-not-exists --table-name labex-d04-orders
aws dynamodb list-tables
aws dynamodb get-item \
  --table-name labex-d04-reference \
  --key '{"id":{"S":"platform"}}' \
  --consistent-read

参照テーブルだけが残り、note は keep unchanged のままです。これらのクエリが成功することにより、削除と接続の失敗を区別できます。AWS View からは注文テーブルが消えます。この使い捨ての接続は、VM とともに終了します。

まとめ

存在しないことを確認する条件で新規注文の作成を保護し、保存された金額を変えずに重複を拒否することを確認しました。別の新しいキーの書き込みは許可されました。次に、期待するバージョンを条件に使い、1 回の状態遷移を適用して古いリクエストの再送を拒否しました。実際のアイテムの最終値を確認してから、所有するテーブルだけを削除しました。

詳しくは、AWS 公式ドキュメントを参照してください:書き込みを保護する条件式。