Демистифицирање на трансакцискиот Outbox за сигурни PHP интеграции
Потврдата на трансакција во база на податоци е ветување: нарачката постои, статусот на плаќањето е променет, сметката е создадена. Настанот испратен до друг систем исто така е ветување. Проблемот почнува кога апликацијата се обидува независно да ги исполни двете ветувања.
Замислете PHP крајна точка што зачувува нарачка, а потоа објавува OrderPlaced до посредник за пораки. Ако трансакцијата во базата на податоци успее, но процесот падне пред објавувањето, подредените системи никогаш нема да дознаат за нарачката. Ако пораката се објави прво, а трансакцијата подоцна се поништи, потрошувачите добиваат настан за нешто што не постои.
Шаблонот за трансакциско излезно сандаче го решава овој незгоден јаз без да се преправа дека базата на податоци и посредникот делат една атомска трансакција. Тој ја прави базата на податоци сигурна точка за предавање, а потоа испорачува настани од трајно излезно сандаче откако деловната промена е потврдена.
Основната идеја: потврдете ги податоците и намерата заедно
Излезното сандаче е табела во база на податоци што содржи пораки кои сѐ уште треба да се испорачаат. Во истата трансакција што ги ажурира податоците на апликацијата, апликацијата внесува ред во излезното сандаче што го опишува настанот. Една потврда ги прави трајни и промената на состојбата и намерата за известување.
Посебен работник ги чита редовите што чекаат и ги испраќа до посредникот, крајната точка за веб-кука или друга интеграција. Откако испораката ќе успее, работникот го означува редот како обработен.
Ова намерно ја менува гаранцијата. Апликацијата повеќе не цели кон испорака точно еднаш, што обично е непрактично низ одделни системи. Таа цели кон испорака барем еднаш во комбинација со идемпотентни потрошувачи. Пораката може да биде испорачана повеќе од еднаш, но не смее да се изгуби откако ќе се потврди трансакцијата во базата на податоци.
Мала, корисна шема
Нека излезното сандаче биде експлицитно и едноставно. Тоа е оперативна инфраструктура, па треба лесно да се провери кога нешто ќе тргне наопаку.
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);
Идентификаторот на пораката е важен. Тој им дава на потрошувачите стабилен клуч за идемпотентност и на операторите прецизен идентификатор за следење неуспеси. Содржината треба да содржи доволно контекст за потрошувачот да дејствува без веднаш да му треба кревок повратен повик кон изворната услуга.
Не третирајте го излезното сандаче како трајна архива на настани, освен ако тоа не е свесно барање. Рутинска политика за задржување може да ги отстрани успешно обработените редови по периодот потребен за дијагностика и повторување.
Запишување на деловната промена во PHP
Важниот дел не е функционалност на рамка; тоа е опсегот на трансакцијата. Деловното запишување и внесувањето во излезното сандаче мора да ја користат истата конекција со базата на податоци и да се случат пред истата потврда.
$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;
}
Забележете што недостасува: нема повик до посредникот во рамките на трансакцијата. Повикот до оддалечена услуга додека се задржуваат заклучувања во базата на податоци ги прави трансакциите побавни и понепредвидливи. Уште поважно, тој сѐ уште не може да ги направи атомски оддалеченото објавување и потврдата на базата на податоци.
Безбедно испорачување пораки
Работникот може да проверува за пораки што чекаат, да преземе мала серија, да го објави секој настан и да го означи секој успешно испорачан како обработен. Точниот SQL зависи од механизмот на базата на податоци, особено кога повеќе работници работат истовремено. Целта е секогаш иста: да се спречи два работника да го обработуваат истиот ред во исто време, додека се избегнуваат долготрајни заклучувања при мрежни повици.
Практичниот дизајн додава полиња за преземање, како locked_at и locked_by. Работникот атомски презема подобни редови, го потврдува тоа преземање, објавува надвор од трансакцијата за преземање, а потоа бележи успех или неуспех. Преземањата треба да имаат истекување, за пораката повторно да стане подобна ако работникот запре среде обработка.
- Користете ограничена големина на серија, за една бавна интеграција да не го монополизира работникот.
- Зголемете го
attemptsи зачувајте безбедна дијагностичка грешка по неуспешно објавување. - Применете одложување при повторливи неуспеси наместо да се обидувате повторно со неисправна дестинација во тесна јамка.
- Поставете предупредување за стари необработени пораки и невообичаено високи броеви на обиди.
- Држете го кодот за објавување одделен од кодот за запишување на доменот; така неуспесите полесно се тестираат и управуваат.
Работникот мора да претпостави дека е можна следнава низа: објавувањето успева, а потоа процесот умира пред да се ажурира processed_at. Пораката ќе биде испратена повторно откако ќе истече преземањето. Тоа не е недостаток на шаблонот; тоа е очекуваната причина поради која потрошувачите мора да бидат идемпотентни.
Идемпотентноста е другата половина
Потрошувачите треба да го евидентираат идентификаторот на настанот пред или при применувањето на сопствениот несакан ефект. На пример, услуга може да го внесе идентификаторот на настанот во табела заштитена со единствено ограничување. Ако пристигне дупликат, ограничувањето покажува дека веќе е обработен.
За надворешно API, користете го неговиот механизам за идемпотентност кога е достапен, со клуч што е идентификаторот на пораката од излезното сандаче. Ако нема таков механизам, зачувајте доволно локална состојба за да избегнете повторување на неповратното дејство. Испраќањето е-пошта двапати или создавањето дупликат претплата ретко се решава само со поупорно повторување.
Исто така, направете ја семантиката на настаните јасна. Настанот со име orders.placed треба да опишува завршен факт, а не инструкција чие толкување се разликува по потрошувач. Вклучете верзија во содржината кога таа може да се развива и нека потрошувачите толерираат дополнителни полиња.
Вообичаени кратенки што тивко не успеваат
Некои тимови го запишуваат редот во базата на податоци, потврдуваат, а потоа „се обидуваат повторно да објават подоцна“ од меморијата на апликацијата. Рестартирањето ја брише таа редица за повторни обиди. Други објавуваат пред потврдувањето и се надеваат дека поништувањата се ретки. Ретките недоследности сѐ уште се недоследности и често се најтешките за усогласување.
Друга грешка е означување на ред од излезното сандаче како обработен пред да заврши оддалечениот повик. Тоа ги заменува дупликатите со изгубени пораки. Побезбедната замена е обратната: прво објавете, потоа означете успех, прифаќајќи дупликати и дизајнирајќи за нив.
Направете ја сигурноста видлива
Шаблонот за излезно сандаче не е само код; тој е оперативен модел. Следете го бројот на пораки што чекаат, староста на најстарата порака што чека, неуспесите при испорака и пропусноста на обработката. Обезбедете контролиран начин за повторување порака по поправка на потрошувач или дестинација. Доследно евидентирајте ги идентификаторите на пораките низ API-барањето, работникот и клиентот за интеграција.
Трансакциското излезно сандаче не ги прави распределените системи едноставни. Тоа ѝ дава на неизвесноста трајно место каде што може повторно да се обиде, да се набљудува и да се поправи. За PHP интеграции што мора да преживеат падови, истекувања на време и распоредувања, тоа е многу поцврста основа отколку надежта дека две независни запишувања ќе се случат по правилен редослед.