После сбоя потребитель может получить одно сообщение повторно: первый запуск уже изменил локальную базу, но не успел подтвердить сообщение брокеру. Какой механизм делает такую обработку безопасной?
Используйте транзакционный inbox: сохраните уникальный идентификатор сообщения и бизнес-изменение в одной локальной транзакции базы данных, а подтверждайте сообщение брокеру только после её фиксации. При повторной доставке потребитель обнаружит уже сохранённый идентификатор и не применит бизнес-изменение повторно.
Это не означает абсолютную семантику «ровно один раз» во всей распределённой системе. Механизм надёжно защищает локальные изменения потребителя, но внешние побочные эффекты требуют отдельной идемпотентности или исходящего надёжного обмена.
Брокер сообщений и база данных потребителя обычно не участвуют в одной общей транзакции. Поэтому между обработкой сообщения, фиксацией бизнес-изменения и подтверждением доставки существует окно сбоя.
Если процесс завершится после записи в базу, но до подтверждения брокеру, сообщение будет доставлено повторно. Если подтвердить его раньше записи, при падении потребителя изменение может быть потеряно. Inbox появился как локальный механизм устранения этой неопределённости без распределённой транзакции между брокером и базой данных.
Рассмотрим последовательность: потребитель получил сообщение, изменил состояние заказа, но упал до отправки подтверждения. Брокер корректно считает сообщение необработанным и доставляет его снова.
Без защиты повторная обработка может дважды начислить бонус, создать дубликат записи или повторно перевести состояние. Простая проверка «сообщение обычно приходит один раз» недостаточна: повторная доставка является нормальным следствием отказа, а не обязательно ошибкой брокера.
Каждое сообщение должно иметь стабильный уникальный идентификатор в пределах контракта. В одной транзакции базы данных потребитель:
Подтверждение брокеру отправляется только после успешной фиксации транзакции. Если сбой произошёл до фиксации, повторная доставка выполнит операцию заново. Если сбой произошёл после фиксации, повторная доставка обнаружит идентификатор в inbox и завершится без повторного эффекта.
Уникальное ограничение на идентификатор сообщения должно защищать от гонки, когда два экземпляра одновременно обрабатывают одну доставку. Проверка в приложении без ограничения базы данных может привести к двойной обработке.
Inbox не устраняет необходимость повторных попыток: временные ошибки должны приводить к повторной доставке или запланированному retry. Для постоянно некорректных сообщений нужны ограничение числа попыток и dead-letter queue, иначе одно сообщение может бесконечно блокировать обработку.
Механизм действует только на изменения, находящиеся в той же локальной транзакции. Если обработчик сначала изменяет свою базу, а затем вызывает внешний платёжный сервис, транзакция inbox не сделает внешний вызов атомарным. Для такого побочного эффекта нужны идемпотентный ключ операции, отдельный надёжный процесс публикации команды или другая предметная стратегия согласования.
Сервис заказов получает событие о подтверждённой оплате и должен перевести заказ в состояние «оплачен». После записи изменения процесс завершается из-за сбоя до подтверждения сообщения. Брокер повторяет доставку.
Вариант без inbox прост, но опасен: повторный обработчик может дважды создать запись о начислении программы лояльности. Вариант с распределённой транзакцией между брокером и базой данных теоретически уменьшает окно неопределённости, но усложняет инфраструктуру, повышает связанность и не решает автоматически внешние вызовы.
Выбранное решение — таблица inbox с уникальным идентификатором сообщения и перевод заказа в статус «оплачен» в одной транзакции. Повторная доставка фиксируется как уже обработанная и не меняет заказ повторно. Начисление бонусов оформляется отдельным идемпотентным действием с тем же бизнес-идентификатором операции.
Результат: сбой между фиксацией базы и подтверждением брокеру приводит только к безопасной повторной проверке, а не к повторному бизнес-эффекту. При этом система сохраняет at-least-once delivery, но делает локальную обработку эффективной идемпотентной.
1. Достаточно ли проверять идентификатор сообщения в приложении без уникального ограничения базы данных?
Нет. Два экземпляра могут одновременно проверить отсутствие идентификатора, оба начать обработку и оба применить бизнес-изменение. Уникальное ограничение или эквивалентная атомарная операция на стороне хранилища превращает проверку в защищённое от гонки решение.
2. Что произойдёт, если идентификатор inbox записывается в одной транзакции, а бизнес-изменение — в другой?
Появится новое окно сбоя. Если идентификатор зафиксирован первым, а процесс упал до бизнес-изменения, повторная доставка может быть ошибочно признана обработанной. Поэтому маркер обработки и локальный бизнес-эффект должны фиксироваться одной транзакцией либо управляться явной машиной состояний, допускающей безопасное возобновление.
3. Гарантирует ли inbox отсутствие повторного списания денег во внешней платёжной системе?
Нет. Inbox защищает локальную транзакцию потребителя, но не внешний сервис. Платёжная операция должна иметь идемпотентный ключ, который платёжный сервис использует для распознавания повторного запроса; иначе повторная попытка после тайм-аута всё ещё может создать повторное списание.