Razvoj

PHP Transactions: The Outbox Pattern for Reliable Inter-Service Communication

PHP transakcije: obrazac Outbox za pouzdanu komunikaciju među servisima

Većina kvarova između servisa nisu dramatični prekidi rada. To su tihe praznine: narudžba je spremljena, ali se događaj naplate nikada ne pošalje; e-poruka se nakon ponovnog pokušaja stavi u red čekanja dvaput; radni proces se sruši između objavljivanja poruke i bilježenja da je to učinio.

Temeljni je problem jednostavan. Transakcija baze podataka i posrednik poruka zasebni su sustavi. Ako aplikacijski kôd piše u jedan pa zatim u drugi, između tih operacija uvijek postoji prozor za kvar. Obrazac outbox zatvara taj prozor tako što poslovnu promjenu i namjeru objavljivanja događaja čini dijelom iste transakcije baze podataka.

Zašto je „spremi, pa objavi” nepouzdano

Tipičan servis počinje kôdom koji izgleda razumno: stvori narudžbu, potvrdi je, zatim objavi OrderCreated. To funkcionira sve dok proces ne izgubi vezu, ne bude prekinut ili posrednik privremeno nije nedostupan nakon potvrde u bazi podataka.

Obrtanje redoslijeda ne rješava problem. Najprije objavljivanje može izložiti događaj za narudžbu čije se spremanje kasnije ne potvrdi. Ponovni pokušaj bilo kojeg redoslijeda uvodi duplikate, osim ako svaki potrošač nije spreman na njih.

Obrazac outbox mijenja ugovor. Umjesto izravnog objavljivanja unutar zahtjeva, servis zapisuje zapis događaja u vlastitu bazu podataka uz poslovne podatke. Zaseban proces prijenosa kasnije objavljuje zapise na čekanju.

Transakcijski outbox u praksi

Tablica outbox obični su aplikacijski podaci. Zadržite dovoljno informacija za identifikaciju događaja, njegovo usmjeravanje, reprodukciju njegova sadržaja i praćenje napretka isporuke. Točna shema ovisi o bazi podataka i posredniku, ali osnove su stabilne.

CREATE TABLE outbox_messages (
    id CHAR(36) PRIMARY KEY,
    event_type VARCHAR(100) NOT NULL,
    aggregate_type VARCHAR(100) NOT NULL,
    aggregate_id CHAR(36) NOT NULL,
    payload JSON NOT NULL,
    occurred_at TIMESTAMP NOT NULL,
    published_at TIMESTAMP NULL,
    attempts INT NOT NULL DEFAULT 0
);

Kada se stvori narudžba, umetnite i narudžbu i njezin događaj prije potvrđivanja. Važan detalj nije oblik JSON-a; važno je da oba umetanja koriste istu vezu s bazom podataka i istu transakciju.

<?php

$pdo->beginTransaction();

try {
    $orderId = $uuidFactory->create();

    $insertOrder = $pdo->prepare(
        'INSERT INTO orders (id, customer_id, total_amount)
         VALUES (:id, :customer_id, :total_amount)'
    );
    $insertOrder->execute([
        'id' => $orderId,
        'customer_id' => $customerId,
        'total_amount' => $totalAmount,
    ]);

    $messageId = $uuidFactory->create();
    $payload = json_encode([
        'event_id' => $messageId,
        'order_id' => $orderId,
        'customer_id' => $customerId,
        'total_amount' => $totalAmount,
    ], JSON_THROW_ON_ERROR);

    $insertOutbox = $pdo->prepare(
        'INSERT INTO outbox_messages
         (id, event_type, aggregate_type, aggregate_id, payload, occurred_at)
         VALUES (:id, :event_type, :aggregate_type, :aggregate_id, :payload, CURRENT_TIMESTAMP)'
    );
    $insertOutbox->execute([
        'id' => $messageId,
        'event_type' => 'OrderCreated',
        'aggregate_type' => 'order',
        'aggregate_id' => $orderId,
        'payload' => $payload,
    ]);

    $pdo->commit();
} catch (Throwable $exception) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }

    throw $exception;
}

Ako se transakcija potvrdi, postoje i narudžba i namjera njezina događaja. Ako se poništi, ne postoji nijedno. Zahtjev se sada može vratiti bez potrebe da posrednik bude ispravan baš u tom trenutku.

Izgradite proces prijenosa za isporuku barem jednom

Pozadinski radni proces čita neobjavljene poruke, šalje ih posredniku i označava uspješne poruke kao objavljene. To omogućuje pouzdanu eventualnu isporuku, a ne isporuku točno jednom.

Postoji neizbježan rubni slučaj: radni proces može uspješno objaviti poruku i srušiti se prije ažuriranja published_at. Pri ponovnom pokretanju ponovno objavljuje poruku. To je ispravno ponašanje procesa prijenosa outboxa. Pokušaj uklanjanja tog ponovnog pokušaja jednostavnom zastavicom umjesto toga riskira gubitak događaja.

Potrošači stoga moraju biti idempotentni. U svaki sadržaj uključite stabilan ID događaja i zadržite dovoljno stanja za prepoznavanje već obrađenih događaja. Servis za plaćanja, primjerice, ne bi smio stvoriti drugu naplatu samo zato što dvaput primi isti događaj OrderCreated.

Preuzmite posao prije objavljivanja

Više radnih procesa prijenosa treba koordinaciju. Nemojte dopustiti da svaki radni proces odabere iste neobjavljene retke i objavi ih istodobno. Upotrijebite strategiju preuzimanja koju podržava odabrana baza podataka, poput zaključavanja redaka tijekom odabira ili stupca zakupa ažuriranog u kratkoj transakciji.

Dizajn temeljen na zakupu obično pohranjuje token preuzimanja i vrijeme isteka. Radni proces preuzima ograničenu seriju, objavljuje svaku stavku, zatim svaku uspješnu stavku označava kao objavljenu. Ako radni proces prestane raditi, istekli zakupi ponovno čine poruke dostupnima. Neka preuzimanja budu kratka, a serije umjerene kako spor posrednik ne bi držao zaključavanja baze podataka tijekom trajanja mrežnog I/O-a.

Bilježite broj pokušaja i najnoviju pogrešku tamo gdje je to operativno korisno. Nakon ponovljenih neuspjeha, usmjerite poruku u stanje kvara koje se može pregledati, umjesto da je bez vidljivosti ponovno pokušavate slati zauvijek. Poslovna odluka ovdje je važna: neki se događaji mogu ručno popraviti, dok drugi zahtijevaju automatizirani kompenzacijski tijek rada.

Redoslijed, sadržaji i evolucija sheme

Outbox osigurava trajnost, ali ne jamči automatski globalni redoslijed. U distribuiranim sustavima globalni redoslijed obično je skuplji nego što je vrijedan. Zapitajte se što zapravo treba redoslijed.

Za mnoge domene redoslijed je važan samo po agregatu: događaji za jednu narudžbu, račun ili pošiljku. Uključite identifikator agregata u poruku i konfigurirajte daljnju obradu oko te granice tamo gdje je posrednik podržava. Potrošači bi također trebali tolerirati zakašnjelu i dupliciranu isporuku.

Učinite sadržaje događaja dovoljno samostalnima da potrošači mogu djelovati bez sinkronog pozivanja proizvođača. Minimalan događaj koji sadrži samo ID često ponovno stvara povezanost putem naknadnih API poziva. Istodobno izbjegavajte tretirati događaj kao potpunu repliku baze podataka. Objavite činjenice koje potrošači trebaju, promišljeno verzionirajte oblik događaja i očuvajte povratnu kompatibilnost dok postoje stari potrošači.

  • Upotrebljavajte eksplicitne nazive događaja: OrderCreated označava dovršenu činjenicu, a ne internu naredbu.
  • Uključite ID događaja: on je temelj deduplikacije na strani potrošača.
  • Zadržite jasnu politiku čuvanja: objavljeni retci mogu se arhivirati ili izbrisati tek nakon što se razumiju operativni zahtjevi i zahtjevi revizije.
  • Pratite starost zaostatka: rastući broj redaka na čekanju je koristan, ali starost najstarijeg događaja na čekanju često prije otkriva utjecaj na korisnike.

Neka prva verzija bude jednostavna

Obrazac outbox ne zahtijeva razrađenu platformu za događaje prvog dana. Tablica, transakcija, mali proces prijenosa i idempotentni potrošači već su znatno poboljšanje pouzdanosti. Počnite s jasnim vlasništvom: svaki servis zapisuje u vlastiti outbox i objavljuje događaje o vlastitom potvrđenom stanju.

Taj skroman dizajn mijenja ponašanje kvarova. Umjesto da kratki prekid posrednika pretvori u propuštene poslovne radnje, sustav pohranjuje svoje obećanje komunikacije i nastavlja pokušavati. U pozadinskom inženjerstvu to je često razlika između sustava koji samo funkcionira u idealnom scenariju i onoga koji ostaje pouzdan kada idealni scenarij nestane.

Portret autora bloga

Mihajlo

Ja sam Mihajlo — programer vođen znatiželjom, disciplinom i stalnom željom da stvorim nešto smisleno. Dijelim uvide, tutorijale i besplatne usluge kako bih pomogao drugima da pojednostave svoj rad i rastu u svijetu softvera i umjetne inteligencije koji se neprestano razvija.