😱

「再実行の恐怖」から救う冪等性の話

に公開

1. はじめに:「何度やり直しても大丈夫」と言えますか?

「この処理、失敗したからもう一回実行しておいて」
そう言われたとき、あなたは自信を持って「はい、すぐに再実行します」と答えられるでしょうか?
それとも、「二重計上されないか」「データが壊れないか」と不安になるでしょうか。
エンジニアリングの世界には、この不安を解消するための重要な原則があります。それが 冪等性(Idempotency) です。
「API の話でしょ?」と思われがちですが、実はもっと広く、堅牢なシステムを作るための共通言語です。
冪等性が保たれていないと、次のような事故が起こります。

  • 決済・注文: ボタン連打で注文が2件作成されてしまう
  • インフラ: 設定スクリプトを2回流したらエラーで止まる
  • DB: マイグレーションを再実行したらカラムが増殖する
  • 配信: 配信やプッシュ通知が何度も届いてしまう(多重送信)

これらはすべて、「ある操作を何度行っても安全である」という保証 が欠けているために起こります。
この記事では、API に限らず、インフラやDB構成も含めた「エンジニアリング全体の安全装置」としての冪等性について解説します。

2. 冪等性とは?

冪等性を一言でいうと、以下のようになります。

ある操作を1回実行しても複数回実行しても、システムの状態(結果)が同じになる性質

回数を気にせず、「とりあえず実行すれば正しい状態になる」と言い換えてもいいでしょう。

身近な例でイメージしてみます。

日常の例:電気のスイッチ vs エレベーターのボタン

冪等である:電気のスイッチ(ON/OFF式)

「電気をONにする」という操作を考えます。

  • スイッチを1回押して ON にする → 部屋は明るい
  • そのままもう1回「ON側」に押す → 部屋は明るいまま

何度「ONにする」操作をしても、結果は「明るい」で変わりません。これが冪等です。

冪等でない:音量ボタン(+ボタン)

テレビのリモコンの「音量アップ」ボタンはどうでしょうか。

  • 1回押す → 音量が1上がる
  • 3回押す → 音量が3上がる

押した回数分だけ状態が変わってしまうため、これは冪等ではありません。

3. インフラにおける冪等性

冪等性は API だけの話ではありません。

インフラ構成管理(IaC)

Ansible, Terraform, AWS CDK などのインフラツールは、冪等性を利用しています。

Terraform の例

冪等でない操作(手動でのリソース追加)

# 何も考えずに実行すると、実行するたびにサーバーが増えてしまう
aws ec2 run-instances --image-id ami-xxx ...

冪等な操作(Terraform)

main.tf
# 何度 apply しても、「web-server というインスタンスが1台ある」状態が保証される
resource "aws_instance" "web" {
  ami           = "ami-xxx"
  instance_type = "t3.micro"
  tags = {
    Name = "web-server"
  }
}

「現状がどうあれ、あるべき姿(Desired State)にする」という宣言的な定義は、冪等性が土台になっています。AWS CDK も同様に、何度デプロイしても最終的な構成はコードで定義された状態に収束します。

データベース・マイグレーション

DBスキーマの変更も冪等であるべきです。

  • CREATE TABLE users ... をそのまま流すと、2回目は "Table already exists" で失敗します。
  • CREATE TABLE IF NOT EXISTS users ... なら、何度流しても成功し、結果も変わりません。

4. API・非同期処理での重要性

やはり、最も注意が必要なのは API や非同期処理の領域です。

原則GET と PUT/DELETEは冪等性が必要

REST の原則として、HTTPメソッドにも冪等性の決まりがあります。

  • GET, PUT, DELETE: 冪等であるべき
    • GET /users/123: 何度呼んでも、その時点での同じユーザー情報が返ってきます。サーバー側のデータは変更されません。
    • PUT /users/123: 「名前を田中さんに変更する」というリクエストを10回送っても、最終的に名前は「田中さん」のままです。
    • DELETE /users/123: 1回目で消える。2回目は「既にない(404)」かもしれませんが、「ユーザー123がいない」という最終状態は変わらないので冪等とみなせます。
  • POST: 基本的に冪等ではない(リソースの新規作成など)

上記はあくまで原則です。

予期せぬ「再実行」を防ぐ

ネットワークの状態やユーザーの挙動によってはタイムアウト、再送、連打は必ず起きます。
「リクエストは送ったがレスポンスが届かなかった」場合に、SDKやブラウザ、あるいはユーザー自身がリトライを行うためです。

だからこそ、「本来冪等でない操作(注文のPOSTなど)」にも、冪等性を持たせる工夫(Idempotency Key の利用など)が必要になります。

5. 冪等性がないと実際どう壊れるのか

冪等性が考慮されていないと、システムは簡単に破綻します。よくある失敗例を見てみましょう。

  • 二重課金・二重注文
    • ユーザーが購入ボタンを連打したり、タイムアウト時の自動リトライによって、決済APIが2回呼ばれてしまいます。結果、クレジットカードに2回請求が走ることになります。
  • データの不整合
    • 非同期処理でログを集計するバッチにおいて、エラーで再実行された際に、同じログを重複して集計してしまうことがあります。これにより集計値が実際の倍になってしまうなどの問題が起きます。
  • 通知の多重送信
    • Webhook の受信処理に失敗してリトライが繰り返される際、メール送信処理だけ先に走ってしまい、同じ内容のメールがユーザーに何十通も届いてしまうことがあります。

「冪等性を軽視して痛い目を見た」という話は多く、注意が必要です。

6. 実装時の “落とし穴”

「気をつけていたつもり」でもハマりやすいポイントがあります。

  • タイムアウト時の処理
    • サーバー側で処理は完了したものの、クライアントへのレスポンス中にタイムアウトが発生する場合です。クライアントは失敗とみなしてリトライしますが、サーバーは「新しいリクエスト」として処理してしまい、二重処理になります。
  • DBのユニーク制約忘れ
    • アプリケーションコードで「存在チェック」をしていても、データベース側にユニーク制約(一意制約)がないと、タイミングによっては重複データが登録されてしまいます。
  • 「追記(Append)」の罠
    • データを上書き(UPDATE / PUT)するなら冪等になりやすいですが、「リストに追加(PUSH / APPEND)」する処理は、リトライされるたびに要素が増え続けてしまいます。
  • Webhookの重複見逃し
    • 署名検証(改ざんチェック)はしていても、Event-ID などを使った「以前処理したイベントか?」のチェックを忘れてしまい、同じイベントを二重処理してしまうミスです。

7. 冪等性を設計に組み込むための考え方

どのように冪等性をシステムに組み込めばよいのでしょうか。

  • 冪等性は後付けが難しい
    • システムが一度稼働してしまうと、データ構造やAPI仕様を変えるのは困難です。最初から考慮に入れる必要があります。
  • 「再実行される前提」でAPIを設計する
    • REST API なら Idempotency-Key ヘッダーを受け取れるようにするなど、同じリクエストが来ても安全に処理できる仕組みを用意します。
  • 非同期処理は「1回以上実行される(At Least Once)」前提
    • メッセージキューなどは「少なくとも1回」届く保証はあっても、「ぴったり1回」届く保証をするのは非常に難しいです。「2回届いても大丈夫」な作りにしておくのが定石です。
  • イベント駆動で特に重要
    • マイクロサービスやイベント駆動アーキテクチャでは、イベントの再送が日常的に発生します。「イベントID」をキーにして処理済みを記録するテーブルを用意しましょう。

8. まとめ

冪等性とは、「失敗しても安心して再実行できる世界」を実現するための原則です。

ネットワーク、バッチ、外部サービス連携、イベント駆動...
どの領域でも"想定外の再実行"は必ず起きます。

だからこそ最初から冪等性を意識した設計をしておくことで、強くて壊れにくいシステムを作ることができます。

株式会社ソニックムーブ

Discussion