トランザクションスコープの決定などについて少し悩んだことがあるので、メモしておこうかと思います。
トランザクションスコープ設計の基本原則
トランザクションサイズの最適化
基本的な考え方:
- トランザクションはできるだけ小さくまとめる
- デッドロックのリスクを最小化する
- 処理の性質に応じて適切に分割する
トランザクション分割の判断ポイント
失敗時のロールバック要件による分割
分割が必要なケース:
失敗した処理を「失敗した処理」として明記し、その記録自体はコミットする必要がある場合。
従来の方式(問題あり):
トランザクション開始
├─ 処理1
├─ 処理2
└─ コミット
推奨する方式:
トランザクション開始
├─ 処理1
└─ コミット
トランザクション開始
├─ 処理2
└─ コミット
この分割により:
- 処理1は完結しているため、処理2から再開可能
- 各処理の成否を独立して管理可能
- 部分的な成功状態を適切に記録可能
処理の性質による分割
分割を検討すべき処理:
-
成否確認が困難な処理
- メール送信、プッシュ通知
- 外部サービスとの連携
- 送信成功通知があってもユーザーへの実際の配信は保証できない
-
影響度の低い処理
- ログ記録
- 統計データの更新
- 分析用データの生成
これらの処理は:
- トランザクションスコープ外に配置
- 失敗してもメイン処理のロールバックは行わない
長時間処理の分割
分割が必要な理由:
- レスポンスタイムの改善
- ロック時間の短縮
- デッドロック発生リスクの軽減
ループ処理での考慮点:
基本的に1ループ = 1トランザクションとして設計することで、処理の中断・再開が容易になります。
アンチパターン:過大なトランザクションスコープ
問題の症状
性能面の問題:
- レスポンスが異常に遅い
- 障害時の待機時間が長い
- デッドロック発生の増加
運用面の問題:
- 非同期処理で成功表示後に実際は処理が失敗
- 障害時の影響範囲が広い
- 復旧処理が複雑化
対策手法
設計時の対策:
-
1リクエスト = 1トランザクション原則
- 処理の単位を明確化
- どの処理がどのトランザクションに属するかを明確にする
-
プログラム構造の改善
- コントローラーに処理を集中させない(Serviceクラスで処理を書いて、Action層でトランザクションスコープを書くのがよい。 https://qiita.com/umanari145/items/7dc8daed4ae2e1cc971b)
- 処理の責任範囲を明確化
-
不要な処理の除去
- 1リクエスト内に複数の独立した処理を混在させない
- 継ぎ足し的なシステム構築を避ける
システム基盤の改善:
- キューイングシステムの導入
- 非同期処理機構の活用
- 適切な処理の並列化
ロールバック不可能な処理の考慮
処理順序の最適化
問題のあるパターン:
API登録 → DB登録
この場合、DB登録に失敗してもAPI側の取り消しができない可能性があります。
推奨するパターン:
DB登録 → API更新
この順序により:
- API更新の成否で全体の処理確定を判断
- API失敗時にはDB側のロールバックで整合性を保持
- ロールバック困難な処理を後回しにすることでリスクを軽減
トランザクション内での処理順序
基本原則:
- ロールバック可能な処理を先に実行
- ロールバック困難な処理を後に配置
- 外部システムとの連携は可能な限りトランザクション外で実行
例外発生時のトランザクションスコープ
スコープ設計の考慮点
単一レコード vs 複数レコード:
- デッドロックリスクを考慮し、可能な限り小さなスコープを選択
- 業務要件で複数レコードの同時更新が必須の場合のみ、大きなスコープを採用
処理継続の判断:
- 大量データ処理では、個別レコードエラーでの全体ロールバックは避ける
- エラーレコードのログ記録と処理継続を検討
- トランザクション単位でのリトライ機構の実装
まとめ
適切なトランザクションスコープの設計は、以下の要素を総合的に考慮する必要があります:
設計時の考慮点:
- 処理の独立性と依存関係の明確化
- ロールバック要件の詳細な検討
- 外部システム連携時の処理順序の最適化
性能・運用面の考慮点:
- トランザクション時間の最小化
- デッドロック発生リスクの軽減
- 障害時の影響範囲の限定