0Pricing
PHP Academy · レッスン

イベント駆動ワークフローの構築

イベントと冪等性を通じてサービスを連携させます。

「イベント駆動ワークフローの構築」は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フィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. 非同期メッセージングが必要な理由
  2. PHPでRabbitMQを扱う
  3. PHPでApache Kafkaを扱う
  4. イベント駆動ワークフローの構築
← PHP Academyに戻る