Concurrency:同時リクエストを正しく扱う
Spring Bootは複数のHTTPリクエストを同時に処理します。Javaサービスをステートレスに保ち、残高競合の最終判断をPostgreSQLへ委ねる理由を学びます。
Java / Spring Bootを入口に、金融システムのService・Repository・Transaction設計を読む。
先に学ぶ内容 — Prerequisite: ソフトウェア工学 / 銀行システム学ぶこと — LEARNING GOALS
このページで学ぶこと
- Webアプリケーションの同時実行
- singleton Beanと共有可変状態
- DBロックとJava側事前確認の限界
体験38 · JAVA CONCURRENCY(並行を正しく扱う)
複数のリクエストが同時に届いた時、Payment Serviceは毎回新しいインスタンスを作るか確かめよう
まずは予想をひと言。本文を読んだ後、教材内の参照元で実際のServiceと相関IDの実装を確認します。
- 01まず予想する — PREDICT
まずここだけ:思ったことをひと言書く
複数のリクエストが同時に届いた時、Payment Serviceは毎回新しいServiceインスタンスを作ると思いますか?
正解でなくて大丈夫です。「〜だと思う」から始めてみましょう。
ヒント1を見る
SpringのControllerやServiceは、既定でいくつ作られるでしょうか?
ヒント2を見る
同じインスタンスを複数のリクエストが使う時、何に注意すべきでしょうか?
もっと詳しく:この問いで使う専門用語
- singleton Beanは、アプリ内で通常1つだけ作られ共有される部品です。
- ステートレスは、リクエスト固有の状態を共有フィールドへ残さない設計です。
- row lockは、同じDB行の競合更新へ順番を付ける仕組みです。
日常語で理解する — PLAIN LANGUAGE
まず、ここだけつかむ
Spring Bootは複数リクエストを同時に処理し、同じsingleton Serviceが複数スレッドから呼ばれます。
Payment Serviceはリクエスト固有値をBeanのフィールドへ保存せず、メソッド内だけで扱うステートレスな構成です。
残高の競合はJavaの事前確認では解決できません。更新と同じDB transactionでlockし、最新状態を再確認します。
順番に理解する — DIFFICULT PARTS, STEP BY STEP
難しいポイントを、順番にほどく
専門用語を覚えることより、「なぜ必要か」「このシステムのどこで使うか」を理解することが目標です。
singleton Bean
アプリ内で通常1つだけ作られ共有される部品です。
軽量ですが、可変状態を持つとリクエスト間で混線するためです。
ControllerやServiceは引数とローカル変数を使います。
ステートレス
リクエスト固有の状態を共有フィールドへ残さない設計です。
並行処理と複数インスタンスで結果を一貫させるためです。
amountやtokenをServiceフィールドへ保存しません。
row lock
同じDB行の競合更新へ順番を付ける仕組みです。
古い残高を基に複数送金が確定するのを防ぐためです。
既存PostgreSQL RPCが更新地点でlockします。
発展 — ADVANCEDもっと詳しく確認する最初の学習では読まなくても問題ありません。仕組みをもっと深く理解したい場合に開いてください。
同じServiceインスタンスが複数リクエストから使われる
SpringのController、Service、Componentは既定でsingletonです。複数スレッドが同じインスタンスのメソッドを同時に呼びます。
PaymentApplicationServiceはリクエスト固有の金額やtokenをフィールドへ保存せず、メソッド引数とローカル変数だけで扱います。このステートレス性がスレッド間の混線を防ぎます。
残高を先に読んでも安全にはならない
Java側で残高を表示・事前確認しても、RPC実行までに状態は変わり得ます。正式な検査は、更新と同じDB transaction内で行う必要があります。
- 01リクエストAが残高10,000円を読む
- 02リクエストBも同じ残高を読む
- 03Aが8,000円の振込を実行
- 04Bも8,000円を実行しようとする
- 05最終更新地点でlockと残高再確認がなければ過剰送金になる
排他制御の責任は既存RPCに残す
| 層 | できること | できないこと |
|---|---|---|
| Controller | 入力形式を拒否 | 同時送金間の残高を直列化 |
| Service | 処理順序を表す | 別HTTP接続のDB lockを共有 |
| Gateway | 同じtokenでRPCを呼ぶ | 業務lock規則を置き換える |
| PostgreSQL RPC | 対象行をlockし同じtransactionで再確認 | Javaプロセス内の共有状態管理 |
相関IDは並行処理を追跡するためにある
RequestIdFilterはX-Request-Idを引き継ぐかUUIDを生成し、request属性、レスポンスヘッダー、MDCログへ設定します。
同時に大量のリクエストが動いても、requestIdで1件の経路を追えます。ただし外部入力のrequestIdを無制限に信頼する場合は、長さや文字種、ログ注入への対策も検討対象です。
体験学習の続き · LEARNING LOOP
ここまで読めましたか?続けて確認してみましょう
singleton Beanとステートレスの考え方を読んだところで、教材内の参照元で実際のServiceと相関IDの実装を確認し、自分の言葉で説明してみます。
- 02仕組みを確かめる — CONCEPT CHECK
画面操作なしで、仕組みを確かめる
この仕組みは、Mini Bankの画面には現れません。次のEvidenceとCode Trail、教材本文だけで理解を進めます。
- 03証拠を確認する — EVIDENCE
何を見たか、1つずつ確かめる
Evidence(判断の根拠となる記録)を確認したらチェックします。各項目の「何を見るか」を手がかりにしてください。
画面を開けない項目は、Source Trail(教材の参照元)と教材の説明で確認できます。失敗を実際に起こす必要はありません。
- 04コードの処理経路を追う — CODE TRAIL
コードを書く必要はありません
ここでは「説明されている仕組みが、実際のMini Bankのどこに実装されているか」を確認します。まずは日本語の役割説明を読み、入力した依頼がどこへ渡るかをつかみましょう。コードの全文を読む必要はありません。
まずServiceが可変状態を持たないことを見て、次に並行リクエストを追跡する相関IDの実装を見ます。
教材の参照元を確認する — Source Trail ↓共有可変状態を持たないService。
services/payment-service/src/main/java/com/minibank/payment/application/PaymentApplicationService.java並行リクエストを追う相関ID。
services/payment-service/src/main/java/com/minibank/payment/config/RequestIdFilter.java - 05自分の言葉で説明する — EXPLAIN
LEVEL 1 — やさしい説明(まずここだけ)
なぜPaymentApplicationServiceのようなsingleton Beanで、リクエスト固有の値をインスタンスのフィールドへ保存してはいけないのでしょうか? 短い2文を目安に説明してください。
日常語だけで完了できます。専門用語の数や正誤を自動採点するものではありません。
ヒント1を見る
「ステートレス」という言葉を使って書いてみましょう。
ヒント2を見る
リクエスト固有の値をどこに置くべきか考えてみましょう。
もっと詳しく:LEVEL 2 — 専門用語を使った説明(任意)
singleton Bean、ステートレス、row lockの3つを使って、並行リクエストを安全に扱う設計を説明してみましょう。
挑戦する場合は、上の説明欄に追記できます。追加しなくても完了できます。
説明の確認ポイント — Checkpoint- singletonが複数リクエストから共有されると説明できる
- リクエスト固有値を共有フィールドへ置かない理由を説明できる
理解を確かめる — CHECK YOUR UNDERSTANDING
3問で理解チェック
答えを頭の中で考えてから、各項目を開いて確認してください。
Q1ServiceのフィールドへcurrentUserを保存してよいですか?
いけません。別リクエストから上書きされる共有可変状態になります。
Q2Javaで残高を読んで十分なら、その後のlockは不要ですか?
不要にはできません。読み取り後から更新までに別処理が残高を変えられます。
Q3requestIdは排他制御を行いますか?
行いません。並行するログから同じリクエストを追跡する識別子です。
完全に暗記する必要はありません。大切な考え方を説明できそうなら押してください。
教材の参照元 — SOURCE TRAIL
根拠と関連教材
CURRENT IMPLEMENTATIONは確認したrepository内の実装です。HISTORICAL IMPLEMENTATIONは導入・修正の履歴で、後続migrationによる再定義があり得ます。RELATED LESSONは教材への案内で、実装の一次Sourceではありません。ASSUMPTIONは仮定、UNKNOWNは未確認を表します。
確認基点: d02eae8914f39cbf06bfe422dc39c71886f82b3e / 2026-09-11。稼働環境への適用確認: UNKNOWN。パス実在の自動検査と、記述した技術的事実の確認は別です。
- 現在の実装 — CURRENT IMPLEMENTATION
services/payment-service/src/main/java/com/minibank/payment/application/PaymentApplicationService.java共有可変状態を持たないService - 現在の実装 — CURRENT IMPLEMENTATION
services/payment-service/src/main/java/com/minibank/payment/config/RequestIdFilter.java並行リクエストを追う相関ID - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/口座lockと残高再確認の実装