Какую модель гарантии доставки сообщений вы использовали в своих проектах на 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. дубликатов) и допустимой сложности реализации.

Ответ 18+ 🔞

Слушай, я тут подумал, пока кофе остывал, про свои проекты на Node.js. Там же постоянно асинхронщина, задачи всякие, интеграции — ёпта, голова кругом. Так вот, в 99% случаев я использовал модель «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 с временем жизни и тупо проверяем, не видели ли уже эту хрень.
  3. Транзакционность. Если используешь Kafka, там можно врубить isolation.level: 'read_committed' и прочие транзакционные плюшки. Но это, блядь, сразу оверхеад и волосы седеть начинают.

В общем, выбор всегда был на грани. С одной стороны — потерять данные нельзя, с другой — задублировать тоже не айс. Смотрел по ситуации, что бизнесу важнее, и какого хуя он готов за это платить сложностью кода.