冪等性(idempotency)とは — 同じ処理を2回実行しても安全にする設計
「処理が途中で止まったので、もう一度実行した」。この操作が安全かどうかは、処理が冪等(べきとう)に作られているかで決まる。
冪等とは、同じ操作を何回繰り返しても、結果が1回実行したときと同じになる性質のことである。通信の失敗、タイムアウト、画面の二重クリック、スケジュールの重複起動など、「同じ処理が2回走る」場面は、運用していると必ず起きる。
この記事では、冪等な処理と冪等でない処理の違いを、手元で動かした小さな実験で確認し、冪等にするための代表的な設計を整理する。
補足: 実験は、メモリ上の SQLite(Python 標準ライブラリ)で行った。特定のサービスやサイトの挙動を測ったものではなく、考え方を確かめるための最小の例である。
冪等とは何か
数学の言葉では、「f(f(x)) = f(x)」が成り立つ操作を冪等と呼ぶ。日常の言葉に直すと、次のようになる。
- エレベーターの「上」ボタンは冪等。1回押しても5回押しても、エレベーターは1回だけ来る
- 「残高から100円引く」は冪等ではない。2回実行すると、200円引かれる
- 「残高を900円にする」は冪等。何回実行しても900円である
ポイントは、1回目と2回目以降で、結果が変わらないことである。副作用が一切ない、という意味ではない。「同じ結果に収束する」という意味である。
実験: 同じ SQL を2回実行する
残高1000の口座と、空の表を用意して、同じ文を2回ずつ実行した。
| 操作 | 2回実行後の結果 | 冪等か |
|---|---|---|
UPDATE ... SET bal = bal - 100(相対的な更新) |
残高 800 | 冪等ではない |
UPDATE ... SET bal = 900(絶対値の更新) |
残高 900 | 冪等 |
INSERT(単純な追加) |
2行できる | 冪等ではない |
INSERT ... ON CONFLICT DO UPDATE(UPSERT) |
1行のまま | 冪等 |
見分けるコツは、「今の状態」に依存して結果が決まるか、「最終的にあるべき状態」を指定しているかである。
- 「100引く」「1行足す」は、今の状態からの差分を指定している。実行するたびに状態が進む
- 「900にする」「このキーの行がある状態にする」は、あるべき状態を指定している。何回実行しても、同じ状態に収束する
なぜ「2回実行」は必ず起きるのか
「2回実行は例外的な事故」と考えると、設計が甘くなる。実際には、次のような場面で日常的に起きる。
- リトライ: 通信が切れたとき、クライアントは「リクエストが届いたか」を判断できない。届いていないと思って再送したら、実は届いていて処理も完了していた、ということが起こる
- 二重クリック・二重送信: 画面の応答が遅いと、人は同じボタンをもう一度押す
- スケジュールの重複: 前回の実行が終わる前に、次の実行が始まる
- 途中失敗からの再実行: 10件中5件目で止まったとき、最初からやり直すと、1〜4件目が2回処理される
- メッセージの再配送: 多くのキュー型の仕組みは「少なくとも1回」届ける設計で、同じメッセージが重複して届くことがある
特に1番は、本質的に避けられない。「送った側から見て、成功か失敗か分からない」状態があるため、安全に再送できる(冪等である)ことが、信頼できる仕組みの前提になる。
冪等にするための代表的な設計
1. 「差分」ではなく「状態」を書く
先の実験のとおり、bal = bal - 100 は bal = 900 に書き換えられるなら、書き換える。設定の変更、ファイルの内容の置き換え、ステータスの更新は、多くが「あるべき状態」の指定にできる。
ファイル操作でも同じである。「ディレクトリを作る」は、すでにあるとエラーになる操作だが、「ディレクトリがある状態にする」(存在すれば何もしない)形にすれば冪等になる。Python なら os.makedirs(path, exist_ok=True)、シェルなら mkdir -p がこれに当たる。
2. 一意制約で、重複を拒否する
「追加」が避けられない場合は、重複を判定するキーを決めて、データベース側の一意制約(UNIQUE)で守る。実験の UPSERT は、k 列に一意制約を付けたうえで「同じ k が来たら更新する」と指定した。
アプリケーション側で「あるか確認してから追加する」処理は、確認と追加の間に別の処理が割り込むと、二重に追加される。重複の防止は、競合が起きない場所(データベースの制約)に置くのが基本である。
3. 冪等キー(idempotency key)を使う
「決済」「注文」のように、キーにできる自然な値がない操作では、クライアントが操作ごとに固有の識別子(冪等キー)を生成して、リクエストに添える。サーバーは、受け取ったキーを記録し、同じキーのリクエストが再び来たら、処理を繰り返さず、前回の結果を返す。
この方式の要点は、次のとおりである。
- キーは、「操作の意図」ごとに1つ。再送時は同じキーを使い、別の操作には別のキーを使う
- サーバーは、キーと結果を、一定期間は保存しておく必要がある
- 同じキーなのに内容が違うリクエストが来たら、エラーとして拒否するのが安全
4. 処理済みの印を残す
バッチ処理や移行処理では、「どこまで終わったか」を記録しておき、再実行時に終わった部分を飛ばす設計が有効である。
- 処理済みの行にフラグや処理日時を付ける
- 移行処理なら、「すでに新しい形式になっているデータには何もしない」ことを、処理の最初で判定する
「判定してから処理する」形は、一見、先の「あるか確認してから追加する」と同じに見える。違いは、同時に走る処理が1つだけ(排他制御がされている)なら安全で、同時実行がありうるなら、一意制約や排他ロックと組み合わせる必要がある点である。排他の考え方は、楽観ロックと悲観ロックの違いで扱っている。
「冪等」と「安全」は同じではない
混同しやすい点を、2つ挙げる。
冪等は、「副作用がない」ことではない。 「ファイルを削除する」は冪等(2回目は、すでに無いだけで、状態は変わらない)だが、1回目の削除には取り返しがつかない副作用がある。冪等性は「再実行しても状態が悪化しない」ことを保証するだけで、「その操作自体が望ましい」ことは保証しない。
冪等でも、「結果の返り方」は揃わないことがある。 2回目の削除が「すでにありません」というエラーを返す場合、状態は同じでも、呼び出し側から見ると失敗に見える。再実行する側は、「すでにあるべき状態になっている」ことを、成功と見なせるように作る必要がある。
自分の処理を点検する観点
新しい処理を書くときや、既存の処理を見直すときは、次の問いを順に確認するとよい。
- この処理を途中で止めて、もう一度最初から実行したら、何が起きるか
- この処理を同時に2つ実行したら、何が起きるか
- 追加・加算・送信(メールや通知)のような、差分を積み上げる操作はないか。あるなら、状態の指定に置き換えられないか
- 置き換えられない場合、重複を判定するキーは何か。それを守る場所は、データベースの制約か
- 再実行時に「すでに済んでいる」ことを、エラーではなく成功として扱えるか
このツール自体の設計でも、データの形式を移行する処理は、「すでに移行済みなら何もしない」冪等な作りを意識している。何度起動しても同じ結果になることが、更新や中断からの復旧を落ち着いて行うための前提になるからである。
まとめ
- 冪等とは、同じ操作を何回実行しても、結果が1回の場合と同じになる性質
- 「100引く」「1行足す」は冪等ではなく、「900にする」「この行がある状態にする」は冪等
- 通信の再送、二重クリック、スケジュール重複、途中失敗からの再実行など、2回実行は日常的に起きる
- 冪等にする手段は、状態を書く・一意制約で守る・冪等キーを使う・処理済みの印を残す
- 冪等は「副作用がない」ことではない。再実行しても状態が悪化しないことを指す
「もう一度実行しても大丈夫か」を、処理を書く段階で問うておくと、障害のあとの復旧が、ずっと落ち着いたものになる。