Ответ
Обеспечить атомарную транзакцию между двумя разными СУБД (например, 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.
Ответ 18+ 🔞
Ну, вот смотри, история классическая, как будто на дворе 2002-й год. Хочешь атомарно обновить данные в двух разных базах — PostgreSQL и MongoDB — так, чтобы либо всё прошло, либо нихуя? Так вот, блядь, стандартными средствами это невозможно в принципе. Это как пытаться заставить двух котов слаженно тащить сосиску в разные стороны — нихуя не выйдет, только порвут. Сами базы координацию между собой не обеспечивают, им похуй друг на друга.
Поэтому в Node.js, да и вообще где угодно, приходится выкручиваться умом. Основной рабочий подход — это паттерн Saga, или, как я это называю, история с откатами. Смысл в чём: ты делаешь операцию в первой базе, потом во второй. Если на втором шаге всё пошло по пизде, ты запускаешь компенсирующую транзакцию (обратную операцию) для первой базы, чтобы откатить изменения. Полной атомарности нет, зато есть контроль над пиздецом.
Вот смотри, пример на пальцах — перевод денег между счетами в SQL и NoSQL:
// Псевдокод, иллюстрирующий логику
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. Это надёжнее: если сервис упал, сообщение дождётся его воскрешения. Иначе рискуешь получить хитрую жопу, когда транзакция зависла в непонятном состоянии.
- Библиотеки. Для сложных сценариев есть готовые фреймворки, которые эту всю муть абстрагируют. Но изучать их — это отдельная история, там овердохуища нюансов.
Есть ещё альтернатива — Outbox Pattern. Суть: ты в одной транзакции с основным действием в PostgreSQL пишешь ещё и запись в таблицу outbox (исходящие события). Потом отдельный процесс-воркер (типа CDC) эту таблицу мониторит, вычитывает события и гарантированно применяет их к MongoDB. Консистентность в итоге будет не мгновенная, а eventual (когда-нибудь сойдётся). Зато архитектурно иногда чище. Выбирай, что для твоей задачи меньше похоже на манду с ушами.