> Как решать проблемы с транзакциями при изменении нескольких агрегатов в одной транзакции (Node.js, JavaScript)

Уровень: senior · Роль: backend · Категория: Технические вопросы

Компании: Mosline

Стек: Node.js, JavaScript

> Пример ответа

Короткий ответ

Проблема решается через паттерн Saga (хореография или оркестрация), eventual consistency и компенсирующие транзакции. Вместо ACID в рамках одного сервиса используем распределённые транзакции с outbox pattern для гарантии доставки событий. Для Node.js подходят библиотеки вроде @eventstore/db-client или собственные реализации на основе очередей (RabbitMQ, Kafka). Важно избегать distributed transactions через двухфазный коммит - это убивает производительность.

Подробное объяснение

При изменении нескольких агрегатов в одной транзакции (например, заказ + склад + платёж) в микросервисной архитектуре стандартные ACID-транзакции неприменимы, так как агрегаты могут находиться в разных сервисах или даже базах данных. Основные подходы:

  • Saga - последовательность локальных транзакций с компенсациями при сбое. Бывает хореографическая (каждый сервис слушает события и реагирует) и оркестрируемая (центральный координатор управляет шагами).
  • Outbox pattern - запись события в ту же БД, что и агрегат, в одной локальной транзакции. Отдельный процесс читает outbox и отправляет события в очередь/шину.
  • Eventual consistency - согласие на временную несогласованность данных, которая разрешается через события.

Для Node.js типичный стек: PostgreSQL/MySQL с outbox таблицей + Kafka/RabbitMQ для событий. Важно избегать distributed transactions (XA) - они сложны, медленны и не масштабируются.

На практике

  1. Определите границы агрегатов - каждый агрегат должен изменяться в своей локальной транзакции.
  2. Используйте outbox - при сохранении агрегата пишите событие в ту же транзакцию.
  3. Реализуйте Saga - для цепочки операций (например, создание заказа → списание со склада → списание денег). При ошибке на любом шаге запускайте компенсации.
  4. Добавьте идемпотентность - каждое событие должно обрабатываться только один раз, даже при повторной доставке.
  5. Мониторинг - логируйте шаги Saga, используйте tracing (OpenTelemetry) для отладки.

Для Node.js популярны: библиотека sagas (редко), самописные решения на основе очередей, или использование готовых платформ вроде Temporal (но это уже выход за рамки простого Node.js).

Пример кода

JAVASCRIPT
// Пример хореографической Saga с outbox pattern на Node.js
const { Pool } = require('pg');
const { Kafka } = require('kafkajs');
const pool = new Pool({ /* config */ });
const kafka = new Kafka({ clientId: 'order-service', brokers: ['localhost:9092'] });
const producer = kafka.producer();
async function createOrder(orderData) {
const client = await pool.connect();
try {
await client.query('BEGIN');
// 1. Сохраняем заказ
const orderResult = await client.query(
'INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id',
[orderData.userId, orderData.total]
);
const orderId = orderResult.rows[0].id;
// 2. Пишем событие в outbox (та же транзакция)
await client.query(
'INSERT INTO outbox (aggregate_id, event_type, payload) VALUES ($1, $2, $3)',
[orderId, 'OrderCreated', JSON.stringify({ orderId, userId: orderData.userId, total: orderData.total })]
);
await client.query('COMMIT');
// 3. Отправляем событие в Kafka (после коммита)
await producer.send({
topic: 'order-events',
messages: [{ key: orderId.toString(), value: JSON.stringify({ type: 'OrderCreated', orderId, userId: orderData.userId }) }]
});
return orderId;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}

Как отвечать на собеседовании

Начните с ключевой идеи: в распределённых системах нельзя использовать ACID-транзакции между агрегатами, поэтому применяем Saga и eventual consistency. Упомяните outbox pattern как способ гарантировать доставку событий без двухфазного коммита. Приведите пример из Node.js: как вы реализовывали Saga с PostgreSQL и Kafka. Подчеркните важность идемпотентности и мониторинга. Если спросят про альтернативы - скажите, что distributed transactions (XA) возможны, но их избегают из-за сложности и падения производительности.

Что проверяет интервьюер

  • Понимание ограничений ACID в микросервисах.
  • Знание паттернов Saga, outbox, eventual consistency.
  • Умение проектировать отказоустойчивые системы.
  • Практический опыт с Node.js и очередями.
  • Понимание trade-off между согласованностью и доступностью (CAP-теорема).

Типичные ошибки

  • Попытка использовать распределённые транзакции (XA) в Node.js - это антипаттерн для микросервисов.
  • Игнорирование идемпотентности - повторная обработка события ломает данные.
  • Отсутствие компенсаций в Saga - при сбое данные остаются в несогласованном состоянии.
  • Синхронные вызовы между сервисами вместо асинхронных событий - это убивает производительность и увеличивает связанность.
  • Забывают про outbox - если событие не записано в той же транзакции, что и агрегат, возможна потеря события при сбое.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью