TT Lab
はじめる
学ぶ 学習パス コース

冪等性 — 二度押しても決済は一度だけ

最後の在庫を二人が予約した

TT Labで続きを見る

目標

在庫の減算・予約id・キャンセルをアトミックにまとめ、過剰販売を防ぎます。

なぜ重要なのか

在庫が1つ残っているのに、2つのリクエストが同時に参照して、どちらも予約に成功しました。その後、同じキャンセルメッセージが2回届くと、在庫が元より増えました。予約の重複排除とキャンセルの重複排除は、それぞれ別の状態遷移であり、残りの数量の検査も、減算と同じトランザクションの中にある必要があります。

ステップ

  1. /root/work/idem-stock-reservation-lab/service.pyで、quantity(value)は、boolを除く正のintだけを返し、それ以外はValueErrorです。

最初に1回だけ準備してください。既存のファイルは上書きしません。

mkdir -p /root/work/idem-stock-reservation-lab
test -e /root/work/idem-stock-reservation-lab/service.py || cp /opt/fixtures/ten_labs/idem-stock-reservation-lab/service.py /root/work/idem-stock-reservation-lab/service.py
cd /root/work/idem-stock-reservation-lab
  1. /root/work/idem-stock-reservation-lab/service.pyで、init_db(path)は、stock(sku TEXT PRIMARY KEY,available INTEGER NOT NULL)とreservations(id TEXT PRIMARY KEY,sku TEXT NOT NULL,qty INTEGER NOT NULL,cancelled INTEGER NOT NULL DEFAULT 0)を、冪等に作成します。

  2. /root/work/idem-stock-reservation-lab/service.pyで、add_stock(path,sku,amount)は、boolを除く0以上のintを検査し、新しい商品だけをINSERTします。重複した商品は、IntegrityErrorです。

  3. /root/work/idem-stock-reservation-lab/service.pyで、available(path,sku)は、保存された数量を返し、商品がなければKeyErrorです。

  4. /root/work/idem-stock-reservation-lab/service.pyで、reserve(path,rid,sku,qty,fault=lambda:None)は、数量を検証します。既存の予約idは、同じ商品/数量ならFalse、違えばValueErrorです。新しい予約は、商品の存在と在庫を確認して、減算→fault→予約の挿入のあとにTrueです。不足・ない商品はValueErrorです。

  5. /root/work/idem-stock-reservation-lab/service.pyで、cancel(path,rid)は、ない予約・すでにキャンセルされた予約ならFalseです。activeの予約なら、同じトランザクションで、cancelled=1と在庫の返却を行ってTrueです。

  6. /root/work/idem-stock-reservation-lab/service.pyで、reservation(path,rid)は、(sku,qty,cancelled)のタプル、またはNoneを返します。

  7. /root/work/idem-stock-reservation-lab/service.pyで、compete(path,sku)は、reserveを、互いに異なるid first/second、数量1で、2つのスレッドで実行します。ValueErrorだけをFalseに変えた、入力順の結果のリストを返します。

参考

数量を整数の契約に固定する

/root/work/idem-stock-reservation-lab/service.pyで、quantity(value)は、boolを除く正のintだけを返し、それ以外はValueErrorです。

最初に1回だけ準備してください。既存のファイルは上書きしません。

mkdir -p /root/work/idem-stock-reservation-lab
test -e /root/work/idem-stock-reservation-lab/service.py || cp /opt/fixtures/ten_labs/idem-stock-reservation-lab/service.py /root/work/idem-stock-reservation-lab/service.py
cd /root/work/idem-stock-reservation-lab

負の予約が、在庫を増やせないようにします。

保存したあと、bash /opt/lab/checks/idem-stock-reservation-lab/01-contract.shで確認してください。

在庫と予約を分けて保存する

/root/work/idem-stock-reservation-lab/service.pyで、init_db(path)は、stock(sku TEXT PRIMARY KEY,available INTEGER NOT NULL)とreservations(id TEXT PRIMARY KEY,sku TEXT NOT NULL,qty INTEGER NOT NULL,cancelled INTEGER NOT NULL DEFAULT 0)を、冪等に作成します。

キャンセルされた予約も残しておいて初めて、同じキャンセルと再予約を区別できます。

保存したあと、bash /opt/lab/checks/idem-stock-reservation-lab/02-contract.shで確認してください。

商品の初期化を、在庫の変更と区別する

/root/work/idem-stock-reservation-lab/service.pyで、add_stock(path,sku,amount)は、boolを除く0以上のintを検査し、新しい商品だけをINSERTします。重複した商品は、IntegrityErrorです。

同じ初期化コマンドが、使用中の在庫を上書きしないようにします。

保存したあと、bash /opt/lab/checks/idem-stock-reservation-lab/03-contract.shで確認してください。

残りの数量を取得する

/root/work/idem-stock-reservation-lab/service.pyで、available(path,sku)は、保存された数量を返し、商品がなければKeyErrorです。

ない商品を売り切れと区別して、間違った商品idを隠さないようにします。

保存したあと、bash /opt/lab/checks/idem-stock-reservation-lab/04-contract.shで確認してください。

予約と在庫の減算をまとめる

/root/work/idem-stock-reservation-lab/service.pyで、reserve(path,rid,sku,qty,fault=lambda:None)は、数量を検証します。既存の予約idは、同じ商品/数量ならFalse、違えばValueErrorです。新しい予約は、商品の存在と在庫を確認して、減算→fault→予約の挿入のあとにTrueです。不足・ない商品はValueErrorです。

検査と減算を同じトランザクションに置き、障害のときはすべてロールバックします。

保存したあと、bash /opt/lab/checks/idem-stock-reservation-lab/05-contract.shで確認してください。

キャンセルも1回だけ反映する

/root/work/idem-stock-reservation-lab/service.pyで、cancel(path,rid)は、ない予約・すでにキャンセルされた予約ならFalseです。activeの予約なら、同じトランザクションで、cancelled=1と在庫の返却を行ってTrueです。

キャンセルメッセージの再送で、在庫が増え続けてはいけません。

保存したあと、bash /opt/lab/checks/idem-stock-reservation-lab/06-contract.shで確認してください。

予約の状態を確認する

/root/work/idem-stock-reservation-lab/service.pyで、reservation(path,rid)は、(sku,qty,cancelled)のタプル、またはNoneを返します。

戻り値だけでなく、キャンセルの状態がDBに残ったかを読みます。

保存したあと、bash /opt/lab/checks/idem-stock-reservation-lab/07-contract.shで確認してください。

最後の在庫の競合を再現する

/root/work/idem-stock-reservation-lab/service.pyで、compete(path,sku)は、reserveを、互いに異なるid first/second、数量1で、2つのスレッドで実行します。ValueErrorだけをFalseに変えた、入力順の結果のリストを返します。

在庫1と在庫2の2つの状況を比べて初めて、無条件に1つだけを成功させる実装も、見分けられます。

保存したあと、bash /opt/lab/checks/idem-stock-reservation-lab/08-contract.shで確認してください。