イベント駆動ワークフローの構築
イベントと冪等性を通じてサービスを連携させます。
「イベント駆動ワークフローの構築」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。
イベント駆動ワークフロー
単一のメッセージは簡単です。しかし、複数のサービスにまたがるワークフロー — 「注文受付 → 在庫確保 → カード決済 → 発送 → 通知」 — でこそイベント駆動設計の価値が発揮されます。ただし、安易に実装すると問題も生じます。
このレッスンでは、コレオグラフィーとオーケストレーションの違い、公開をアトミックに行うためのoutboxパターン、分散ロールバックのためのsaga、そしてこれらすべてを支える冪等性について説明します。
コレオグラフィーとオーケストレーション
複数ステップのフローを調整する方法は2つあります。
- コレオグラフィー — 各サービスがイベントに反応し、それぞれのイベントを発行します。中央の司令塔はありません。疎結合である一方、全体のフローが暗黙的になり、追跡が困難です。
- オーケストレーション — 中央のコーディネーターが各サービスに次の処理を指示します。明示的で観測しやすい一方、オーケストレーターが結合点になります。
目安として、単純なファンアウトにはコレオグラフィーを、多くの順序付きステップがあり状態を可視化する必要があるフローにはオーケストレーションを使用します。
イベントとコマンド
メッセージの名前は意図を明確にして付けます。
- イベントは、過去に起きた事実を表します。例:
OrderPlaced。発行元は誰がリッスンするかを気にしません。 - コマンドは、特定のハンドラーに将来のアクションを要求します。例:
ChargeCard。
イベントはコレオグラフィーを動かし、コマンドはオーケストレーションを動かします。用語を混同すると、たとえば実際には1つのハンドラーを暗黙に期待する「イベント」を作ると、隠れた結合の原因になります。
<?php
final class OrderPlaced {
public function __construct(
public readonly string $orderId,
public readonly string $customerId,
public readonly int $amountCents,
public readonly string $occurredAt,
) {}
}
$e = new OrderPlaced('o-42', 'c-7', 1990, gmdate('c'));
echo json_encode($e), "\n";デュアルライト問題
典型的なバグは、ハンドラーがデータベースの更新とメッセージの公開を、2つの別々の操作として行うことです。その間にプロセスが停止すると不整合が発生します。行は変更されたのにイベントが送信されない、またはその逆の状態になります。
<?php
// BROKEN: not atomic. A crash between the two lines corrupts state.
function placeOrder(PDO $db, $broker, array $o): void {
$db->prepare('INSERT INTO orders ...')->execute($o);
// <-- crash here = row exists but no event ever published
$broker->publish('OrderPlaced', json_encode($o));
}トランザクショナルOutbox
解決策はoutboxパターンです。データを変更するDBトランザクションと同じトランザクション内で、イベントをoutboxテーブルに挿入します。別のリレープロセスが未公開の行を読み取り、ブローカーへ送信します。1回のアトミックなコミットで処理できるため、デュアルライトは発生しません。
<?php
function placeOrder(PDO $db, array $o): void {
$db->beginTransaction();
$db->prepare('INSERT INTO orders (id, total) VALUES (?, ?)')
->execute([$o['id'], $o['total']]);
// Same transaction -> atomic with the business write
$db->prepare('INSERT INTO outbox (id, type, payload) VALUES (?, ?, ?)')
->execute([bin2hex(random_bytes(8)), 'OrderPlaced', json_encode($o)]);
$db->commit();
}リレー(パブリッシャー)
ワーカーはoutboxをポーリングするか、CDCによってDBの変更ログを追跡し、各行を公開してから送信済みとしてマークします。リレーは公開後、送信済みとしてマークする前にクラッシュする可能性があるため、リレー自体も少なくとも1回の配信になります。しかし、コンシューマーが冪等であれば問題ありません。
<?php
function relayOutbox(PDO $db, $broker): void {
$rows = $db->query(
'SELECT id, type, payload FROM outbox
WHERE published_at IS NULL ORDER BY created_at LIMIT 100'
)->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) {
$broker->publish($r['type'], $r['payload'], messageId: $r['id']);
$db->prepare('UPDATE outbox SET published_at = now() WHERE id = ?')
->execute([$r['id']]);
}
}冪等なコンシューマー(再び)
リレーとブローカーのどちらも少なくとも1回の配信であるため、下流のハンドラーには重複が届きます。各コンシューマーは処理済みのメッセージIDを記録し、重複を早期に終了させます。これは以前に学んだ重複排除ゲートを、サービスごとに適用したものです。
<?php
function onOrderPlaced(PDO $db, string $messageId, array $data): void {
$db->beginTransaction();
try {
$db->prepare('INSERT INTO inbox (message_id) VALUES (?)')
->execute([$messageId]); // unique index = dedup
} catch (PDOException $e) {
$db->rollBack();
return; // already handled this message
}
reserveStock($data['orderId']);
$db->commit();
}
function reserveStock(string $id): void {}Saga:分散ロールバック
サービス間に1つのACIDトランザクションを開くことはできません。sagaでは、長時間実行されるフローをローカルトランザクションの連続としてモデル化し、それぞれに処理を取り消す補償アクションを用意します。ステップ3が失敗した場合は、ステップ2とステップ1の補償処理を逆順に実行します。
たとえば、在庫を確保した後に決済が失敗した場合は、補償処理としてReleaseStockを発行します。自動的なロールバックはありません。すべてのステップについて、取り消し処理を設計する必要があります。
オーケストレーションされたSaga
オーケストレーターがsagaを進行させます。成功時には次へ進み、失敗時には補償処理をディスパッチします。クラッシュ後に再開できるよう、sagaの状態を永続化してください。
<?php
function handleStepResult(array $saga, string $step, bool $ok, $bus): array {
if ($ok) {
$next = ['reserveStock' => 'chargeCard', 'chargeCard' => 'ship'][$step] ?? null;
if ($next) { $bus->send($next, $saga['orderId']); $saga['state'] = $next; }
else { $saga['state'] = 'completed'; }
} else {
// Run compensations in reverse for whatever already succeeded
foreach (array_reverse($saga['done']) as $s) {
$bus->send('compensate.' . $s, $saga['orderId']);
}
$saga['state'] = 'compensating';
}
return $saga;
}長時間フローのタイムアウト
sagaのステップは、単に応答を返さないことがあります。決済サービスが停止していたり、人による承認が返ってこなかったりする場合です。タイムアウトがなければ、予約を保持したままsagaが永久に停止します。ステップごとに期限を永続化し、スケジューラーで期限超過したsagaを検索して、失敗または補償処理のパスを実行します。
<?php
function reapTimedOutSagas(PDO $db, $bus): void {
$rows = $db->query(
"SELECT order_id, state FROM sagas
WHERE state NOT IN ('completed','compensating')
AND deadline_at < now()"
)->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) {
echo "Saga {$r['order_id']} timed out at step {$r['state']}\n";
$bus->send('saga.compensate', $r['order_id']); // trigger rollback
}
}バージョニングと可観測性
ワークフローは何年にもわたって存続するため、イベントは安全に進化させる必要があります。
- すべてのイベントに
version(またはスキーマ)を追加し、コンシューマーは未知の新しいフィールドを許容するとともに、フィールドの存在を決めつけないようにします。 - 追加的な変更を優先し、既存のフィールドの意味を決して別の用途に転用しないでください。
- すべてのメッセージに相関IDを引き継がせ、ログやトレーシングで1つの業務トランザクションをすべてのサービスにわたって追跡できるようにします。
相関IDがなければ、5つのサービスをまたぐコレオグラフィー形式のフローのデバッグはほぼ不可能です。
<?php
$envelope = [
'type' => 'OrderPlaced',
'version' => 2,
'correlationId' => $incoming['correlationId'] ?? bin2hex(random_bytes(8)),
'occurredAt' => gmdate('c'),
'data' => ['orderId' => 'o-42'],
];
echo json_encode($envelope, JSON_PRETTY_PRINT), "\n";クイックチェック
二重書き込み問題を回避します。
まとめ
これで、信頼性の高いイベント駆動型ワークフローを設計できるようになりました。
- フローの複雑さに応じて、コレオグラフィー(イベント)またはオーケストレーション(コマンド)を選択します。
- トランザクショナルアウトボックスとリレーを組み合わせて、二重書き込みを解決します。
- inboxや重複排除キーを使い、すべてのコンシューマーを冪等にします。
- 分散ロールバックには、補償アクションを伴うSagaを使用します。
- イベントはバージョン管理を追加的に行い、追跡可能性のために相関IDを引き継がせます。
これらのパターンにより、疎結合なメッセージを、信頼性と可観測性を備えた業務プロセスへと変えられます。
よくある質問
「イベント駆動ワークフローの構築」レッスンは無料ですか?
はい。「イベント駆動ワークフローの構築」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。
「イベント駆動ワークフローの構築」で何を学びますか?
イベントと冪等性を通じてサービスを連携させます。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
PHP Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。
「イベント駆動ワークフローの構築」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このPHP Academyレッスンでコードを書いて実行できますか?
はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- 非同期メッセージングが必要な理由
- PHPでRabbitMQを扱う
- PHPでApache Kafkaを扱う
- イベント駆動ワークフローの構築