銀行エンジニアリング 04:冪等性・残高再構築・整合性検査
同じ依頼が2回届いても1回分しか記帳しない。台帳から残高をいつでも作り直せる。ずれを自動で見つける。この3つが、障害時にも説明できる銀行システムの土台です。
v8以降に実装した台帳・記帳エンジンなどを実際に動かし、正常時と異常時の両方から銀行システムの構造を学ぶ。
Prerequisite: BANKING SYSTEMLEARNING GOALS
このページで学ぶこと
- Idempotency Key と request fingerprint
- 時点を指定した残高再構築
- Posting整合性検査が見るもの
- 「DB更新直後にAPI応答が失敗した」ときの考え方
PLAIN LANGUAGE
まず、ここだけつかむ
同じ Idempotency Key の依頼は、内容が同じなら同じ結果を返し、違えば拒否します。
通信エラー後は同じキーで再送すれば、記帳済みかどうかに関係なく1件だけになります。
仕訳から残高をいつでも再構築でき、整合性検査がずれを見つけて記録します。
DIFFICULT PARTS, STEP BY STEP
難しいポイントを、順番にほどく
専門用語を覚えることより、「なぜ必要か」「このシステムのどこで使うか」を理解することが目標です。
Idempotency Key
1つの依頼を識別する番号です。
再送を安全にするためです。
(発生元, イベント種別, キー)の一意制約と fingerprint 照合(PE009)。
Balance Reconstruction
記録から残高を計算し直すことです。
キャッシュが正しいか確かめ、過去時点の残高を説明するためです。
posting.recompute_balances(時点)。
Integrity Check
記録全体の不変条件を後から検査することです。
処理中の防御をすり抜けたずれを見つけるためです。
posting.run_integrity()。check_type は posting。
Level 1 基礎:同じ依頼を2回受け取ったら
通信が不安定だと、利用者の画面は「失敗した」と表示していても、銀行側では処理が終わっていることがあります。利用者がもう一度ボタンを押しても二重に記帳しないよう、依頼ごとに一意な番号(Idempotency Key)を付けて、同じ番号の依頼は同じ結果を返します。この性質を冪等性(Idempotency)と呼びます。
Level 3 システム:MiniBankでの実装(正常時)
| 仕組み | 実装 |
|---|---|
| 一意性 | posting.business_events の(発生元, イベント種別, Idempotency Key)に一意制約 |
| 内容の一致確認 | イベント種別・金額・文脈から作る request fingerprint が同じなら既存イベントを返し、違えば PE009 |
| 同時に届いた重複 | posting.find_idempotent() が同じキーの処理を advisory lock で直列化してから確認する |
| 画面からの再送 | 管理画面の操作フォームは送信ごとに Idempotency-Key を作り、通信エラー後の再送では同じキーを使う |
異常時:DB更新直後にAPI応答が失敗した
| 段階 | 内容 |
|---|---|
| 症状 | 管理者の画面に「通信エラー」と表示されたが、記帳されたかどうか分からない |
| 調査 | 同じ Idempotency Key で再送する。フォームは通信エラー時にキーを保持している |
| 考えられる原因 | ①DBに届く前に失敗 ②DBで記帳が完了した後、応答だけが失われた |
| 真因の見分け方 | 再送で①なら新しく記帳され、②なら既存のイベントがそのまま返る。どちらでも記帳は1件だけ |
| 対応 | 結果が返るまで同じキーで再送する。新しいキーで送り直すと別の依頼になり二重記帳の原因になる |
| 再発防止 | キーは画面操作の開始時に1つ作り、成功または確定的な失敗(4xx)まで使い続ける |
Level 3 システム:残高の再構築と整合性検査
- posting.recompute_balances(時点):仕訳明細だけから全勘定の残高を計算し直し、顧客口座は cached_balance との差額も返す。時点を指定すると、その時刻までの仕訳だけで計算する
- posting.run_integrity():全体の借方=貸方、仕訳ごとの借方=貸方、cached残高と仕訳残高、イベント⇔仕訳⇔ルールの対応、イベント金額と仕訳合計、イベントのない仕訳、取消仕訳が元仕訳の正確な反転か、を検査する
- 検査結果は v7 と同じ reconciliation.integrity_runs / integrity_issues に check_type = 'posting' として保存され、v7 の整合性チェック画面にも表示される
異常時:検査が不一致を見つけた
DBテストでは、テスト用DBに限りトリガーを外して残高キャッシュを7円ずらし、借方だけの明細を1行差し込みます。検査は BALANCE_MISMATCH・JOURNAL_NOT_BALANCED・LEDGER_TOTAL_IMBALANCE を検出し、差し込みを元に戻すと再び0件になることを確認しています。
Level 4 アーキテクチャ:Exactly-once は作らず、At-least-once+冪等で近づける
ネットワーク越しに「必ず1回だけ届く」ことは保証できません。現実的な設計は「少なくとも1回は届ける(再送する)」と「受け取る側で重複を無害にする(冪等)」の組み合わせです。MiniBankは、キーの一意制約・内容の照合・直列化の3点で、記帳が1回分だけになる状態を作っています。
さらに、記帳の正しさは処理中の検査だけに頼らず、後から台帳全体を検査して確かめます。処理時の防御と事後の照合を分けることで、想定外の経路で生じたずれも見つけられます。
どこを確認すればよいか
- 管理画面「Ledger・Posting」→ Posting整合性チェック・残高差異
- 管理画面「整合性チェック」→ check_type が posting の実行履歴
- scripts/banking/v8-posting.test.mjs の各テスト
CHECK YOUR UNDERSTANDING
3問で理解チェック
答えを頭の中で考えてから、各項目を開いて確認してください。
Q1通信エラーの後、新しいキーで送り直すと何が起こりえますか?
前の依頼が実は成功していた場合、二重記帳になります。
Q2同時に同じキーで6回送ったら何件記帳されますか?
1件です(DBテストで確認)。
Q3整合性検査は不一致を自動で直しますか?
直しません。検出して記録し、修正は取消・訂正や修復手続きで行います。
完全に暗記する必要はありません。大切な考え方を説明できそうなら押してください。
SOURCE TRAIL
根拠と関連教材
CURRENT IMPLEMENTATIONは確認したrepository内の実装です。HISTORICAL IMPLEMENTATIONは導入・修正の履歴で、後続migrationによる再定義があり得ます。RELATED LESSONは教材への案内で、実装の一次Sourceではありません。ASSUMPTIONは仮定、UNKNOWNは未確認を表します。
確認基点: d02eae8914f39cbf06bfe422dc39c71886f82b3e / 2026-09-11。稼働環境への適用確認: UNKNOWN。パス実在の自動検査と、記述した技術的事実の確認は別です。
Implementation Status: IMPLEMENTED IN MINIBANK(この教材が扱うコードの範囲)
- RELATED LESSON/docs/java-idempotencyIdempotencyの一般論
- RELATED LESSON/docs/reconciliation照合と整合性検査(v7)
- CURRENT IMPLEMENTATION
supabase/migrations/20260924100000_v8_posting_engine.sqlposting.post_event / posting.find_idempotentBusiness Eventの記帳と冪等性 - CURRENT IMPLEMENTATION
supabase/migrations/20260924100000_v8_posting_engine.sqlposting.recompute_balances / posting.run_integrity残高再計算と整合性検査 - CURRENT IMPLEMENTATION
scripts/banking/v8-posting.test.mjs冪等性・同時実行・取消・訂正・整合性検出のDBテスト