並行性と競合状態のレビュー
2つのリクエストが同時に届くと仮定し、共有状態を1つずつ辿る
次のコードの並行安全性をレビューしてください。方法:2つのリクエストが同じミリ秒に届くと仮定し、共有状態を順に辿ります。次をお願いします:1)共有される可変状態をすべて挙げる(メモリ上の変数、キャッシュ、データベースの行、ファイル);2)確認と使用のあいだに隙間がある箇所(読んでから書く)を示す;3)どの程度の並行数で表面化するか、表面化したときに何が起きるかを述べる;4)最小の修正方法(ロック、一意制約、アトミック操作、冪等化)と、それぞれの代償を示す。 ``` [コードを貼り付けてください] ```
使い方
並行数の少ないプロジェクトでも一度は通しましょう。「読んでから書く」穴は、たいていトラフィックが増えてから炸裂します。
向いている場面
- 「読んでから書く」処理があるとき。在庫の減算、クーポンの取得、冪等な注文作成などです。
- 単一ノードのテストは必ず通るのに、マルチインスタンス配備や負荷試験の後に重複データが出始めたとき。
向いていない場面
- 単一プロセスで共有状態がなく、純粋関数だけなら、レビューする並行性の問題はありません。
- 分離レベルやロック待ちだと分かっているなら、コード全体ではなくロックとトランザクションを見ます。
例
入力
def claim_coupon(user_id, coupon_id):
if db.exists("select 1 from claims where user_id=? and coupon_id=?", user_id, coupon_id):
return "already"
if db.count("select count(*) from claims where coupon_id=?", coupon_id) >= 100:
return "sold_out"
db.insert("claims", user_id=user_id, coupon_id=coupon_id)
return "ok"得られるもの
共有される可変状態は claims テーブルと在庫数です。check-then-act の隙間は 2 か所あります。同じユーザーの 2 つのリクエストがどちらも exists を通過する点、在庫が 99 のときに 2 つのリクエストがどちらも count<100 を通過する点です。2 インスタンスで秒間 10 リクエスト程度のクーポン争奪で表面化し、現象は重複取得と超過配布です。最小の修正は `(user_id, coupon_id)` の一意制約で受け止め、`INSERT ... SELECT ... WHERE count < 100` の 1 文で原子的に書くことです。代償は競合時のリトライです。
よくある誤用
- 「並行性の問題を見てください」とだけ言い、配備形態(インスタンス数、Redis ロックの有無)を伝えないと、単一ノードで安全なコードが危険と判定されます。
- 「ロックを追加」と言われてプロセス内ロックを足すと、マルチインスタンスでは何も守れません。
こう変えると効く
- 「3 インスタンス、共有キャッシュなしと仮定して」と加えると、プロセス内の変数も共有状態として数えます。
- 「安定して再現する負荷試験の概要(同時数、リクエスト順)を出して」と頼むと、抽象的なリスクが検証可能な実験になります。