Ответ
Обеспечить атомарную транзакцию между двумя разными СУБД (например, PostgreSQL и MongoDB) стандартными средствами невозможно, так как это требует координации, которую не предоставляют сами БД. В Node.js для решения этой проблемы применяются компенсирующие транзакции (Saga) и другие паттерны.
Паттерн Saga (наиболее распространённый подход): Saga — это последовательность локальных транзакций в каждой БД. Каждая следующая транзакция запускается после успешного завершения предыдущей. Если какая-то транзакция падает, выполняются компенсирующие действия (компенсирующие транзакции) для отката уже выполненных шагов.
Пример реализации Saga для перевода денег между счетами в разных БД:
// Псевдокод, иллюстрирующий логику
async function transferMoney(accountFromId, accountToId, amount) {
const transactionId = generateId();
const sagaLog = []; // Для идемпотентности и отслеживания
try {
// 1. Шаг 1: Списать деньги в первой БД (SQL)
await sqlDb.transaction(async (t) => {
await t.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, accountFromId]);
sagaLog.push({ step: 'debit', txId: transactionId, status: 'completed' });
});
// 2. Шаг 2: Зачислить деньги во второй БД (NoSQL)
await mongoDb.withSession(async (session) => {
session.startTransaction();
await mongoDb.collection('accounts').updateOne(
{ _id: accountToId },
{ $inc: { balance: amount } },
{ session }
);
await session.commitTransaction();
sagaLog.push({ step: 'credit', txId: transactionId, status: 'completed' });
});
// 3. Финализация
await sagaLog.markAsCompleted(transactionId);
} catch (error) {
console.error('Saga failed:', error);
// Запуск компенсирующих транзакций
await runCompensation(sagaLog, transactionId);
// Например, вернуть деньги на первый счёт
await sqlDb.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, accountFromId]);
throw new Error('Transfer failed, rollback completed');
}
}
Ключевые моменты и инструменты:
- Идемпотентность: Каждый шаг и компенсация должны быть идемпотентными (повторный вызов не должен ломать систему). Достигается через запись состояния шага (например, в отдельную таблицу
saga_log). - Отслеживание состояния: Нужно хранить состояние всей саги, чтобы иметь возможность восстановиться после сбоя.
- Асинхронная коммуникация: Часто шаги саги связываются через сообщения в брокере (Kafka, RabbitMQ), что делает процесс более отказоустойчивым.
- Библиотеки: Для сложных сценариев можно рассмотреть специализированные решения или фреймворки, реализующие паттерн Saga.
Альтернативный подход — Outbox Pattern: Изменения в первой БД и сообщение для второй БД записываются в одной транзакции (в таблицу outbox). Отдельный процесс (например, CDC) читает outbox и гарантированно доставляет сообщение и применяет изменение ко второй БД. Это обеспечивает eventual consistency.