Publish/Subscribe и гарантии доставки

Один издатель — много подписчиков, и при этом сообщение почти всегда доставляется «как минимум один раз».

Publish/Subscribe (pub/sub) — паттерн, где издатель публикует событие в тему, а все подписчики на эту тему получают копию, ничего не зная друг о друге.

Чем pub/sub отличается от очереди

В обычной очереди сообщение забирает один consumer и оно исчезает. В pub/sub событие получают все подписчики. Пример: пользователь оформил заказ → событие order.created → его независимо получают сервис склада, сервис уведомлений и сервис аналитики. Издатель не знает об их существовании — добавить нового подписчика можно, не трогая издателя.

                       ┌──▶ [ склад ]
[ заказ ] ─order.created─┼──▶ [ уведомления ]
                       └──▶ [ аналитика ]
Издатель не знает о подписчиках; добавляем нового — код издателя не меняем.

Гарантии доставки

Сеть ненадёжна: подтверждение может потеряться, и тогда отправитель не знает, дошло ли сообщение. Отсюда три уровня гарантий — и у каждого своя цена.

ГарантияСмыслРиск
At-most-onceне повторяем отправкусообщение может потеряться
At-least-onceповторяем, пока не подтвердятвозможны дубли
Exactly-onceровно один раздорого и сложно достичь честно

На практике массово используют at-least-once: лучше доставить дважды, чем потерять. Но тогда обработчик обязан корректно переживать дубли — иначе письмо уйдёт дважды, а деньги спишутся повторно.

Идемпотентность — лекарство от дублей

Идемпотентная операция — операция, повторное выполнение которой даёт тот же результат, что и однократное.

Если обработка идемпотентна, дубли при at-least-once безвредны. Типичный приём — ключ идемпотентности: у каждого сообщения есть уникальный id, обработчик запоминает обработанные id и игнорирует повтор.

При получении сообщения с id = X:
  обрабатывали X раньше?
    да  → пропустить (уже сделано)
    нет → выполнить, записать X как обработанный

Так «списать деньги» становится безопасным к повтору: второй приход того же id ничего не спишет. «Exactly-once на практике» — это почти всегда «at-least-once + идемпотентность», а не магия брокера.

Примеры идемпотентных и неидемпотентных операций

ИдемпотентноНеидемпотентно (опасно при повторе)
«установить статус = paid»«увеличить баланс на 100»
«удалить пользователя X»«отправить ещё одно письмо»
HTTP PUT, DELETEHTTP POST без ключа идемпотентности

Итог

  • Pub/sub рассылает событие всем подписчикам; издатель о них не знает — легко добавлять новых.
  • At-least-once — стандарт: лучше дубль, чем потеря, но обработчик должен переживать повторы.
  • Идемпотентность (ключ id) делает дубли безвредными; «exactly-once» на практике — это at-least-once + идемпотентность.
Проверьте себя
1. Чем pub/sub отличается от обычной очереди?
AВ pub/sub сообщение получает только один потребитель
BВ pub/sub событие получают все подписчики, а в очереди — один потребитель
CPub/sub не использует брокер
DВ очереди нет потребителей
2. Почему at-least-once требует идемпотентности обработчика?
AПотому что сообщения теряются
BПотому что одно сообщение может прийти дважды, и повтор не должен ломать результат
CПотому что подписчиков всегда несколько
DИдемпотентность тут не нужна
3. Какая операция идемпотентна?
A«увеличить баланс на 100»
B«установить статус заказа = paid»
C«отправить ещё одно письмо»
D«добавить товар в корзину»
4. Как на практике обычно достигают «exactly-once»?
Aспециальной магией брокера без усилий
Bсочетанием at-least-once и идемпотентной обработки по ключу id
Cотключением повторных отправок
Dпереходом на синхронные вызовы
Поддержать проект