前置き
マイクロサービスでは、アウトボックスパターンという手法が出てきます。
今回は、この各種マイクロサービスが、コマンドモデル内部に持つべき、
・自身の集約内部の状態変化
・送信するドメインイベント
の関係性について、詳しく見ていきます。
集約の状態変化とアウトボックス
まず、イベントストアとアウトボックステーブルは、同じデータベースに配置され、単一のアトミックなトランザクションで書き込まれます。
なぜ同じデータベースである必要があるのか
その理由は、データベースが提供するACIDトランザクションの保証を最大限に活用するためです。
標準的なデータベースのトランザクションは、単一のデータベース内でのみ、
操作が全て成功するか、全て失敗するか(原子性: Atomicity)
を保証できます。
もし、イベントストアとアウトボックステーブルが別々のデータベースにあると、
①. イベントストアへの書き込みには成功
②. しかし、アウトボックスDBへの書き込みには失敗
という最悪の不整合状態が発生する可能性があります。
データベース内部での論理的な分離
同じコマンドモデルのデータベース内に、以下のような2つのテーブルが共存するイメージです。
events テーブル
イベントストア。集約の状態変化という、ドメインの関心事を記録します。
outbox テーブル
これから発行すべきメッセージのキュー。
メッセージを外部に確実に届けるという、技術的な関心事を扱います。
アプリケーションは、これら2つのテーブルへの書き込みを、単一のトランザクションで実行します。
概念図
この構成により、
「ビジネス上の状態変化が記録されること」
「その変更を外部に通知するメッセージが発行キューに入ること」
が、絶対に矛盾なく行われることが保証されるのです。
なぜ状態変化とアウトボックスを論理的に分けるのか?
上記で、
集約内の状態変化 と アウトボックス な論理スキーマで分ける
という風に説明した部分の理由に迫ります。
これは、データベース設計における関心の分離を徹底するためのアプローチです。
なぜスキーマを分離するのが良いのか
eventsテーブルとoutboxテーブルは、同じトランザクションで書き込まれますが、その目的と性質は全く異なります。
集約の状態変化用スキーマ (例: domain_schema)
目的
eventsテーブルなどを格納。ビジネスの核となるドメインデータであり、システムの
「信頼できる唯一の情報源」 です。
性質
長期間保持され、監査やビジネス分析の対象となります。
アウトボックス用スキーマ (例: infra_schema)
目的
outboxテーブルを格納。
メッセージを外部に確実に届けるという技術的なインフラの一部です。
性質
メッセージが転送された後は不要になる、比較的短命なデータです。
これらを論理的なスキーマで分離することで、以下のメリットが生まれます。
①. 関心の分離
データベースの構造を見るだけで、どれがビジネスデータで、どれがインフラのための仕組みかが一目瞭然になります。
②. セキュリティと権限管理
スキーマ単位でアクセス権限を細かく設定できます。
例えば、メッセージリレーのプロセスにはinfra_schema.outboxへの読み取り・更新権限だけを与え、domain_schemaへのアクセスは制限する、といったことが可能です。
トランザクションの原子性について
ここで最も重要な点は、
スキーマが異なっていても、それらが同じデータベース内にある限り、単一のアトミックなトランザクションで両方のスキーマのテーブルに書き込むことができる
ということです。
BEGIN;
-- ドメインスキーマのテーブルに書き込み
INSERT INTO domain_schema.events (event_id, data) VALUES (...);
-- インフラスキーマのテーブルに書き込み
INSERT INTO infra_schema.outbox (event_id, destination) VALUES (...);
COMMIT;
このトランザクションは、データベースのACID特性によって原子性が保証されます。
これにより、論理的な関心の分離と、物理的なデータの一貫性を両立させることができるのです。
複数のアトミックトランザクションがある場合
上記の概念図では、単一のアトミックなトランザクションがコマンドモデルにある時の図を表現していました。
では、1つのコマンドモデルに複数のアトミックトランザクションが存在していたらどうでしょうか?
ちょうど下図のように、同じ色のものがアトミック性を満たすものです。
この図では、1つのコマンドモデルに
2つのアトミックなトランザクションが存在していることになります。
片方のトランザクションと、もう片方のトランザクションが干渉し合うようなリスクがあるって思ってしまいませんか? でしゅが、その心配はありません。
I (Isolation) の役割
データベースにおけるIsolationとは、
あるトランザクションが実行中の中途半端な状態を、他のトランザクションから見えなくする
仕組みです。
各トランザクションは、まるで自分だけがそのデータベースを使っているかのように振る舞うことができます。
例え話:個室での作業
2人の作業者(トランザクションA、B)が、同じ書類棚(データベース)で作業をするとします。
Isolationがない場合
Aさんが書類を取り出して修正している途中で、Bさんがその書きかけの書類を見てしまい、間違った情報で自分の作業をしてしまう可能性があります。
Isolationがある場合
AさんとBさんは、それぞれ鍵のかかった別々の個室で作業をします。
Aさんは自分の作業を完全に終えて書類棚に戻すまで(コミットするまで)、BさんはAさんが作業中であることさえ知ることができません。
どのように実現されているか
この独立性は、データベース管理システム(DBMS)が内部的に持つ、主に2つの仕組みによって実現されています。
ロック (Locking)
あるトランザクションがデータにアクセスする際、そのデータ(行やテーブル)に「鍵(ロック)」をかけ、他のトランザクションが同時にアクセスできないようにします。
多版型同時実行制御 (MVCC: Multi-Version Concurrency Control)
PostgreSQLやOracleなどで採用されている、より高度な仕組みです。
データを更新する際に、元のデータを上書きするのではなく、新しい「バージョン」のデータを作成します。
各トランザクションは、自分が始まった時点での一貫したデータバージョン(スナップショット)を参照するため、他のトランザクションの途中経過が見えてしまうことがありません。
このI (Isolation) という強力な保証があるからこそ、私たちはアプリケーション開発において、複雑な同時実行制御を意識することなく、安心して複数のトランザクションを同時に実行できるのです。