PHP трансакции: Образец Outbox за сигурна комуникација меѓу услуги
Повеќето неуспеси меѓу услуги не се драматични прекини. Тие се тивки празнини: нарачката е зачувана, но настанот за наплата никогаш не е испратен; е-порака се става во ред двапати по повторен обид; работникот паѓа помеѓу објавувањето порака и евидентирањето дека го сторил тоа.
Основниот проблем е едноставен. Трансакцијата во база на податоци и посредникот за пораки се одделни системи. Ако кодот на апликацијата запишува во едниот, а потоа во другиот, секогаш постои прозорец за неуспех помеѓу тие операции. Шемата outbox го затвора тој прозорец така што деловната промена и намерата за објавување настан стануваат дел од истата трансакција во базата на податоци.
Зошто „зачувај, па објави“ е ненадежно
Типична услуга започнува со код што изгледа разумно: креирај нарачка, потврди ја, па објави OrderCreated. Тоа работи сè додека процесот не ја изгуби врската, не биде прекинат или посредникот не е привремено недостапен по потврдувањето во базата на податоци.
Превртувањето на редоследот не го решава проблемот. Првичното објавување може да изложи настан за нарачка што подоцна не успева да се потврди. Повторувањето на кој било редослед воведува дупликати, освен ако секој потрошувач не е подготвен за нив.
Шемата outbox го менува договорот. Наместо директно да објавува во рамките на барањето, услугата запишува запис за настан во сопствената база на податоци покрај деловните податоци. Посебен процес за препраќање подоцна ги објавува записите што чекаат.
Трансакцискиот outbox во пракса
Табелата outbox е обичен податок на апликацијата. Чувајте доволно информации за да го идентификувате настанот, да го насочите, да ја репродуцирате неговата содржина и да го следите напредокот на испораката. Точната шема зависи од базата на податоци и посредникот, но суштинските елементи се стабилни.
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
);
Кога се креира нарачка, вметнете ги и нарачката и нејзиниот настан пред потврдувањето. Важниот детал не е JSON-обликот; туку тоа што и двете вметнувања ја користат истата врска со базата на податоци и трансакција.
<?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;
}
Ако трансакцијата се потврди, постојат и нарачката и намерата за нејзиниот настан. Ако се поништи, не постои ниту едното. Барањето сега може да се врати без да бара посредникот да биде во добра состојба токму во тој момент.
Изградете го препраќачот за испорака барем еднаш
Работник во заднина чита необјавени пораки, ги испраќа до посредникот и успешните пораки ги означува како објавени. Ова обезбедува сигурна евентуална испорака, а не испорака точно еднаш.
Постои неизбежен граничен случај: работникот може успешно да објави порака и да падне пред да го ажурира published_at. По рестартирањето, повторно ја објавува пораката. Тоа е правилно однесување за outbox препраќач. Обидот да се елиминира ова повторување со едноставно знаменце, наместо тоа, ризикува губење на настани.
Затоа потрошувачите мора да бидат идемпотентни. Вклучете стабилен ID на настан во секоја содржина и задржете доволно состојба за да препознаете веќе обработени настани. Услуга за плаќања, на пример, не треба да создаде второ задолжување само затоа што двапати го прима истиот настан OrderCreated.
Преземете работа пред објавувањето
На повеќе работници за препраќање им е потребна координација. Не дозволувајте секој работник да ги избере истите необјавени редови и да ги објави истовремено. Користете стратегија за преземање поддржана од избраната база на податоци, како заклучување редови при изборот или колона за закуп ажурирана во кратка трансакција.
Дизајнот заснован на закуп обично чува токен за преземање и време на истекување. Работникот презема ограничена серија, ја објавува секоја ставка, а потоа секоја успешно објавена ставка ја означува како објавена. Ако работникот откаже, истечените закупи ги прават пораките повторно достапни. Нека преземањата бидат кратки, а сериите умерени, за бавен посредник да не држи заклучувања на базата на податоци во текот на мрежниот I/O.
Евидентирајте го бројот на обиди и најновата грешка таму каде што тоа е оперативно корисно. По повторени неуспеси, насочете ја пораката во состојба на неуспех што може да се прегледа, наместо засекогаш да се обидувате повторно без видливост. Деловната одлука е важна тука: некои настани може рачно да се поправат, додека други бараат автоматизиран компензациски тек на работа.
Редослед, содржини и еволуција на шемата
Outbox обезбедува трајност, но не гарантира автоматски глобален редослед. Во дистрибуираните системи, глобалниот редослед обично е поскап отколку што е вреден. Прашајте што навистина има потреба од редослед.
За многу домени, редоследот е важен само по агрегат: настани за една нарачка, сметка или пратка. Вклучете го идентификаторот на агрегатот во пораката и конфигурирајте ја обработката надолу по текот околу таа граница каде што посредникот го поддржува тоа. Потрошувачите исто така треба да толерираат задоцнета испорака и испорака на дупликати.
Направете ги содржините на настаните доволно самостојни за потрошувачите да можат да дејствуваат без синхроно повторно повикување кон производителот. Минимален настан што содржи само ID често повторно создава спрегнатост преку последователни API повици. Во исто време, избегнувајте да третирате настан како целосна реплика на база на податоци. Објавете ги фактите што им се потребни на потрошувачите, намерно верзионирајте го обликот на настанот и зачувајте ја повратната компатибилност додека постојат стари потрошувачи.
- Користете експлицитни имиња на настани:
OrderCreatedсоопштува завршен факт, а не внатрешна команда. - Вклучете ID на настан: тоа е основата за дедупликација на страната на потрошувачот.
- Одржувајте јасна политика за задржување: објавените редови може да се архивираат или избришат само откако ќе се разберат оперативните и ревизорските барања.
- Следете ја староста на заостанатите пораки: растечкиот број редови што чекаат е корисен, но староста на најстариот настан што чека често порано го открива влијанието врз клиентите.
Нека првата верзија биде едноставна
Шемата outbox не бара сложена платформа за настани уште од првиот ден. Табела, трансакција, мал препраќач и идемпотентни потрошувачи веќе претставуваат значително подобрување на доверливоста. Започнете со јасна сопственост: секоја услуга запишува во сопствениот outbox и објавува настани за сопствената потврдена состојба.
Тој скромен дизајн го менува начинот на кој се однесуваат неуспесите. Наместо краткиот прекин на посредникот да го претвори во недостасувачки деловни дејства, системот го чува своето ветување дека ќе комуницира и продолжува да се обидува. Во backend инженерството, тоа често е разликата помеѓу систем што само работи во среќната патека и систем што останува доверлив кога среќната патека ќе исчезне.