0Pricing
PHP Academy · Lekcja

Tworzenie przepływów pracy sterowanych zdarzeniami

Koordynowanie usług za pomocą zdarzeń i idempotentności

Tworzenie przepływów pracy sterowanych zdarzeniami to bezpłatna lekcja PHP Academy na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej PHP Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs PHP Academy zawiera 4 lekcji w sumie.

Przepływy pracy sterowane zdarzeniami

Pojedyncza wiadomość jest prosta. Przepływ pracy — „złożenie zamówienia → zarezerwowanie towaru → obciążenie karty → wysyłka → powiadomienie”, obejmujący kilka usług — to obszar, w którym projektowanie sterowane zdarzeniami pokazuje swoją wartość, ale też przysparza problemów, jeśli zostanie zastosowane naiwnie.

W tej lekcji omówiono choreografię i orkiestrację, wzorzec outbox do atomowego publikowania, sagi do wycofywania zmian rozproszonych oraz idempotencję, która spaja te mechanizmy.

Choreografia a orkiestracja

Istnieją dwa sposoby koordynowania przepływów wieloetapowych:

  • Choreografia — każda usługa reaguje na zdarzenia i emituje własne; nie ma centralnego koordynatora. Usługi są luźno powiązane, ale ogólny przebieg jest niejawny i trudny do śledzenia.
  • Orkiestracja — centralny koordynator mówi każdej usłudze, co ma zrobić dalej. Przebieg jest jawny i możliwy do monitorowania, ale orkiestrator stanowi punkt sprzężenia.

Praktyczna zasada: choreografię należy stosować przy prostym fan-out, a orkiestrację wtedy, gdy przepływ ma wiele uporządkowanych kroków i wymaga widocznego stanu.

Zdarzenia a polecenia

Nazwy wiadomości należy wybierać świadomie:

  • Zdarzenie przedstawia fakt z przeszłości: OrderPlaced. Wydawca nie musi wiedzieć, kto go nasłuchuje.
  • Polecenie żąda wykonania przyszłej operacji przez konkretny handler: ChargeCard.

Zdarzenia napędzają choreografię, a polecenia — orkiestrację. Mieszanie terminologii (zdarzenie, które w rzeczywistości oczekuje jednego handlera) jest częstym źródłem ukrytego sprzężenia.

<?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";

Problem podwójnego zapisu

Klasyczny błąd: handler aktualizuje bazę danych i publikuje wiadomość jako dwie osobne operacje. Jeśli proces zakończy się między nimi, powstaje niespójność — wiersz został zmieniony, ale nie wysłano zdarzenia, albo stało się odwrotnie.

<?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));
}

Transakcyjny outbox

Rozwiązaniem jest wzorzec outbox: w ramach tej samej transakcji DB, która zmienia dane, należy wstawić zdarzenie do tabeli outbox. Osobny proces przekaźnikowy odczytuje nieopublikowane wiersze i wysyła je do brokera. Jeden atomowy commit, bez podwójnego zapisu.

<?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();
}

Przekaźnik (publisher)

Worker odpytuje outbox (albo śledzi dziennik zmian w DB za pośrednictwem CDC), publikuje każdy wiersz, a następnie oznacza go jako wysłany. Ponieważ przekaźnik może zakończyć działanie po publikacji, ale przed oznaczeniem wiersza, sam działa w modelu co najmniej raz — i jest to w porządku, ponieważ konsumenci są idempotentni.

<?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']]);
    }
}

Idempotentni konsumenci (ponownie)

Ponieważ zarówno przekaźnik, jak i broker działają w modelu co najmniej raz, handlery dalszych etapów zobaczą duplikaty. Każdy konsument zapisuje identyfikator przetworzonej wiadomości i pomija powtórzenia — jest to ta sama bramka deduplikacji, której użyto wcześniej, teraz stosowana osobno w każdej usłudze.

<?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 {}

Sagi: rozproszony rollback

Nie można otworzyć jednej transakcji ACID obejmującej wiele usług. Saga modeluje długotrwały przepływ jako sekwencję lokalnych transakcji, z których każda ma akcję kompensującą, która ją cofa. Jeśli krok 3 się nie powiedzie, należy uruchomić akcje kompensujące dla kroków 2 i 1 w odwrotnej kolejności.

Przykład: płatność kończy się niepowodzeniem po zarezerwowaniu towaru → należy wysłać ReleaseStock, aby wykonać kompensację. Nie ma automatycznego wycofania zmian — dla każdego kroku trzeba zaprojektować operację cofającą.

Saga orkiestracyjna

Orkiestrator prowadzi sagę: po sukcesie przechodzi do kolejnego kroku, a po błędzie uruchamia akcje kompensujące. Stan sagi należy zapisywać, aby po awarii można było wznowić jej działanie.

<?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;
}

Limity czasu w długich przepływach

Krok sagi może po prostu nigdy nie odesłać odpowiedzi — usługa płatności może być niedostępna albo człowiek może nigdy nie wydać zgody. Bez limitu czasu saga zawiesi się na zawsze, blokując rezerwacje. Należy zapisać termin dla każdego kroku; harmonogram skanuje przeterminowane sagi i uruchamia ścieżkę obsługi błędu lub kompensacji.

<?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
    }
}

Wersjonowanie i obserwowalność

Przepływy pracy działają przez lata; zdarzenia muszą ewoluować bezpiecznie:

  • Dodawać do każdego zdarzenia version (lub schemat); odbiorcy muszą tolerować nieznane nowe pola i nigdy nie zakładać obecności konkretnego pola.
  • Preferować zmiany addytywne; nigdy nie zmieniać znaczenia istniejącego pola.
  • Przekazywać identyfikator korelacji w każdej wiadomości, aby można było śledzić jedną transakcję biznesową we wszystkich usługach za pomocą logów i śledzenia.

Bez identyfikatorów korelacji debugowanie przepływu sterowanego choreografią, który przebiega przez pięć usług, jest niemal niemożliwe.

<?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";

Szybki test

Unikanie problemu podwójnego zapisu.

Podsumowanie

Mogą Państwo teraz projektować niezawodne przepływy pracy sterowane zdarzeniami:

  • Dla każdego przepływu wybierać choreografię (zdarzenia) lub orkiestrację (polecenia), zależnie od jego złożoności.
  • Rozwiązywać problem podwójnego zapisu za pomocą transakcyjnego outboxa i przekaźnika.
  • Zapewniać idempotentność każdego odbiorcy za pomocą skrzynki inbox lub klucza deduplikacji.
  • Używać sag z działaniami kompensującymi do wycofywania zmian w systemie rozproszonym.
  • Wersjonować zdarzenia przez dodawanie pól i przekazywać identyfikator korelacji, aby zapewnić możliwość śledzenia.

Wzorce te przekształcają luźne komunikaty w niezawodne i obserwowalne procesy biznesowe.

Często zadawane pytania

Czy lekcja „Tworzenie przepływów pracy sterowanych zdarzeniami” jest bezpłatna?

Tak — pełny tekst „Tworzenie przepływów pracy sterowanych zdarzeniami” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu PHP Academy, przejdź na CoddyKit PRO. Kurs PHP Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Tworzenie przepływów pracy sterowanych zdarzeniami”?

Koordynowanie usług za pomocą zdarzeń i idempotentności Ćwiczysz PHP Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć PHP Academy?

Nie wymagamy żadnego doświadczenia. PHP Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Tworzenie przepływów pracy sterowanych zdarzeniami”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji PHP Academy?

Tak. Każda lekcja PHP Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Dlaczego warto używać komunikacji asynchronicznej
  2. Praca z RabbitMQ w PHP
  3. Apache Kafka z PHP
  4. Tworzenie przepływów pracy sterowanych zdarzeniami
← Powrót do PHP Academy