同じ登録が2件並んだ画面を前に、原因を考えながら手を止めている開発者

AIに冪等性の設計を相談する|リトライで二重実行させない型

「同じ申し込みが2件入っています」

問い合わせの一次受けから回ってきた画面を見て、少し息が止まる。ログを追うと、リクエストは確かに2回来ています。1回目の応答が返る前にタイムアウトして、送り手が同じ内容をもう一度投げていました。

コードは間違っていません。テストも通っています。同じリクエストが2回来ることを、誰も想定に入れていなかっただけです。

しかもこの手の不具合は、忙しい日にだけ出ます。回線が詰まった日、外部サービスが重かった日、障害から復旧してジョブを流し直した日。再現しようとしても、手元では素直に1回で通ってしまう。

この記事では、この「2回来る」を前提にした設計——冪等性(べきとうせい)の詰め方を、AIへの相談の仕方とセットで整理します。冪等性とは、同じ操作を2回・3回受け取っても、結果が1回分にしかならない性質のことです。難しい言葉ですが、やることは「同じだと見分けて、2回目を仕事にしない」だけです。

結論:順番は3つです。①決める——二重に起きたら困る操作を1つ選び、「何をもって同じ依頼とみなすか」=冪等キーを先に決める。②弾く——2回目を、アプリの条件分岐ではなくデータベースの一意制約で落とす。「存在を確認してから登録する」は、同時に来ると通り抜けます。③返す——2回目に1回目と同じ結果を返す。エラーで返すと、送り手はさらに再送します。

覚え方は 決める → 弾く → 返す。①を飛ばすと②の条件が場当たりになり、③がないと再送が止まりません。

全部を今週やる必要はありません。①の「同じとみなす条件を1行で書く」だけでも、AIに渡す相談の質が変わります。

何が起きているのか——2回来る経路は、思っているより多い

2通の封筒に同じ印の札を付ける場面、ゲートで2通目が止められる場面、窓口で1通目と同じ書類を手渡す場面を並べた図
二重実行を防ぐ順番。決める→弾く→返す。①がないと②の条件が場当たりになり、③がないと再送が止まらない

打ち手の前に、状況を整理します。原因の側を先に見ておくと、AIに渡す前提も書きやすくなります。

1つ目は、送り手が「届いたか分からない」と再送すること。通信は、応答が返ってこなかったときに「処理されなかった」のか「処理されたが応答だけ届かなかった」のかを、送り手からは区別できません。安全側に倒すなら再送する——これは送り手の判断として正しい動きです。つまり再送は事故ではなく、正常な運用の一部です。

2つ目は、再送する主体が1つではないこと。ここが見落とされやすいところです。利用者のボタン連打や再読み込み、クライアントの自動リトライ、ロードバランサやプロキシのタイムアウト再試行、メッセージキューの再配信、外部サービスからのWebhook再送、そして障害復旧後のジョブ流し直し。どれか1か所で対策しても、別の経路から同じことが起きます

3つ目は、キューやWebhookの多くが「少なくとも1回」で作られていること。同じメッセージが2回届くことを、仕様として許している作りです。受け取る側で重複を吸収する前提になっているので、受け側が何もしなければ、そのまま二重に処理されます

4つ目は、「存在を確認してから登録する」が競合に弱いこと。ここがいちばん実務で刺さります。同時に2つのリクエストが来ると、両方が「まだ無い」と判定してから、両方が登録に進みます。確認と登録のあいだに、別の処理が入り込む隙間があるためです。これは書き方が雑なのではなく、この形自体が同時実行に弱い、という話です(AIに非同期・並行処理を書かせる|競合とデッドロックの確認)。

5つ目は、AIに頼むと、まさにその形が出やすいこと。「二重登録を防いでください」と頼むと、多くの場合きれいに動く「存在チェック+登録」が返ってきます。読みやすく、単体テストも通ります。同時に来たときだけ破れるので、レビューでも素通りしやすい。AIが手を抜いているわけではなく、同時実行という前提を渡していないからです。

裏を返すと、これは能力や注意力の問題ではなく、前提の渡し方と、確認する場所の問題です。見る場所が決まっていれば、AIはむしろ得意な相談相手になります。

具体例——つい、こうなりがちという形

どれも「まず動くものを」という自然な流れから生まれます。心当たりのある行があれば、そこだけ見れば十分です。

影響——「たまに」なので、見つかるのが遅い

急いで全部直さないと危ない、という話ではありません。ただ、出方が遅く、出たときは説明が要る種類の問題なので、書いておきます。

少し安心できる話をすると、この領域は打ち手がはっきりしています。仕組みを入れ替える必要はなく、キーを1つ決めて、制約を1つ足すところから始められます。効果もすぐ確認できます。

明日からやること(決める・弾く・返す)

上から順に、小さく始められます。今日は①だけで構いません。

1. 決める——「同じ依頼」の定義を、先に1行で書く

ここを飛ばして相談すると、AIの答えも場当たりになります。逆に、ここさえ決まれば残りは詰めやすくなります。

2. 弾く——アプリの条件分岐ではなく、データベースに落としてもらう

いちばん確実で、いちばん安く済む工程です。作業としては制約を1つ足すだけです。

3. 返す——2回目に、1回目と同じ答えを返す

ここが抜けると、弾いているのに再送が止まらない、という状態になります。

AIに相談するときのプロンプト(コピーして使えます)

前提を渡さないと、AIは素直に「存在チェック+登録」を出してきます。同時実行と再送を前提だと明記するのがコツです(AIにコードを書かせる前に渡す前提と制約|伝え方の型)。

ある操作を、同じ依頼が複数回届いても結果が1回分になるように設計したいので、相談に乗ってください。

①まず、この操作が二重に実行されうる経路を、思いつく限り挙げてください(利用者の再操作、クライアントの再送、途中の経路のタイムアウト再試行、キューの再配信、外部からの再送、障害復旧時のやり直しなど)。
②次に、冪等キーの候補を2〜3案、それぞれ効かなくなる条件つきで挙げてください。
「存在を確認してから登録する」形の案は出さないでください。同時に届いたときに通り抜けるためです。データベースの制約で弾く案でお願いします。
1回目がまだ処理中に2回目が届いたときの挙動を、明示してください。
外部サービスの呼び出しとデータ更新の順序、およびその途中で落ちた場合に残る状態を説明してください。
⑥最後に、この設計で守れないケースを正直に挙げてください。

なお、確実でない部分は「不明」と書いてください。埋めなくて構いません。

操作:( ) 再送してくる相手:( ) 使っているDB:( ) 外部連携:( ) やり直しが効かない処理:( )

③の一文が効きます。先に一つ禁じ手を伝えるだけで、返ってくる案の質が変わります。⑥は、そのまま人が見る場所のリストになります。

出てきた案は、次の3点だけ自分の目で確かめます。(1)一意制約が実際に張られているか(2)外部呼び出しがトランザクションの中に入っていないか(3)処理中の扱いが書かれているか。この3点は、それらしい説明で埋まっていても抜けていることがあります(設計レビューの見落としをAIに洗わせる|プロンプトの型)。

確認は、テストにも落としておくと安心です。同じキーで2回呼ぶ同時に2回呼ぶ1回目の処理中に2回目を呼ぶ——この3つが通れば、だいたいの経路は塞がっています(境界値・異常系をAIに考えさせて漏れを減らす手順)。

二重実行を防ぐチェックリスト(コピーして使えます)

全部に○が要るわけではありません。気になった行だけで十分です。

① 決める

② 弾く

③ 返す

この記事のまとめ

二重実行は、腕前の問題ではなく前提の問題です。通信は再送されるもの、キューは同じものを届けうるもの——そこを前提に置くと、打ち手ははっきりします。

やることは3つ。①決める——守る操作を1つ選び、冪等キーとその一意の範囲、保存期間を決める。②弾く——一意制約で2回目を落とし、制約違反を「処理済み」の合図として扱う。外部呼び出しはトランザクションの外へ。③返す——完了済みなら1回目と同じ結果を、処理中なら状態を返す。同じキーで中身が違うときは通さない。

AIに相談するときは、「存在を確認してから登録する案は出さないで」と先に一言添える。それだけで、返ってくる設計の筋がよくなります。

朝のオフィスで、落ち着いた表情で画面を確認し、同僚と短く言葉を交わしている開発者

二重に入ったデータを見つけたとき、まず自分の書いたコードを疑ったと思います。実際には、コードは指示どおりに動いていて、同じ依頼が2回届いていただけでした。

これは、気をつけて防ぐ種類のものではありません。仕組みで受け止める種類のものです。そして仕組みは、キーを1つ決めて、制約を1つ足すところから作れます。

今日は、やり直しが効かない操作をひとつだけ思い浮かべて、「何をもって同じ依頼とみなすか」を1行書いてみる。それだけで、次の障害の夜が、少し静かになります。

よければ、こちらも

関連用語