Razvoj

Demystifying Transactional Outbox for Rock-Solid PHP Integrations

Razotkrivanje transakcijskog obrasca Outbox za besprijekorne PHP integracije

Potvrda transakcije baze podataka je obećanje: narudžba postoji, stanje plaćanja se promijenilo, račun je stvoren. Događaj poslan drugom sustavu također je obećanje. Problem počinje kada aplikacija pokušava oba obećanja ostvariti neovisno.

Zamislite PHP krajnju točku koja sprema narudžbu, a zatim objavljuje OrderPlaced posredniku za poruke. Ako transakcija baze podataka uspije, ali se proces sruši prije objave, nizvodni sustavi nikada ne saznaju za narudžbu. Ako se poruka objavi prva, a transakcija se kasnije poništi, potrošači primaju događaj za nešto što ne postoji.

Obrazac transakcijskog outboxa rješava taj neugodni razmak bez pretvaranja da baza podataka i posrednik dijele jednu atomsku transakciju. Bazu podataka čini pouzdanom točkom predaje, a zatim isporučuje događaje iz trajnog outboxa nakon što se poslovna promjena potvrdi.

Osnovna ideja: zajedno potvrditi podatke i namjeru

Outbox je tablica baze podataka koja sadrži poruke koje još treba isporučiti. Tijekom iste transakcije koja ažurira podatke aplikacije, aplikacija umeće outbox red koji opisuje događaj. Jedna potvrda čini trajnom i promjenu stanja i namjeru obavještavanja.

Zaseban radnik čita retke na čekanju i šalje ih posredniku, krajnjoj točki webhooka ili drugoj integraciji. Nakon uspješne isporuke radnik označava red kao obrađen.

Time se namjerno mijenja jamstvo. Aplikacija više ne cilja na isporuku točno jednom, što je obično nepraktično među zasebnim sustavima. Cilja na isporuku najmanje jednom u kombinaciji s idempotentnim potrošačima. Poruka može biti isporučena više puta, ali se ne smije izgubiti nakon potvrde transakcije baze podataka.

Mala, korisna shema

Neka outbox bude eksplicitan i jednostavan. On je operativna infrastruktura, pa bi ga trebalo biti lako pregledati kada nešto pođe po zlu.

CREATE TABLE outbox_messages (
    id CHAR(36) PRIMARY KEY,
    topic VARCHAR(100) NOT NULL,
    payload JSON NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    processed_at TIMESTAMP NULL,
    attempts INT NOT NULL DEFAULT 0,
    last_error TEXT NULL
);

CREATE INDEX outbox_pending_idx
    ON outbox_messages (processed_at, created_at);

ID poruke je važan. Potrošačima daje stabilan ključ idempotentnosti, a operaterima precizan identifikator za praćenje neuspjeha. Sadržaj poruke trebao bi sadržavati dovoljno konteksta da potrošač može djelovati bez neposredne potrebe za krhkim povratnim pozivom prema izvornoj usluzi.

Nemojte outbox tretirati kao trajnu arhivu događaja, osim ako je to svjestan zahtjev. Redovita politika zadržavanja može ukloniti uspješno obrađene retke nakon razdoblja potrebnog za dijagnostiku i ponavljanje.

Zapisivanje poslovne promjene u PHP-u

Važan dio nije značajka okvira; to je opseg transakcije. Poslovni zapis i umetanje u outbox moraju koristiti istu vezu s bazom podataka i dogoditi se prije iste potvrde.

$pdo->beginTransaction();

try {
    $orderId = '...';       // Generated by the application
    $messageId = '...';     // A unique, stable message ID

    $order = $pdo->prepare(
        'INSERT INTO orders (id, customer_id, status) VALUES (?, ?, ?)'
    );
    $order->execute([$orderId, $customerId, 'placed']);

    $outbox = $pdo->prepare(
        'INSERT INTO outbox_messages (id, topic, payload)
         VALUES (?, ?, CAST(? AS JSON))'
    );
    $outbox->execute([
        $messageId,
        'orders.placed',
        json_encode([
            'event_id' => $messageId,
            'order_id' => $orderId,
            'customer_id' => $customerId,
        ], JSON_THROW_ON_ERROR),
    ]);

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

    throw $error;
}

Primijetite što nedostaje: nema poziva posredniku unutar transakcije. Pozivanje udaljene usluge dok se drže zaključavanja baze podataka čini transakcije sporijima i manje predvidljivima. Još važnije, to i dalje ne može učiniti udaljenu objavu i potvrdu baze podataka atomskima.

Sigurna isporuka poruka

Radnik može anketirati poruke na čekanju, preuzeti malu grupu, objaviti svaki događaj i svaki uspješno obrađen označiti kao obrađen. Točan SQL ovisi o stroju baze podataka, osobito kada istodobno radi više radnika. Cilj je uvijek isti: spriječiti da dva radnika istodobno obrađuju isti red, uz izbjegavanje dugotrajnog držanja zaključavanja tijekom mrežnih poziva.

Praktičan dizajn dodaje polja za preuzimanje, poput locked_at i locked_by. Radnik atomski preuzima prihvatljive retke, potvrđuje to preuzimanje, objavljuje izvan transakcije preuzimanja, a zatim bilježi uspjeh ili neuspjeh. Preuzimanja trebaju rok isteka kako bi poruka ponovno postala prihvatljiva ako radnik prekine rad usred obrade.

  • Koristite ograničenu veličinu grupe kako jedna spora integracija ne bi monopolizirala radnika.
  • Povećajte attempts i pohranite sigurnu dijagnostičku pogrešku nakon neuspjele objave.
  • Primijenite odgodu za ponovljene neuspjehe umjesto ponovnog pokušavanja neispravnog odredišta u tijesnoj petlji.
  • Postavite upozorenja za stare neobrađene poruke i neuobičajeno velik broj pokušaja.
  • Kod za objavljivanje držite odvojenim od koda za zapisivanje domene; to olakšava testiranje i upravljanje neuspjesima.

Radnik mora pretpostaviti da je moguć sljedeći slijed: objava uspije, a zatim proces prekine rad prije ažuriranja processed_at. Poruka će biti ponovno poslana nakon isteka preuzimanja. To nije nedostatak obrasca; to je očekivani razlog zbog kojeg potrošači moraju biti idempotentni.

Idempotentnost je druga polovica

Potrošači bi trebali zabilježiti ID događaja prije ili tijekom primjene vlastitog nuspojave. Na primjer, usluga može umetnuti ID događaja u tablicu zaštićenu jedinstvenim ograničenjem. Ako stigne duplikat, ograničenje pokazuje da je već obrađen.

Za vanjski API upotrijebite njegov mehanizam idempotentnosti kada je dostupan, s ključem koji se temelji na ID-u outbox poruke. Ako nema takav mehanizam, pohranite dovoljno lokalnog stanja kako biste izbjegli ponavljanje nepovratne radnje. Slanje e-pošte dvaput ili stvaranje duplicirane pretplate rijetko se rješava pukim upornijim ponavljanjem pokušaja.

Također jasno definirajte semantiku događaja. Događaj nazvan orders.placed trebao bi opisivati dovršenu činjenicu, a ne uputu čije se tumačenje razlikuje među potrošačima. Uključite verziju u sadržaj poruke kada se može razvijati i neka potrošači toleriraju dodatna polja.

Uobičajeni prečaci koji potajno ne uspijevaju

Neki timovi zapisuju red baze podataka, potvrde ga, a zatim iz memorije aplikacije „ponovno pokušaju objaviti kasnije”. Ponovno pokretanje briše taj red čekanja za ponovne pokušaje. Drugi objavljuju prije potvrde i nadaju se da su poništavanja rijetka. Rijetke nedosljednosti i dalje su nedosljednosti, a često ih je najteže uskladiti.

Još jedna pogreška je označavanje outbox retka kao obrađenog prije završetka udaljenog poziva. Time se duplikati zamjenjuju izgubljenim porukama. Sigurnija je suprotna zamjena: najprije objaviti, zatim označiti uspjeh, prihvaćajući duplikate i projektirajući sustav za njih.

Učinite pouzdanost vidljivom

Obrazac outboxa nije samo kod; on je operativni model. Pratite broj poruka na čekanju, starost najstarije poruke na čekanju, neuspjehe isporuke i propusnost obrade. Omogućite kontroliran način ponavljanja poruke nakon ispravljanja potrošača ili odredišta. Dosljedno bilježite ID-ove poruka u API zahtjevu, radniku i klijentu integracije.

Transakcijski outbox ne čini distribuirane sustave jednostavnima. On neizvjesnosti daje trajno mjesto gdje može biti ponovno pokušana, promatrana i popravljena. Za PHP integracije koje moraju preživjeti rušenja, vremenska ograničenja i implementacije, to je mnogo čvršći temelj od nade da se dva neovisna zapisa dogode pravilnim redoslijedom.

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.