ACID-транзакции в MongoDB с Mongoose
Этот материал объясняет, как обеспечить свойства ACID (атомарность, согласованность, изоляция, надёжность) в MongoDB при помощи Mongoose и Node.js. Вы узнаете требования к окружению (реплика-сет или MongoDB Atlas), два способа реализации транзакций (ручное управление commit/abort и session.withTransaction), типичные ошибки и методику тестирования. Приведены примеры кода, чек‑листы для ролей и схема принятия решения.
Зачем нужны транзакции в MongoDB
Транзакция — это набор операций с базой данных, которые должны выполниться как единое целое. Если одна операция из набора не проходит, все изменения откатываются. Это необходимо, когда вы обновляете несколько документов/коллекций и хотите сохранить целостность данных.
Кратко о терминах (одна строка):
- Транзакция — единица работы, выполняемая атомарно.
- Сессия — контекст, в котором выполняется транзакция.
ACID — что означают свойства
- Атомарность: все операции в транзакции выполняются полностью или не выполняются вовсе.
- Согласованность: после транзакции данные остаются в корректном состоянии согласно бизнес-правилам.
- Изоляция: параллельные транзакции не вмешиваются друг в друга так, чтобы нарушить целостность.
- Надёжность (durability): после фиксации (commit) данные сохраняются даже при сбоях.
Важно: MongoDB реализует транзакции поверх механизма репликации, поэтому транзакции доступны не на standalone‑инстансе.
Требования и совместимость
- Транзакции для многодокументных операций доступны в MongoDB начиная с версии 4.0 для replica set.
- Поддержка транзакций в шарди́рованных кластерах появилась в MongoDB 4.2.
- Транзакции не работают на одиночных (standalone) серверах.
- Проще всего получить подходящее окружение через MongoDB Atlas (по умолчанию replica set или sharded cluster).
Важно: не нужно придумывать механизм транзакций поверх неподдерживаемого окружения — это небезопасно.
Сценарий и схема данных для примера
Рассмотрим простое приложение для вакансий, где пользователь может создавать объявления о работе. Нам нужно создать вакансию и добавить ссылку на неё в массив объявлений автора — это классическая задача для транзакции.
Схема (идея):
- users: { name, email, jobs: [ObjectId] }
- jobs: { title, location, salary, poster: ObjectId }
Подключение Mongoose (пример)
import mongoose from 'mongoose'
let MONGO_URL = process.env.MONGO_URL || 'your-mongo-database-url';
let connection;
const connectDb = async () => {
try {
await mongoose.connect(MONGO_URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log("CONNECTED TO DATABASE");
connection = mongoose.connection;
} catch (err) {
console.error("DATABASE CONNECTION FAILED!");
console.error(err.message);
process.exit(1); // закрываем приложение при ошибке соединения
}
};Сохраняем объект соединения в переменной connection — он понадобится для запуска сессий транзакции.
Схемы коллекций и создание пользователя
const userSchema = new mongoose.Schema({
name: String,
email: String,
jobs: [mongoose.Schema.Types.ObjectId]
});
const jobSchema = new mongoose.Schema({
title: String,
location: String,
salary: String,
poster: mongoose.Schema.Types.ObjectId
});
const userCollection = mongoose.model('user', userSchema);
const jobCollection = mongoose.model('job', jobSchema);Функция для создания пользователя:
const createUser = async (user) => {
const newUser = await userCollection.create(user);
console.log("User added to database");
console.log(newUser);
}Вариант 1 — ручное управление транзакцией (commit/abort)
Ниже пример функции, которая создаст вакансию и добавит её в список вакансий пользователя в одной транзакции.
const createJob = async (job) => {
const { userEmail, title, location, salary } = job;
// получаем пользователя из БД
const user = await userCollection.findOne({ email: userEmail });
// начинаем сеанс транзакции
const session = await connection.startSession();
// выполняем все операции в try-catch
try {
await session.startTransaction();
// создаём вакансию
const newJob = await jobCollection.create(
[
{
title,
location,
salary,
poster: user._id,
},
],
{ session }
);
console.log("Created new job successfully!");
console.log(newJob[0]);
// добавляем id вакансии в список пользователя
const newJobId = newJob[0]._id;
const addedToUser = await userCollection.findByIdAndUpdate(
user._id,
{ $addToSet: { jobs: newJobId } },
{ session }
);
console.log("Successfully added job to user's jobs list");
console.log(addedToUser);
await session.commitTransaction();
console.log("Successfully carried out DB transaction");
} catch (e) {
console.error(e);
console.log("Failed to complete database operations");
await session.abortTransaction();
} finally {
await session.endSession();
console.log("Ended transaction session");
}
};Заметка: create() внутри транзакции часто возвращает массив созданных документов, поэтому мы берём newJob[0]._id.
Вариант 2 — использование session.withTransaction
Метод withTransaction упрощает код: вы передаёте callback, и MongoDB сам выполнит commit/abort в зависимости от результата.
const createJob = async (job) => {
const { userEmail, title, location, salary } = job;
// получаем пользователя из БД
const user = await userCollection.findOne({ email: userEmail });
// начинаем сеанс транзакции
const session = await connection.startSession();
// выполняем запросы внутри withTransaction
try {
const transactionSuccess = await session.withTransaction(async () => {
const newJob = await jobCollection.create(
[
{
title,
location,
salary,
poster: user._id,
},
],
{ session }
);
console.log("Created new job successfully!");
console.log(newJob[0]);
// добавляем id вакансии в список пользователя
const newJobId = newJob[0]._id;
const addedToUser = await userCollection.findByIdAndUpdate(
user._id,
{ $addToSet: { jobs: newJobId } },
{ session }
);
console.log("Successfully added job to user's jobs list");
console.log(addedToUser);
});
if (transactionSuccess) {
console.log("Successfully carried out DB transaction");
} else {
console.log("Transaction failed");
}
} catch (e) {
console.error(e);
console.log("Failed to complete database operations");
} finally {
await session.endSession();
console.log("Ended transaction session");
}
};withTransaction сам вызывает commit или abort, если callback бросил исключение или вернул неудачный результат.
Демонстрация запуска
const mockUser = {
name: "Timmy Omolana",
email: "jobposter@example.com",
};
const mockJob = {
title: "Sales Manager",
location: "Lagos, Nigeria",
salary: "$40,000",
userEmail: "jobposter@example.com", // email созданного пользователя
};
const startServer = async () => {
await connectDb();
await createUser(mockUser);
await createJob(mockJob);
};
startServer()
.then()
.catch((err) => console.log(err));Когда транзакции подойдут, а когда нет
Подходящая ситуация:
- Множественные связанные изменения в разных коллекциях, которые должны быть атомарными.
- Критичные финансовые операции, перевод баланса между кошельками и т. п.
Когда транзакции не нужны или вредны:
- Простые операции над одним документом, где достаточно атомарных операций MongoDB (updateOne, $addToSet и т. п.).
- Высоконагруженные пути, где накладные расходы транзакций ухудшат производительность.
- Когда окружение — одиночный MongoDB (standalone): транзакции не поддерживаются.
Совет: чаще всего разумнее моделировать данные так, чтобы минимизировать количество междокументных транзакций.
Ментальные модели и эвристики
- «Минимизируй область действия транзакции»: выполняйте только те операции, которые требуют атомарности.
- «Сессия — это контейнер»: храните всю логику изменения данных внутри сессии.
- «Предполагаем худшее»: всегда обрабатывайте ошибки и корректно закрывайте сессию (finally → endSession()).
Мини‑методология внедрения транзакций
- Оцените, действительно ли нужна транзакция.
- Подготовьте окружение (реплика‑сет/Atlas, версия MongoDB >= 4.0).
- Напишите unit и интеграционные тесты, которые проверяют откат при ошибке.
- Внедрите логирование шагов транзакции.
- Нагрузочное тестирование, чтобы понять влияние на latency.
- Мониторинг отказов и лагов репликации.
Чек‑листы по ролям
Разработчик:
- Проверил, что операция изменяет несколько документов.
- Использует session и передаёт { session } во все запросы.
- Обрабатывает исключения и всегда завершает сессию.
DevOps:
- Окружение — replica set или sharded cluster.
- MongoDB версии не ниже 4.0 (реплика‑сет), 4.2 для шардинга.
- Настроен мониторинг репликации и задержек.
QA:
- Есть тесты на откат транзакции при исключении.
- Есть интеграционные сценарии с реальной БД (staging).
- Нагрузочное тестирование витального пути.
Критерии приёмки
- Транзакция успешно коммитится при нормальном выполнении всех операций.
- При ошибке хотя бы одной операции все изменения откатываются.
- Сессия всегда завершается (session.endSession вызывается в finally).
- Логирование содержит шаги: startTransaction, commit/abort, endSession.
Тесты и кейсы приёма
- Создать вакансию и убедиться, что запись появилась в jobs и id добавлен в users.jobs.
- Симулировать ошибку в середине транзакции (например, бросить исключение) и проверить, что вакансия не создана и в users.jobs нет ссылки.
- Проверить поведение при потере соединения с первичным узлом реплика‑сета.
Диаграмма принятия решения
flowchart TD
A[Нужно ли изменить несколько коллекций?] -->|Нет| B[Не используем транзакцию]
A -->|Да| C[Работает ли окружение как replica set или sharded cluster?]
C -->|Нет| D[Сначала перейти на подходящее окружение]
C -->|Да| E[Используем транзакции]
E --> F{Нужен fine-grained control?}
F -->|Да| G[Ручное startTransaction/commit/abort]
F -->|Нет| H[Используем session.withTransaction]Альтернативные подходы
- Сведение данных в один документ, чтобы избежать междокументных транзакций (denormalization).
- Саги (saga pattern) для распределённых транзакций: последовательность компенсирующих операций.
- Optimistic concurrency control (версионирование документов) для конфликтов при параллельных обновлениях.
Безопасность и конфиденциальность
- Логи транзакций не должны содержать PII в открытом виде. Используйте маскирование в логах.
- Если вы храните персональные данные, проверьте соответствие требованиям защиты данных (например, GDPR) при репликации и бэкапах.
Замечания по производительности
Транзакции увеличивают задержку (latency) и нагрузку на сеть/репликацию — тестируйте критичные пути. Для редко выполняемых критичных операций это приемлемо; для массовых операций лучше оптимизировать модель данных.
Резюме
- Транзакции в MongoDB доступны с версии 4.0 (реплика‑сет) и 4.2 (шардинг).
- Используйте session.startTransaction/commit/abort или session.withTransaction в зависимости от потребности в гибком управлении.
- Всегда проверяйте окружение, покрывайте сценарии тестами и контролируйте производительность.
Важно
Перед внедрением транзакций проанализируйте, нельзя ли переработать модель данных так, чтобы уменьшить число междокументных операций.
Полезные шаги для дальнейших исследований: реализуйте небольшое приложение (например, финтех‑кошелёк или блог) и протестируйте сценарии rollback при ошибках и при failover реплика‑сета.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone