Ответ
В моих проектах на Node.js, работающих с асинхронной обработкой задач и интеграциями, чаще всего применялась модель «at-least-once» (минимум один раз). Это баланс между надёжностью и производительностью.
Реализация с RabbitMQ и библиотекой amqplib:
// Потребитель (Consumer)
channel.consume('order.queue', async (message) => {
if (message === null) return;
try {
const orderData = JSON.parse(message.content.toString());
// Обрабатываем заказ (например, сохраняем в БД)
await orderService.processOrder(orderData);
// Явное подтверждение (ack) после успешной обработки
channel.ack(message);
} catch (error) {
console.error('Ошибка обработки заказа:', error);
// Отказ без повторной постановки в очередь или с ней (requeue: true)
channel.nack(message, false, false); // Не возвращаем в очередь, отправляем в DLQ
}
}, { noAck: false }); // Важно: отключаем autoAck
Почему at-least-once?
- Надёжность: Сообщение не теряется при падении воркера (оно остаётся в очереди).
- Дубликаты: Мы принимаем риск повторной обработки. Это компенсируется идемпотентностью операций на стороне обработчика. Например, перед созданием записи в БД проверяем её существование по уникальному ID сообщения.
Для сценариев, требующих «exactly-once» (ровно один раз), что является гораздо более сложной гарантией, мы комбинировали подходы:
- Идемпотентные обработчики. Логика обработки такова, что повторное выполнение с теми же данными не меняет результат.
- Дедупликация на стороне получателя. Сохраняли ID обработанных сообщений (например, в Redis с TTL) и проверяли их перед обработкой.
- Транзакционность. При использовании Kafka, например, можно было настраивать
isolation.level: 'read_committed'и использовать транзакционные продюсеры/консьюмеры, хотя это добавляет накладные расходы.
Выбор модели всегда был компромиссом, основанным на бизнес-требованиях (критичность потерь vs. дубликатов) и допустимой сложности реализации.