За доставки «щонайменше один раз» те саме повідомлення може прийти кілька разів. Ідемпотентний споживач гарантує, що повторна обробка не має додаткового ефекту.
Підхід 1 - журнал оброблених повідомлень. Кожне повідомлення має унікальний ідентифікатор. Перед обробкою споживач записує його в таблицю з унікальним індексом - в тій самій транзакції, що й зміни даних:
public function handle(OrderPaid $message): void
{
DB::transaction(function () use ($message) {
$inserted = DB::table('processed_messages')->insertOrIgnore([
'message_id' => $message->id,
'consumer' => 'loyalty.points',
'processed_at' => now(),
]);
if ($inserted === 0) {
return; // уже оброблено
}
LoyaltyAccount::forCustomer($message->customerId)->addPoints($message->points);
});
}
Транзакція гарантує: або повідомлення позначено і бали нараховано, або ні те, ні інше. Окремий запис «оброблено» після дії (не в транзакції) залишає вікно для дубліката.
Підхід 2 - ідемпотентна операція за природою. Замість «додати 100 балів» - «встановити бали за замовлення 42 у 100»:
LoyaltyEntry::updateOrCreate(
['order_id' => $message->orderId],
['points' => $message->points],
);
Повтор перезаписує те саме значення.
Підхід 3 - перевірка стану. «Якщо замовлення вже позначено оплаченим - нічого не робити». Працює, якщо стан однозначно показує, що дію виконано.
Підхід 4 - ключ ідемпотентності в зовнішньому виклику. Якщо споживач викликає платіжну систему, передати ключ (наприклад, id повідомлення) - провайдер сам відкине повтор.
Складні випадки:
- побічні ефекти поза базою (лист, SMS, виклик API) не відкочуються транзакцією. Якщо лист надіслано, а позначка «оброблено» не збереглася, повтор надішле ще раз. Рішення - ключ ідемпотентності в провайдера чи окремий крок із власною позначкою;
- порядок повідомлень: дублікат старого повідомлення може прийти після нового - перевіряти версію чи час сутності;
- очищення журналу: таблиця оброблених повідомлень росте - старі записи видаляють після періоду, за який дублікати вже неможливі.
У Laravel для джоб у черзі: ShouldBeUnique запобігає дублікатам у черзі, але не замінює ідемпотентності обробника - джоба все одно може виконатися двічі після збою воркера.
Докладніше в документації: Microservices.io: Idempotent Consumer