Outboxパターンは、分散システムにおけるデータベース操作とメッセージング(イベント発行)の整合性を保証するための設計パターンです。
課題
例えば以下のようなJavaアプリの処理があったとします。
@Transactional
public void createOrder(Order order) {
orderRepository.save(order); // DBに保存
eventPublisher.publish(new OrderCreatedEvent(order)); // イベント発行
}
この実装の問題点は、DBトランザクションorイベント発行のどちらかが失敗した場合にデータの整合性が取れなくなる恐れがある点です。
- DB成功・イベント失敗
- DBにはデータがあるが、イベントは失敗しているので期待する状態になっていない
- DB失敗・イベント成功
-
@TransactionalアノテーションによりDBトランザクションはロールバックされるが、イベント発行処理はロールバックされない。 - DBにデータがないが、イベント側は処理が走ってしまう。
-
Outboxパターンによる解決法
Outboxテーブルという新しいテーブルを同じDBに作成します。そして先ほどの処理を以下のように修正します。
CREATE TABLE outbox (
id UUID PRIMARY KEY,
event_type VARCHAR,
payload JSONB,
status VARCHAR DEFAULT 'pending',
retry_count INT DEFAULT 0,
created_at TIMESTAMP,
processed_at TIMESTAMP
);
@Transactional
public void createOrder(Order order) {
orderRepository.save(order); // 注文データを保存
// Orderから OrderCreatedEvent を生成
OrderCreatedEvent event = new OrderCreatedEvent(order);
outboxRepository.save(event); // イベントをOutboxテーブルに保存
}
アプリケーション側の処理はこれで終了です。
イベント発行処理は別のプロセスに委譲します。
例えばAWS Lambdaなどで、Outboxテーブルをポーリングしてイベント発行する処理を書きます。
def process_outbox(self):
"""未処理イベントを取得して発行"""
events = self.outbox_repository.find_unprocessed()
for event in events:
try:
# イベント発行
self.event_publisher.publish(event)
# Outboxテーブルを更新
self.outbox_repository.mark_as_processed(event.id)
logger.info(f"Processed event: {event.id}")
except Exception as e:
# リトライ回数をインクリメント
self.outbox_repository.increment_retry_count(event.id)
# 上限を超えた場合はステータスを失敗に更新
if event.retry_count >= MAX_RETRY:
self.outbox_repository.mark_as_failed(event.id)
# アラート通知など
self.alert_service.notify_failed_event(event.id)
logger.error(f"Failed to process event {event.id}: {e}")
このLambdaをEventbridgeなどで定期実行することでOutboxテーブルをポーリングすることができます。
以上の流れをシーケンス図に起こします。
Outboxパターンのポイント
- イベント発行処理をワーカー側(Lambda)に委譲できる
- Javaアプリ側はOutboxテーブルへのINSERTまで担保できていればOK。単純に2つのテーブルへのコミットを1つのトランザクションで行うだけなので、特に複雑な実装は不要。
- イベントの発行自体はワーカーが責任を持つ
- 上限回数までリトライしてもイベントに失敗する場合は、別途そのことを通知する仕組みをワーカー側もしくはCloudWatchなどのアラートで通知する必要がある。
- イベントは非同期処理で行う前提
- イベントが正常終了した後に別の処理を行う、といった要件がある場合はOutboxパターンは適していない。
- その場合は同期処理を使った別の実装が必要
注意点
-
べき等性の保証が必要
- ワーカーがイベント発行後、Outboxテーブルの更新前に失敗すると、同じイベントが複数回発行される可能性があります
- サブスクライバー側で、同じイベントを複数回受け取っても問題ないように実装する必要があります
- 例:イベントIDを記録して、既に処理済みのイベントはスキップするなど
参考