Какую модель гарантии доставки сообщений вы использовали в своих проектах на Node.js?

«Какую модель гарантии доставки сообщений вы использовали в своих проектах на Node.js?» — вопрос из категории Брокеры сообщений, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В моих проектах на 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» (ровно один раз), что является гораздо более сложной гарантией, мы комбинировали подходы:

  1. Идемпотентные обработчики. Логика обработки такова, что повторное выполнение с теми же данными не меняет результат.
  2. Дедупликация на стороне получателя. Сохраняли ID обработанных сообщений (например, в Redis с TTL) и проверяли их перед обработкой.
  3. Транзакционность. При использовании Kafka, например, можно было настраивать isolation.level: 'read_committed' и использовать транзакционные продюсеры/консьюмеры, хотя это добавляет накладные расходы.

Выбор модели всегда был компромиссом, основанным на бизнес-требованиях (критичность потерь vs. дубликатов) и допустимой сложности реализации.