冪等性の理解: 信頼性のあるシステム設計へのガイド

[![表紙](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/ab9dd9a5-fcd6-4eba-841f-95cb09e52fe6.png)](https://leapcell.io/?lc_t=q_idempotency)

## まず冪等の概念について話しましょう

### 抽象的な概念

冪等（idempotent、idempotence）は、数学およびコンピューターサイエンスの概念であり、抽象代数においてよく見られます。

プログラミングでは、冪等操作の特徴は、何回実行してもその影響が 1 回だけ実行した場合と同じであることです。冪等関数（idempotent function）または冪等メソッドとは、同じパラメータで繰り返し実行しても、常に同じ結果が得られる関数を指します。これらの関数はシステムの状態に影響を与えず、繰り返し実行してもシステムを変更する心配がありません。例えば、`getUsername()` や `setTrue()` のような関数は冪等な関数です。

簡単に言えば、ある操作を何回行っても、その結果や影響が常に同じであることを意味します。

### いくつかの例を挙げてみましょう：

- フロントエンドで同じフォームデータを何度も送信した場合、バックエンドでは一つの結果しか生成されないべきです。
- 例えば、ユーザーが支払いリクエストを発行した際に、ユーザーの口座から 1 回だけお金が引き落とされるべきです。ネットワークの再送信やシステムのバグで再送信されたとしても、同じ金額が重複して引き落とされることはあってはなりません。
- メッセージの送信も同様に、1 回だけ送るべきです。同じ SMS を何度もユーザーに送信したら、ユーザーは困惑するでしょう。
- ビジネスオーダーの作成も同じで、一度のリクエストで 1 つのオーダーしか作成されるべきではありません。複数のオーダーが作成されるのは望ましくありません。

このようなシナリオはたくさんありますが、これらのロジックには冪等性の特性が必要不可欠です。

## 冪等性を実現する技術的手法

### クエリ操作

データが変更されていない場合、1 回のクエリと複数回のクエリの結果は同じになります。そのため、`SELECT` 文は自然に冪等な操作となります。

### 削除操作

削除操作も冪等です。1 回削除しても、複数回削除しても、データは削除されます。（ただし、返される結果は異なる可能性があります。例えば、削除するデータが存在しない場合は `0` を返し、複数のデータが削除された場合はその件数を返すなど。）

### 一意インデックスで不正データの追加を防ぐ

例えば、資金口座とユーザーアカウントを考えてみましょう。各ユーザーには 1 つの資金口座しか持てないべきです。では、どうすればユーザーに対して複数の資金口座を作成することを防げるでしょうか？  
この場合、資金口座テーブルの `ユーザーID` に一意インデックスを追加することで、新規追加の際に 1 つのリクエストだけが成功し、その他のリクエストは `一意インデックス違反` エラーを発生させることができます。例えば、Spring Framework では `org.springframework.dao.DuplicateKeyException` という例外が発生します。この場合、再度クエリを実行すればデータがすでに存在していることを確認できます。

### トークン機構でページの二重送信を防ぐ

**要件：** ページのデータは 1 回だけ送信されるべき。

**発生原因：** ユーザーの連続クリック、ネットワークの再送信、Nginx によるリクエストのリトライなどにより、データが重複送信される可能性がある。

**解決方法：**

- **クラスタ環境:** `トークン + Redis` を使用する。
- **単一 JVM 環境:** `トークン + Redis` または `トークン + JVM メモリ` を使用する。

**処理フロー：**

1. データ送信前にサービスからトークンをリクエストし、トークンを `Redis` または `JVM メモリ` に保存する。トークンには有効期限を設定。
2. フォームを送信後、バックエンドでトークンを検証し、トークンを削除した上で、新しいトークンを生成し返却する。

**トークンの特徴：** 事前にリクエストする必要がある、一度だけ有効、リクエストのレート制限が可能。

**注意：** `Redis` を使用する場合は `削除操作` でトークンの有無を判定するべきです。`SELECT + DELETE` を行うと並行処理の問題が発生するため推奨されません。

### 悲観ロック

データを取得する際にロックをかける方法です。

```sql
SELECT * FROM table_xxx WHERE id='xxx' FOR UPDATE;
```

**注意：** `id` は主キーまたは一意インデックスである必要があります。そうでない場合、テーブル全体がロックされ、パフォーマンス問題が発生する可能性があります。

悲観ロックは一般的にトランザクションと組み合わせて使用されます。データのロック時間が長くなる可能性があるため、状況に応じて選択する必要があります。

### 楽観ロック

楽観ロックは、データの更新時のみロックをかけ、それ以外の時間はロックをかけません。悲観ロックと比較すると、効率が高い方法です。

**実装方法は複数あります。以下の 2 つの方法が一般的です：**

#### 1. バージョン番号を用いた方法

```sql
UPDATE table_xxx SET name=#name#, version=version+1 WHERE version=#version#
```

#### 2. 条件による制約

```sql
UPDATE table_xxx SET avai_amount=avai_amount-#subAmount# WHERE avai_amount-#subAmount# >= 0
```

この方法では `avai_amount - subAmount >= 0` という条件を満たす場合のみ更新が実行されます。  
このシナリオではバージョン番号を使用せず、データの安全性をチェックするだけで済むため、在庫管理やリソースの割り当てに適しています。

**注意：** `楽観ロックの更新処理` は `主キー` または `一意インデックス` で行うのが望ましいです。  
例えば、上記の SQL を次のように修正すると、`行ロック` で済みます。

```sql
UPDATE table_xxx SET name=#name#, version=version+1 WHERE id=#id# AND version=#version#

UPDATE table_xxx SET avai_amount=avai_amount-#subAmount# WHERE id=#id# AND avai_amount-#subAmount# >= 0
```

### 分布式ロック

データの挿入を例に考えてみましょう。分散システムでは、グローバルな一意インデックスを作成するのが難しい場合があります。例えば、ユニークなフィールドを特定するのが困難な場合です。

このような場合、**分布式ロック** を導入することで、**Redis** や **Zookeeper** などのサードパーティシステムを利用してロックを管理できます。  
ビジネスシステムがデータを挿入または更新する際、まず分布式ロックを取得し、その後操作を実行し、完了後にロックを解除します。  
これは、**並行処理を制御する手法** の一つであり、分散システムでよく使用されるアプローチです。

**分布式ロックに関する詳細は、分布式ロックのまとめ記事を参考にするとよいでしょう。**

### `SELECT + INSERT` の方法

**低頻度なバックエンドシステムやジョブタスク** では、冪等性を確保するために `SELECT + INSERT` を利用することがあります。  
この方法では、**事前にデータをクエリし、すでに処理済みかどうかを確認** した後、未処理の場合のみビジネス処理を実行します。

**注意：** 高並行環境ではこの方法は適していません。

### 状態機械を利用した冪等性の確保

請求書関連のビジネスやタスク関連のビジネスでは、**状態遷移（State Transition）** を考慮する必要があります。  
通常、業務プロセスには**有限状態機械（Finite State Machine, FSM）** が存在し、特定の条件下でのみ状態が変更されます。

例えば、ある注文がすでに「発送済み」の状態になっているときに、「支払い待ち」に戻すことは通常許可されません。  
このように、状態機械を適切に設計することで、冪等性を確保することができます。

**注意：** 注文管理などの業務システムでは、状態の遷移フローが長くなることがあるため、**状態機械を正しく理解することがシステム設計の向上に役立ちます。**

## 外部 API の冪等性確保方法

### 例：銀行の送金 API

例えば、ある銀行が提供する送金 API では、加盟店が送金リクエストを送る際に、以下の 2 つのフィールドを付与する必要があります。

- **`source`（リクエストの発信元）**
- **`seq`（シーケンス番号）**

この `source + seq` をデータベースで一意インデックスとして設定することで、**同じリクエストが何度送信されても、処理が 1 回しか実行されないように制御** できます。（並行処理時も、同じリクエストは 1 回のみ受け付けられます。）

### 重要ポイント：

外部 API が**冪等性を保証** するためには、**最低でも以下の 2 つのフィールドを API リクエストで渡す** 必要があります。

1. **`source`（リクエストの発信元）**
2. **`seq`（シーケンス番号）**

これらのフィールドを受け取る側のシステムで**ユニークインデックス** を作成し、リクエストが処理済みかどうかを **事前にクエリ** することで、冪等性を確保できます。

**注意：**  
冪等性を確保するために、**必ず事前にクエリを実行し、処理済みかどうかを確認** してください。  
事前クエリをせずに直接データを挿入すると、**一意制約違反エラー** が発生する可能性があります。  
この場合、実際には処理が完了しているにも関わらず、エラーが発生してしまい、リクエストの正確な処理状況が分かりづらくなります。

## 最後のまとめ

**冪等性は、優秀なプログラマーにとって不可欠な考え方です。**  
システムを設計する際、特に以下のような業界では **冪等性の確保が極めて重要** になります。

- **第三者決済プラットフォーム**
- **銀行システム**
- **インターネット金融サービス**

これらのシステムでは、高速かつ正確な処理が求められるため、**誤って二重課金・二重送金が発生することは許されません。**  
そのため、冪等性を考慮しない設計では、**システムの信頼性が著しく低下し、ユーザー体験が大幅に損なわれる** 可能性があります。

冪等性を確保することは、単なる技術的な課題ではなく、**安定したシステムを構築するための基本的な設計思想** です。

---

### [私たちはLeapcell、バックエンド・プロジェクトのホスティングの最適解です。](https://leapcell.io/?lc_t=q_idempotency)

[![Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/c99837e7-693d-48c0-89f5-82c3c05176e2.png)](https://leapcell.io/?lc_t=q_idempotency)

[Leapcell](https://leapcell.io/?lc_t=q_idempotency)は、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです：

**複数言語サポート**

- Node.js、Python、Go、Rustで開発できます。

**無制限のプロジェクトデプロイ**

- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

**比類のないコスト効率**

- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。

**洗練された開発者体験**

- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

**簡単なスケーラビリティと高パフォーマンス**

- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。

[ドキュメントで詳細を確認！](https://docs.leapcell.io/)

[![Try Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/f38acf76-9fa5-4ac8-94e8-52c709ae260b.png)](https://leapcell.io/?lc_t=q_idempotency)

Xでフォローする：[@LeapcellHQ](https://x.com/LeapcellHQ)

---

[ブログでこの記事を読む](https://leapcell.io/blog/idempotency-explained)