Open7

マイクロサービスパターン(Microservices Patterns)

tamaco489tamaco489

後で整理するものリスト

  • サーガについて
  • サーガをコーディネートするための2つのアーキテクチパターン
    • コレオグラフィの概要
    • オーケストレーションの概要
  • ローカルトランザクションが失敗時に実行するべきトランザクション処理パターン
    • 補償トランザクションについて
    • ピボットトランザクションについて
    • 再試行可能トランザクションについて
  • コンウェイ・逆コンウェイの法則について
  • 分離性の欠如への対処方法について
    • 更新の消失
    • ダーティーリード
    • ファジー/反復不能読み取り
  • ビジネスロジック構成パターンについて
    • Transaction script
    • Domain model
  • ドメイン駆動設計について
    • DDD の Agratate パターンを使ったビジネスロジックの設計
    • マイクロサービスアーキテクチャにおける Domain event パターンの使い分け
  • ドメインイベントの見つけ方について
    • イベントのブレーンストーミングについて
    • イベントトリガーの特定
    • アグリゲートの特定 ※イベント、コマンド、アグリゲート、ポリシーを洗い出す
  • イベントソーシングを用いた設計パターンについて
    • 概要
    • 楽観的ロックを使った同時更新について
    • イベントソーシングとイベントのパブリッシュについて
    • 利点と欠点
  • マイクロサービスアーキテクチャでのクエリーの実装
    • API composition パターン
    • CQRS パターン
  • 外部APIパターン(API ゲートウェイパターン)
    • 概要
    • API ゲートウェイのアーキテクチャ
    • BFF(Backend for frontend)パターンの導入
    • 利点と欠点
    • 設計上の問題

Reference

https://microservices.io/index.html
https://martinfowler.com/microservices/
https://book.impress.co.jp/books/1118101063

tamaco489tamaco489

CAP定理について

概要

https://en.wikipedia.org/wiki/CAP_theorem?_fsi=hoHF169O

分散システムは以下の3つの特性のうち同時に2つまでしか満たせないことを意味する。

項目 意味 説明
C (Consistency) 一貫性 すべてのノードが同じデータを返す
A (Availability) 可用性 全てのリクエストが必ず応答を返す
P (Partition Tolerance) 分断耐性 ネットワーク障害が起きてもシステムが動作し続ける
なぜ3つ同時に満たすことができないとされているのか

例: ノードが2つあり、ネットワークが分断されたとき

  • 一貫性を守ろうとすると → 片方は応答を止める → 可用性を失う
  • 可用性を守ろうとすると → 両方で処理しちゃう → データがずれる → 一貫性を失う
  • でもネットワーク分断(Partition)は現実に必ず起こる

なので、「分断耐性は現実的に必須」だから、多くの設計は C or Aのどちらを優先するか を選ぶことになる。


システム例 選ばれる組み合わせ
リレーショナルDB (例: RDB, 2PC) CA (Pは弱い)
DynamoDB, Cassandra AP
Zookeeper, etcd CP
tamaco489tamaco489

サーガをコーディネートするための2つのデザインパターン

サーガ (Saga) とは

https://docs.aws.amazon.com/ja_jp/prescriptive-guidance/latest/modernization-data-persistence/saga-pattern.html

分散トランザクションの代替パターンで、複数のローカルトランザクションを順に実行し、何か問題が起きた場合は補償トランザクションを実行して整合性を保つ仕組みのこと。

ローカルトランザクション: 1つのマイクロサービス内で完結する、通常のトランザクション処理
→1つのマイクロサービス内で完結する通常のトランザクションを意味する。


サーガのコーディネーション方式:2つのパターン

コレオグラフィ(Choreography)について

概要

  • 各サービスが自律的にイベントを発行し、それに反応するかたちで処理を進めていく方式。
  • 中央で全体を制御する専用のコーディネータ(制御役)は存在しない。
  • いわゆる「イベント駆動」的なかたちで構成される。


ざっくりなフロー

例: ECサイトにおける注文処理のケース

  • OrderService が「注文作成完了」イベントを発行。
  • PaymentService がそのイベントを受けて支払い処理を実施 → 「支払い完了」イベントを発行。
  • InventoryService が「支払い完了」イベントを受けて在庫を引き当てる。
  • 各イベント発行・処理が 疎結合で順次連鎖している様子。
  • 各ステップは非同期イベントでつながるため、柔軟性とスケーラビリティが高いが、フローの可視性が下がるのが難点。


pros/cons

pros:

  • サービス間の疎結合性が高い。
  • 拡張が容易(新しい参加者を追加しやすい)。
  • イベント駆動アーキテクチャと相性が良い。

cons:

  • 全体の流れが見えにくくなる(スパゲッティ的なイベント連鎖)。
  • トラブルシューティングやデバッグが難しい。
  • 複雑なロールバックが分散するため実装負担が増える。
オーケストレーション(Orchestration)について

概要

  • 専用の「サーガ・オーケストレータ(Saga orchestrator)」が存在し、各ステップを明示的に順序制御する中央集権型の設計。
  • 指揮者(オーケストレータ)が各サービスを呼び出して完了を管理する仕組み。


例: ECサイトにおける注文処理のケース

  • OrchestratorOrderService に「注文作成」を指示。
  • 完了後に OrchestratorPaymentService に「支払い処理」を指示。
  • InventoryService に「在庫引当」を指示。
  • 問題が起きたら Orchestrator が補償処理を順に指示。
  • E --> F{支払い成功?} で明確に判定ノードを挿入し、成功時と失敗時のフローを分岐させる。
  • 成功時は在庫引当 → 注文完了へ、失敗時はキャンセル処理へと明確に区分。
  • コレオグラフィと比較して、フロー全体の可視性と制御構造が分かりやすい。

pros/cons

pros:

  • フローの全体像が一元管理されて明確。
  • ログ管理・エラー処理・再試行が集約されるので保守性が高い。
  • ロジックの追跡や監査も容易。

cons:

  • オーケストレータ自体が複雑化・肥大化する可能性。
  • 中央制御なので疎結合性はやや犠牲になる。
  • 単一障害点(SPOF)になりうる。
サーガのコーディネーション方式:比較
観点 コレオグラフィ オーケストレーション
管理者 不在(分散制御) サーガオーケストレータ
分かりやすさ イベントが複雑化しやすい 処理の流れが明確
柔軟性 高い(疎結合) 低い(集中管理)
実装の簡単さ シンプルな処理には向く 複雑な処理には向く
障害時の対応 各サービスが自律的に補償 オーケストレータが集中制御
規模感 小規模・中規模に向く 大規模・複雑処理に向く
tamaco489tamaco489

サーガにおける失敗時のトランザクション処理パターンについて

サーガでは、分散システムである以上、一貫性と信頼性を保ちつつも、ビジネス要求などに向き合うための可用性、スケーラビリティについても考慮する必要がある。

背景にある課題(分散トランザクションの難しさ)

従来のモノリシックなシステムの場合、1つのRDBMSの中で begin ~ commit or rollback のようなトランザクションを用いてACID特性を確保することが可能であるが、マイクロサービスでは以下のようなことに考慮する必要がある。

  • システムが複数のサービスやDBに分割されている。
  • ネットワーク越しに非同期的にデータのやり取りを行う必要がある。
  • 各コンポーネントは独立にスケール、デプロイさせる必要がある。


上記のような構成では、以下の問題を抱えることになる。

  1. 可用性の低下

→全サービスが応答しないと commit できない。つまり、OrderService=注文データを保存、PaymentService=支払い処理を実行、InventoryService=在庫を引き当てる、という一貫性のある状態にできない、ということ。

  1. スケーラビリティの低下: Lock、コーディネーションの負荷が高い
  • 2Phese Commitを前提とした場合、全サービスの状態を中央集権的に調整(コーディネーション)しなければならない。
  • NW越しに行うとなった場合はさらに状態を都度確認、同期をとるなども考慮する必要性が出てくる。

→故に遅延やLock状態による他の処理に影響が出やすいサービスになってしまう。

  1. 実装困難: サポートされていないDB、言語が多い
  • 例えば以下のように構成されているとした場合
    • OrderService: Datastore=PostgreSQL, Application=Java
    • PaymentService: Datastore=DynamoDB, Application=Go
    • Inventory: Datastore=MySQL, Application=PHP

→この状態で、同じ分散トランザクションの仕組みをサポートするのは中々しんどい。※DBが同じプロトコル、トランザクション方式をサポートしていない可能性もあれば、言語間でライブラリが統一されていない等も現場によっては全然あり得る。


その結果、各サービスのローカルトランザクションだけで、最終的な整合性を保つ仕組みを設計できないかどうかに軸を置いて検討が進み、以下の考慮が必要とされた。

  • 補償トランザクション: 後続の処理の失敗に備えた「巻き戻し」が必要。
  • ピボットトランザクション: 取り消し不能な境界を意識して設計する。
  • 再試行可能トランザクション: 一時的な失敗を吸収して処理継続を可能にする考え方。


より現実的な整合性の確保を実現するにあたり、CAP定理における「C(整合性)」と「A(可用性)」のトレードオフの考慮する必要がある。

  • 強整合性を犠牲にしてても可用性を保つ。
  • その代わりに「最終的な整合性(eventual consistency)」を実現する。

失敗時のトランザクションパターン3点

1. 補償トランザクション(Compensating Transaction)

概要・特徴

ローカルトランザクションが成功した後に、後続処理の失敗などにより全体としてロールバックが必要になった場合、「元に戻す処理」として実行されるのがトランザクション。

  • 元の処理を厳密に巻き戻すことが理想(ただし不可能な場合も)。
  • 補償トランザクションはビジネスロジックとして明示的に設計が必要。
  • 実際には「削除」ではなく「ステータス変更」などで論理的取消しを行うケースが多い。


設計時に注意しなければならないこと

操作は“元に戻す”というより、“反対方向に進める”という考え方。実務では、論理的な取消し(例:ステータス変更) を行うことが多い。
→RDBの ROLLBACK のように完全に元に戻すのは、分散環境では非現実的として認識する。

order_reservations.status = 'ACCEPTED''CANCELLED'

※イベントログなどの痕跡が残る設計にしておくことで、監査やトレース的にも有効


補償処理は明示的に別トランザクションとして設計しておく。
例えばAPI連携で行う場合には以下のように各エンドポイントで行うべき責務を区別しておく。

  • 予約API: POST: /products/orders/{order_id}/purchase
  • 予約キャンセルAPI: POST: /products/orders/{order_id}/cancel
    →そのため、設計段階で「補償可能か?どう取り消すのか?」を検討しておくのがbetter。

非同期イベント経由で行うとリトライ・失敗対応の考慮も必要。
例えば、キャンセル指示を行うも処理途中にNW障害等で失敗してしまう等が考えられる。
→メッセージの再送などで、補償処理による再試行とリカバリ処理を行う必要がある。


「補償できない操作」はサーガに不向きであると認識する。
例えば外部システムとの連携、決済確定など「一度処理すると元には戻せない」といったケース。
→その操作をピボットトランザクションとして、より後続の処理に移す等の設計が必要になる。


補償トランザクション設計時に気にしておくべきことまとめ

項目 詳細
補償操作は論理的に実装可能か? データ削除よりステータス変更の方が安全・可視性が高い
補償操作は明示的に切り出されているか? API・イベント・関数などで分離設計されているか
冪等性があるか? 同じ補償処理を複数回実行しても正しく完了するか
補償失敗時の再試行/アラート設計は? 障害時のリカバリフロー・通知・運用方針があるか
補償できないケースは排除・設計変更しているか? ピボット配置の見直しや同期処理への切替など


2. ピボットトランザクション(Pivot Transaction)

概要・特徴

サーガの中でも取り消し不能な重要処理(例:決済確定など)を担うトランザクション。
「ここを越えるともう元には戻れない」という境界線 ピボット=支点 として機能する。

整理すると以下のようなイメージとなる。

  • ピボット処理の前に起きた失敗は補償トランザクションで巻き戻しが可能。
  • ピボット処理の後に起きた失敗は基本的には元に戻せない。

支払い確定契約成立通知 などが該当。


例:ECサイトにおける注文処理のサーガ

サービス 処理内容 補償可能か? 備考
OrderService 注文作成 ✅ 可能 ステータス変更で取消可能
PaymentService 支払い決済 ← ピボット ❌ 不可 決済完了後は戻せない
InventoryService 在庫引当 ✅ 可能 在庫予約解除で補償可能

この例では、支払い決済がピボットトランザクションとなる。
ここを越えて在庫引当で失敗しても支払いは元に戻せないため、「在庫が無かったら~」という対応を考えておく必要がある。


設計時に注意しなければならないこと

結論から言うとピボットは「なるべく後ろ」に配置しておくのがbetter。
→判断が難しい場合は、できるだけ補償可能な処理は先に終わらせてしまって、巻き戻せない処理は全て成功しそうなタイミングで実行する。とすることで、万が一の時に影響範囲を狭められるメリットがある。

より本質的なところでは、ピボット後に失敗した際の対応について考えておくことが重要。
例えば、「支払いは成功した。ただし、在庫がなかった。」といった例外が発生した場合、以下のように、ユーザ体験や整合性が破綻しないように対策を立てておく必要がある。

  • 一時的な「取り置き」の状態にしておく。
  • 事後返金するものとして、対応時に手動でレコードを変更。
    ※ユーザ申告で検知するのではなく、システム的にアラート発砲等で即座に検知できることが望ましい


ピボットトランザクション設計時に気にしておくべきことまとめ

項目 詳細
ピボットの位置は適切か? 補償可能な処理はできるだけピボット前に完了させる
ピボット処理は取り消せないことを認識しているか? 決済・契約確定・通知などは前提として巻き戻せない
ピボット後の障害時に整合性を保てるか? 返金・仮状態・人手対応などの戦略があるか
ピボット処理の手前ですべての前提条件が揃っているか? 不備のあるまま実行されると不可逆な損害になる可能性
業務ルールとして「後戻り不可」が明示されているか? ピボットを越えたら「業務上ロールバック不可」が明文化されているか
3. 再試行可能トランザクション(Retryable Transaction)

概要・特徴

一時的な障害による失敗を予め想定しておき、同じ処理を再度実行することで成功することを目指す設計のこと。

  • NWエラー、外部APIの一時的な負荷、接続タイムアウトなど、Http Status Code 5xx が該当。
  • 成功すれば補償処理は不要、失敗し続けた場合に補償やアラートへ移行するようなイメージ。


例:InventoryService における在庫引当処理

処理内容 結果 対応
在庫APIへの接続タイムアウト 失敗(瞬断) 数秒後にリトライ
再試行後に正常応答 成功 そのまま処理継続
再試行しても失敗 失敗確定 補償処理に切り替え or アラート通知


設計時に注意しなければならないこと

冪等性(idempotency) を保証することが重要。
→要するに実行しても結果が変わらないような設計をしておくということ)

例: 注文イベントが行われた後に既に注文済みの場合(orders.status = accepted)の場合は処理をスキップする。※この後にリトライが走ってもステータスが巻き戻っていない場合は同じようにスキップされる


リトライ設計のポイント

項目 設計方針
最大試行回数 無限ループを避けるために制限をかける(例:3回)
待機時間(delay) 次の試行まで一定時間または変動時間を空ける
バックオフ戦略 再試行間隔を徐々に伸ばす(例: 指数バックオフ)
タイムアウト制御 外部サービスの応答がなければ、早めに切り上げて再試行へ

失敗し続けた場合のハンドリングとしても考慮しておくのがbetter

  • すべての再試行が失敗したら「確定失敗」として扱う。
  • 補償トランザクションやエラーログ、アラート通知などに分岐する。


再試行トランザクション設計時に気にしておくべきことまとめ

項目 詳細
処理は冪等になっているか? 同じリクエストが複数回送られても副作用が出ない設計になっているか
再試行条件は明示されているか? リトライすべきエラーと、してはいけないエラーが分離されているか
最大試行回数と待機戦略は適切か? 繰り返し失敗による負荷増加やタイムアウトが防げているか
失敗時の分岐処理が設計されているか? 最終的な補償処理 or エラー報告のルートがあるか
ログや監視による追跡性が担保されているか? 問題発生時にトレース可能な仕組みが用意されているか
tamaco489tamaco489

コンウェイ・逆コンウェイの法則について

参考にさせていただいた記事
コンウェイの法則について

コンウェイの法則 - Melvin Conway, 1968 -

「組織が設計するシステムは、その組織のコミュニケーション構造を模倣したものになる」

例: 開発組織が以下のように構成されているとした場合

  • 商品チーム(Product Team)
  • 注文チーム(Order Team)
  • 支払いチーム(Payment Team)

この場合、この人たちが作るシステムも自然とこうなりがちであるという考え方。

[ProductService] <--> [OrderService] <--> [PaymentService]
逆コンウェイの法則について

望ましいアーキテクチャを実現するには、それに合わせて組織を再構成するべき、という考え方のこと。


例えば以下のような目的を持っている場合、サービス設計に合わせて組織編制を行う必要がある。

  • 「ドメイン駆動設計(DDD)」に基づいて バウンデッドコンテキストごとにサービスを切りたい。
  • 「独立デプロイ」「疎結合な責任分離」を強化したい。


上記を考慮した内容で組織編制した場合、例えば以下のようなメリットがあると考えられる。※必ずしもそうではないという前提で

  • OrderService の開発と運用がひとつのチームに集中 → 開発スピードと責任が明確になる。
  • Cross-functional なチーム(開発+テスト+運用)で、1サービスをオーナーシップできるように


イメージとしてはこのような形式(マイクロサービスでの具体例)

  • 商品チーム(Product Team)
  • 注文チーム(Order Team)
  • 支払いチーム(Payment Team)
両者の比較
法則名 概要 マイクロサービスとの関係
コンウェイの法則 組織構造がシステム構造に現れる 組織ごとの責務がサービス境界になる
逆コンウェイの法則 システム構造に合わせて組織を設計すべき 望むサービス境界に合わせてチーム構成を変える
tamaco489tamaco489

分離性の欠如への対処方法について

そもそもなぜ分離性を意識しないといけないのか?

マイクロサービスの本質的な狙いとしては以下が挙げられる。

  • 各サービスが独立にデプロイ、開発、スケーリングできること
  • 障害や変更が「局所化」され、システム全体に波及しないこと

ここで、この分離性が欠けてしまうことで、

  • 変更が他サービスに影響 → リリースが同時になり、実質モノリスのような構造になってしまう。
  • 障害が伝搬してしまう。つまり部分障害のはずが全体の障害に陥ってしまう。
  • テストや検証の複雑化による生産性の低下に陥りやすい。

といった問題に発展してしまう可能性がある。

とはいえで分散させたからといって全てが解決するわけではない

具体的なところでは、サービスを分散させた故にトランザクションが割れてしまい、ビジネスロジックの一貫性を担保することが難しくなった。 等がわかりやすい例。

→そのため、分散型のアーキテクチャを検討する場合は特に以下の3点について意識する必要がある。

課題 内容 解決指針(例)
更新の消失
(Lost Update)
並行更新によって、一方の更新が上書きされ消える 楽観ロック・バージョン管理・セマンティックロック
ダーティーリード(Dirty Read) トランザクション未確定の値を他のトランザクションが読み取ってしまう 悲観的制御・リトライ制御
ファジー/反復不能読み取り 同一トランザクション中で読み取るたびに結果が異なる 値の再読み・一貫したスナップショットの保持

※書籍にはなかったがファントムリードも同じようなことが言えるはず?

更新の消失(Lost Update)

概要

複数のクライアント/スレッドが同じデータに対して並行して更新処理を行うと、一方の変更が上書きされて消える現象。


主な原因

  • 読み→修正→書き込みのパターンで排他制御がない
  • 最後に保存した人がすべてを上書きする構造


例えば、ゲーム内のプレイヤー所持金の更新処理などがわかりやすい

処理の流れ:

  • プレイヤー player_id = 1001 がいて、「所持金(coins)」を管理している
  • このプレイヤーに対して 2つの処理 がほぼ同時に動く
処理A 処理B
クエスト報酬:+100コイン アイテム購入:-50コイン

結果: 最終的な所持金は 1050 にならないといけないのに 950 になってしまう。
クエスト報酬(+100)は完全に失われてしまう といった問題が起きる。

ダーティーリード(Dirty Read)

概要

未コミットのトランザクションの値を他トランザクションが読み取ってしまう状態。
もし最初のトランザクションがロールバックされると、読み取ったデータは「存在しなかった」ことになる。
→つまり、データ整合性・一貫性が壊れ、予測不能なバグにつながる。


主な原因

  • データベースのトランザクション分離レベルが低すぎる(特に READ UNCOMMITTED)。
  • 楽観的にデータを参照し、整合性よりもパフォーマンスを優先する実装方針にしてるなどはこの問題が起こりやすい。


例えばプレイヤーのコインを増減する処理などがわかりやすい

処理の流れ:

  • トランザクションA:プレイヤーのコインを +100(未コミット)
  • トランザクションB:現在のコイン数を参照(+100された状態を読み取る)
  • トランザクションA:何らかの理由で ロールバック
  • トランザクションB:存在しない状態を前提に処理


この結果、本来であれば、

  • トランザクションA(コイン +100) はまだコミットしていないため、他のトランザクション(B)は「+100された状態」を読み取ってはいけません。
  • トランザクションB は「コインの正確な現在値(ロールバック前の値)」を読み取り、
    それに基づいて処理を進めるべきです。

→つまり、未コミットの変更は外部に見えない状態が保たれ、一貫性が守られることが望ましい。


ダーティリードされてしまうことで、以下のような挙動になってしまう。

  • トランザクションBが未コミットの「+100コイン」状態を読み取ってしまうため、「コインが増えた」という誤った状態を前提に処理を行う。
  • その後、トランザクションAがロールバックされて元の状態に戻った場合でも、トランザクションBは「増えた状態」のまま動いてしまう。

→なので、もともと100コイン不足してたから決済できなかったけどトランザクションAの処理で+100になったから決済できるじゃん。とまだ確定していないにも関わらずトランザクションBが処理進めてしまってる状態。※ほんとはトランザクションAの100コイン購入処理は失敗しててトランザクションBも失敗しないといけなかったはずなのに、と

ファジーリード (Fuzzy Read)

概要

  • 同一トランザクション内で同じデータを2回以上読み取った際に、値が異なる現象のこと。
  • ノンリピータブルリード(反復不能読み取り)とも呼ばれる。
  • 特に更新の競合がある場合に発生し、トランザクションの一貫性が保ててない状態に陥る。


主な原因

  • 他のトランザクションが並行して対象データを更新し、コミットしている。
  • トランザクションの分離レベルが READ COMMITTED である場合に発生しやすい。
  • スナップショット分離やシリアライズ分離を使用していないケース。


プレイヤーの在庫アイテム数を2回読み取るトランザクションなどがわかりやすい

処理の流れ

  • トランザクションAがプレイヤーの「ポーション在庫数=10」を読み取る。
  • トランザクションBが同じポーションの在庫数を「10 → 7」に更新し、コミット。
  • トランザクションAが再度ポーションの在庫数を読み取ると、値が10から7に変わっている。


この結果、本来であれば、プレイヤーがゲーム内ショップでアイテムを買おうとしているとき、トランザクション全体の間は「在庫数」が変わらず一定であることが保証されるべきであるため、「トランザクション開始時点での在庫数=最初に読んだ値」がトランザクション終了まで変わらないことが望ましい。


ファジーリードされてしまうことで、以下のような挙動になってしまう。

  • 最初の読み取りで在庫数は「10」。
  • 別のトランザクションで他のプレイヤーが3個ポーションを購入し、在庫が「7」に減少してコミット。
  • プレイヤーがトランザクション中にもう一度在庫を読み取ると「7」 に変化している。(!?)
  • そのため、最初の読み取り時点の在庫数「10」とは矛盾した状態で処理が進む。

→なので、特に早いものがちのようなシステムの場合は最初に10個購入しようとしたプレイヤーが優先されないといけないはずが、後から購入処理を行ったプレイヤーの処理が先に完了してしまうことで、公平性を担保できない、つまりはプロダクトとしての信用を損なう問題にまで発展してしまう。

ファントムリード (Phantom Read)

概要

  • 同じ検索条件で複数行のデータを読み取ったとき、他のトランザクションによるINSERT/DELETEの影響で結果セットが変わってしまう現象。
  • 端的に言えば、同じクエリを2回実行したのに、“行の数”や“存在するレコード”が変わってしまうこと。


主な原因

  • 他トランザクションが間に行った INSERT or DELETE により、検索条件にヒットするデータが増減してしまう。
  • 一般的な REPEATABLE READ 分離レベルでは防げず、SERIALIZABLE が必要になる。


PvPバトルマッチングなどがわかりやすい

  • ゲームには 「ランク18以上のプレイヤー」 のみが参加できるPvPマッチング機能がある。
  • トランザクションAはこの条件で現在参加可能なプレイヤー一覧を取得する。


この結果、本来であれば、トランザクションAがクエリ SELECT * FROM players WHERE rank >= 18; を実行した際、トランザクション中に何も変更されなければ、2回目のSELECTの結果も同じ結果になることが望ましい。


ファントムリードされてしまうことで、以下のような挙動になってしまう。

  • トランザクションAが上記のクエリを実行 → PlayerA, PlayerB
  • 同時にトランザクションBが rank=18 の PlayerC を新規登録・コミット
  • トランザクションAが再び同じクエリを実行 → PlayerA, PlayerB, PlayerC
  • 同じ条件でクエリしたのに、2回目のクエリにのみ 幻のプレイヤーC (Phantom) が結果に現れた。

→特に len() などで取得したレコード数などのカウントチェックを行っている場合は、ロジックがあっているにも関わらず、トランザクションの分離レベルが担保できていない故に原因不明のエラーのように見えてしまう。

対策 (カウンターメジャー):トランザクションの分離レベルを適切に設定する必要がある

https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html

要求する整合性 推奨する分離レベル 対策できる分離性の欠如
最低限でよい(高速重視) READ COMMITTED ダーティーリードのみ対策
一貫した読み取りが欲しい REPEATABLE READ ダーティーリード+ファジーリード対策
完全な整合性が必要 SERIALIZABLE すべての問題(+ファントムリード)対策


  1. READ UNCOMMITTED(未コミット読み取り可)
  • すべての問題が起きうる(ダーティーリード、ファジーリード、ファントムリード)
  • 対策できていない(もしくはしないと決めた)状態。


  1. READ COMMITTED(コミット済のみ読む)
  • ダーティーリードは防げる。
  • ファジーリード、ファントムリードは発生し得る。


  1. REPEATABLE READ(繰り返し読み取り可能)
  • ダーティーリード、ファジーリード防止できるためほとんどの読み取り異常を防げる。
  • ファントムリードは発生し得る。


  1. SERIALIZABLE(完全直列化)
  • 全ての分離性の欠如を防止可能。
  • 最も強力なカウンターメジャーだが、同時実行性は大幅に犠牲になる(性能に注意)。


分離レベル ダーティーリード ファジーリード ファントムリード
READ UNCOMMITTED ×
(発生する)
×
(発生する)
×
(発生する)
READ COMMITTED
(防止される)
×
(発生する)
×
(発生する)
REPEATABLE READ
(防止される)

(防止される)

(DBMS依存*)
SERIALIZABLE
(防止される)

(防止される)

(防止される)

△: 防止できない場合もある

tamaco489tamaco489

マイクロサービスアーキテクチャでビジネスロジックを構成するための手法

1. Transaction Script(トランザクションスクリプト)パターン

概要

  • ビジネスロジックを手続き的にまとめたスクリプトとして実装する手法。
  • 入力を受け取り → バリデーション → データアクセス → 処理 → 出力、という流れを1つの関数またはメソッド内に記述。
  • 小規模・単純な処理向け。

特徴

  • コードの構造がシンプルで理解しやすい。
  • ビジネスルールは特定のメソッドやサービス関数に集約される。
  • 設計よりも実装の即効性を優先するケースで有効。

メリット

  • 実装・学習コストが低い。
  • 単純なユースケースでは開発スピードが速い。
  • トランザクション単位でスクリプト化するので処理の流れが明快。

デメリット

  • ロジックの重複が発生しやすい(似た処理を別スクリプトにコピペ)。
  • 複雑化すると保守性が低下(修正箇所が分散)。
  • ドメインの概念が明確にモデル化されないため、ビジネス知識の共有が難しい。

マイクロサービスでの適用例

  • サービスの責務が小さく、単純なCRUD処理中心のマイクロサービス。
  • 例:ログ収集サービス、シンプルなマスターデータ管理APIなど。
2. Domain Model(ドメインモデル)パターン

概要

  • ビジネス領域(ドメイン)の概念やルールをクラスやオブジェクトとしてモデル化し、ロジックをそのモデル内部に閉じ込める手法。
  • DDD(ドメイン駆動設計)で推奨されるアプローチ。
  • モデル間の関係や不変条件(Invariants)をオブジェクト設計で表現。

特徴

  • ビジネスロジックがドメインオブジェクト内部に存在する。
  • 状態(フィールド)と振る舞い(メソッド)を同じクラスに持たせる。
  • データ構造とビジネスルールが一貫して管理される。

メリット

  • ビジネスロジックの再利用性が高い。
  • ドメイン知識をコード上で明示的に表現できる。
  • 複雑な業務ルールや状態遷移を安全に管理しやすい。

デメリット

  • 設計・実装の初期コストが高い。
  • 過剰設計になりやすい(単純なケースでは不要に複雑化)。
  • ドメイン理解とチームのスキルが必要。

マイクロサービスでの適用例

  • ビジネスロジックが複雑で、状態管理や不変条件が多いサービス。
  • 例:決済サービス、在庫管理サービス、予約システムなど。
3. Transaction Script vs Domain Model(比較表)
項目 Transaction Script Domain Model
構造 手続き型 オブジェクト指向
適用領域 単純・短命なロジック 複雑・長命なロジック
実装速度 速い やや遅い
保守性 複雑化で低下 構造化されやすく高い
再利用性 低い 高い
チームスキル要求 低い 高い

マイクロサービスアーキテクチャにおけるビジネスロジックの設計

マイクロサービスアーキテクチャにおけるアグリゲートの整理

背景課題

マイクロサービスでは、ドメインロジックが複数の独立したサービスに分散するため、以下の課題が発生する。

[1. モデルの分散による結合度の高さ]

  • 多くのドメインモデルは相互に依存・参照し合う。
  • クラスやエンティティがサービス境界を越えて直接参照されると、境界の独立性や疎結合性が崩れる。
  • 特にトランザクションが絡む場合、直接参照はデータ整合性や障害切り分けの面でリスク。

[2. 分散トランザクションの制約]

  • 単一サービス内ではACIDトランザクションを利用可能。
  • 複数サービスにまたがる場合、2PC(Two-Phase Commit)はスケーラビリティや可用性の観点から避けられる。
  • 代替としてSagaパターンなどの最終的整合性を前提とした設計が必要。
アグリゲート(Aggregate)の役割

DDDのAggregateは、一貫性の境界(Consistency Boundary) として機能する。

[1. 定義]

  • 関連するエンティティと値オブジェクトのグループ。
  • 集約の内部は不変条件(Invariants)を保つ。
  • 外部からは集約ルート (Aggregate Root) を通してのみアクセスできる。

[2. マイクロサービスでの適用理由(これを意識すると何が良いのか)]

  • 2-1. 境界越えの参照削減

    • 他のアグリゲートを直接オブジェクト参照しない。※例: AサービスがBサービスのクラス構造やフィールドに依存してしまうと、Bサービス側の仕様変更等がAサービスに影響してしまうため
    • 外部アグリゲートはID(主キー)などの識別子で保持することで、サービス間の強結合を回避する。つまり、他サービスの内部構造(クラス定義やDBスキーマ等)を知らなくても動かるようになることで、独立したサービスにしやすい。※リアルタイム同期からイベント駆動といった結果整合性モデルへの移行をしやすくなる
  • 2-2. トランザクション境界の明確化

    • トランザクションは1アグリゲート単位で完結させる。
    • マイクロサービスの「1リクエスト=1アグリゲート更新」というモデルにフィットする。
設計のポイント
  • 集約ルートを小さく保つ
    • 不変条件を守るために必要な最小単位に限定する。
    • 大きすぎる集約は更新競合の温床になる。※コンカレンシー問題
  • 外部とのやり取りはIDで行う
    • サービス境界を越えてドメインオブジェクトを共有しない。
  • トランザクションのスコープを1アグリゲートに限定する
    • 複数アグリゲートに跨る場合はSagaやイベント駆動で調整。
まとめ

マイクロサービスアーキテクチャでアグリゲートを活用することで以下を実現できるようになり、結果として分散システム特有の複雑さを抑制できる。

  • 疎結合化(直接参照を減らす)
  • トランザクションの明確化(境界を意識した更新)
  • 整合性の局所化(サービス内での強整合性、サービス間では結果整合性)
ほか参考にさせていただいた記事

DDD の Aggregate パターンを使ったモデル設計

この文脈での Aggregate について

Aggregate とは

  • ドメインモデルの中で一貫性境界(Consistency Boundary) を持つオブジェクト群
  • 1つのルートエンティティ(Aggregate Root) を通じてのみ外部とやりとりする集合
  • 内部のエンティティや値オブジェクトは Aggregate Root が管理し、外部から直接触れられない

  • 注文 (Order) を Aggregate Root とし、その中に 注文アイテム (OrderItem) や 配送情報 (ShippingInfo) を内包する
  • 外部のサービスやモジュールは Order を経由してしか注文アイテムを操作できない

ポイント

  • 不変条件(ビジネスルール)を守る単位が Aggregate
  • 1トランザクションは1 Aggregate に閉じるのが原則
マイクロサービスアーキテクチャに適合する理由
  • マイクロサービスではサービス境界を越えたトランザクションやオブジェクト参照が難しいが、これらの問題は Aggregate を上手く活用することで対処できる。
  • アグリゲートが全体の不変条件を徹底できる自己完結型のユニットになるには、以下のルールを意識しておくことが重要

2-1. サービス間の強結合を防ぐ

  • Aggregate は外部参照を直接のオブジェクト参照ではなく ID(識別子) に限定
  • これにより、サービス間で複雑なオブジェクト依存が発生せず、独立デプロイが容易
  • 例: Order AggregateCustomer を参照する場合、Customer オブジェクトそのものではなく customerId だけを保持する

2-2. トランザクション境界が明確

  • 原則として1トランザクション = 1 Aggregate
  • マイクロサービスは ACID トランザクションをサービス内部に限定し、サービス間では Saga パターンやイベント駆動で整合性を保つ必要がある
  • Aggregate 単位での更新は、この制約にフィットしやすい

2-3. 不変条件をローカルで担保

  • Aggregate 内の整合性(例:注文金額 = 各アイテム金額の合計)は、単一トランザクションで安全に更新可能
  • 他サービスのデータに依存せずに整合性を確保できる

ドメインイベントのパブリッシュについて

前提: DDD の文脈におけるドメインイベントとは、アグリゲートに起きた何かを意味する。

1. なぜ変更イベントをパブリッシュするのか?
  • システム内の状態変化を外部に通知し、他のコンポーネントやサービスに伝搬させるため。
  • マイクロサービス間の連携や内部の非同期処理を実現しやすくなる。
  • 変更が起きた事実をイベントとして発行することで、他の関心事(例:ログ記録、通知送信、集計処理)を疎結合に実装可能。
  • トランザクション境界を越えた整合性管理(Sagaパターンなど)をサポート。
2. ドメインイベントとは何か?
  • ドメイン内で重要な事象(ビジネス上の意味を持つ出来事)を表すオブジェクト。
  • 例:OrderPlaced(注文が確定した)、PaymentReceived(支払い完了)、ItemShipped(商品発送済み)
  • ドメインモデルの状態変更を明確に表現し、システムの反応や他サービス連携のトリガーとなる。
  • イベントは過去形(過去に起きたこと)で命名するのが慣例。
3. イベントのエンリッチメント
  • 発行するイベントに、必要なコンテキスト情報(顧客ID、注文ID、タイムスタンプなど)を付加すること。
  • エンリッチメントにより、イベント受信側は追加の問い合わせを減らし、効率的に処理可能になる。
  • ただしイベントは軽量に保ち、不要な情報を詰め込み過ぎないバランスも重要。
4. ドメインイベントの見つけ方
  • (1) イベントのブレーンストーミング

    • ドメインの専門家やチームで議論し、ビジネス上重要な状態変化やアクションを洗い出す。
  • (2) イベントトリガーの特定

    • どの操作・状態変化がイベントとして表現されるべきかを判断。
    • 例:注文確定時、支払い完了時、キャンセル時など。
  • (3) アグリゲートの特定

    • イベントがどのアグリゲートの変更に伴うものかを明確化。
    • アグリゲートはイベントの発生源であり、一貫性の境界でもある。
5. ドメインイベントの生成、パブリッシュ
  • (1) ドメインイベントの生成とは

    • アグリゲート内部で状態変更があったタイミングで、イベントオブジェクトを作成。
    • 変更処理の副産物としてイベントを生成し、変更内容を外部に通知する意図を示す。
  • (2) ドメインイベントを信頼できるかたちでパブリッシュする方法

    • イベントの発行はトランザクション内で行うことが理想(例:DB更新とイベント保存を一括処理)。
    • アウトボックスパターンなどを使い、DBとイベントストアの整合性を保つ。
    • イベントはメッセージブローカー(Kafka、RabbitMQ、AWS SNSなど)へ送信し、他サービスやコンポーネントへ通知。
6. ドメインイベントの消費について
  • 消費者(いわゆるConsumer) はイベントを受け取り、自身のドメインロジックや処理を実行する。
  • Consumerの実行は非同期で行うことが多く、受信側の独立性を保つ。
  • 例:注文サービスのOrderPlacedイベントを受けて在庫サービスが在庫を引き当てる。
  • Consumer側でイベントの再試行や冪等性(同一イベントを複数回受けても影響がない処理)を考慮する必要がある。