Как меняется стратегия поведения потребителя сообщений при разных гарантиях доставки (at least once, at most once, exactly once)?
Что проверяют
Понимаете ли вы, что гарантия доставки определяется не брокером, а тем, в какой момент потребитель подтверждает сообщение относительно обработки. Следом обычно спрашивают, как сделать обработчик идемпотентным и бывает ли exactly once на самом деле.
Как отвечать
Разберите три режима через момент подтверждения:
- at most once: подтверждаем до обработки. В RabbitMQ это
autoAck: true, в Azure Service Bus режимReceiveAndDelete, в Kafka коммит offset до бизнес-логики. Упали посреди обработки, сообщение потеряно. Годится для телеметрии, где потеря одного события не страшна; - at least once: подтверждаем после успешной обработки. В RabbitMQ ручной
BasicAckв конце обработчика, в Service BusPeekLockиCompleteMessageAsync, в Confluent.KafkaEnableAutoCommit = falseиCommitпосле обработки либоEnableAutoOffsetStore = falseсоStoreOffset. Упали после записи в базу, но до подтверждения, и сообщение придёт снова. Отсюда главное требование: идемпотентность; - exactly once: в чистом виде это свойство обработки, а не доставки. Транзакции Kafka (
SendOffsetsToTransaction,IsolationLevel.ReadCommittedу читателя) дают его только в цепочке «прочитал из Kafka, записал в Kafka». Как только есть внешняя база, письмо или платёж, на практике это at least once плюс дедупликация.
Идемпотентность строится так: таблица обработанных MessageId с уникальным ключом и запись в неё в той же транзакции, что и бизнес-изменение (паттерн Inbox), либо операции, которые безопасно повторять: upsert, установка статуса вместо инкремента. В MassTransit и NServiceBus есть готовые inbox и outbox.
Где спотыкаются
Думают, что exactly once в брокере защитит от двойного списания в базе. Не защитит: граница транзакции брокера до вашей базы не дотягивается.
Делают дедупликацию через «проверить, потом вставить» без уникального индекса. Два экземпляра потребителя проскакивают проверку одновременно.
Забывают про ядовитые сообщения: бесконечный requeue блокирует очередь. Нужен лимит повторов и dead letter queue.