ACID транзакции в MongoDB и Mongoose для Node.js

Зачем нужны транзакции в MongoDB
Создание промышленного веб‑приложения требует гарантии целостности и согласованности данных. Транзакция — это набор операций, которые либо выполняются все вместе, либо не выполняются вовсе. В реляционных СУБД термин ACID (atomicity, consistency, isolation, durability) привычен. В MongoDB с документной моделью ACID не применим ко всем операциям по‑умолчанию, но MongoDB поддерживает многодокументные транзакции при определённой конфигурации кластера.
Важно: транзакции в MongoDB работают только на реплика‑сете или в шардинговом кластере. На standalone-инстансе они недоступны.
Что такое транзакция в базе данных
Транзакция — это последовательность запросов к базе, которые должны выполниться как единое целое для завершения одной логической задачи. Если любая операция из набора терпит неудачу, все изменения откатываются.
Свойства ACID простыми словами
- Atomicity — атомарность: либо все операции выполнены, либо ни одной.
- Consistency — согласованность: состояние данных остаётся корректным до и после транзакции.
- Isolation — изоляция: параллельные транзакции не мешают друг другу и результат каждой соответствует сериализуемой последовательности.
- Durability — долговечность: после фиксации (commit) изменения переживут сбои сервера.
Требования и версии
- MongoDB: многодокументные транзакции появились в MongoDB 4.0 для реплика‑сета и в 4.2 для шардинга.
- Развертывание: используйте реплика‑сет или MongoDB Atlas (по умолчанию реплика‑сет/шард).
- Клиент: Node.js + Mongoose (любая стабильная версия, поддерживающая сессии).
Пример: сценарий и схема коллекций
Рассмотрим простое приложение с вакансиями: пользователь может создать вакансию, и её id должен добавиться в список jobs пользователя.
Ниже показаны основные шаги: подключение, модели, создание пользователя, создание вакансии в транзакции.
Подготовка и подключение (необходимая конфигурация)
- Настройте MongoDB как реплика‑сет или используйте Atlas.
- Убедитесь, что в строке подключения указаны все реплики и опции репликации при необходимости.
- В приложении сохраните экземпляр соединения, чтобы запускать сессии:
`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); // close the app if database connection fails
}
};
`
Сохраните connection в переменной модуля — он понадобится для startSession().
Модели коллекций
(Оставляем схемы простыми для примера)
`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);
}
`
Реализация транзакции через startSession и commit/abort
Ниже — подробный пример создания вакансии и добавления её id в профиль пользователя в одной транзакции.
`
const createJob = async (job) => {
const { userEmail, title, location, salary } = job;
// get the user from the DB
const user = await userCollection.findOne({ email: userEmail });
// start transaction session
const session = await connection.startSession();
// run all database queries in a try-catch block
try {
await session.startTransaction();
// create job
const newJob = await jobCollection.create(
[
{
title,
location,
salary,
poster: user._id,
},
],
{ session }
);
console.log("Created new job successfully!");
console.log(newJob[0]);
// add job to users list of posted jobs
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.
Альтернативный стиль: withTransaction
withTransaction упрощает жизнь: вы передаёте колбэк, внутри него запускаете операции с опцией { session }. MongoDB автоматически выполнит commit или abort.
`const createJob = async (job) => {
const { userEmail, title, location, salary } = job;
// get the user from the DB
const user = await userCollection.findOne({ email: userEmail });
// start transaction session
const session = await connection.startSession();
// run all database queries in a try-catch block
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]);
// add job to users list of posted jobs
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 удобен, когда логика помещается в одну функцию‑колбэк. startTransaction/commit/abort дают более явный контроль, если нужно выполнять дополнительные шаги вне колбэка.
Демонстрация запуска
`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 of the created user
};
const startServer = async () => {
await connectDb();
await createUser(mockUser);
await createJob(mockJob);
};
startServer()
.then()
.catch((err) => console.log(err));
`
Когда транзакции не решают проблему
- Высокая нагрузка и долгие транзакции: транзакция удерживает ресурсы и может вызвать конкуренцию/ретраи.
- Небольшие операции: если можно хранить связанную информацию в одном документе — это предпочтительнее (atomic single‑document).
- Огромные документы или большие массивы внутри документов: обновляйте аккуратно, чтобы не превышать лимиты документа.
Альтернативы и паттерны проектирования
- Денормализация: дублируйте небольшие поля для уменьшения количества операций.
- Изменение модели данных: вложенные документы для атомарных обновлений.
- Компенсирующие транзакции (sagas): используйте в распределённых системах вместо долгих транзакций.
- Event sourcing / CQRS: для сложных бизнес‑процессов с историей изменений.
Практические эвристики и правила
- Держите транзакции короткими: меньше операций — меньше вероятность конфликтов.
- Проверяйте наличие пользователя до открытия транзакции, если можно; или блокируйте актуальность данных в транзакции.
- Логируйте идентификаторы сессий и транзакций для отладки.
- Не полагайтесь на транзакции для управления внешними системами (например, отправка писем) — такие операции делайте после commit и с обработкой ошибок.
Факто‑бокс
- Минимальные требования: MongoDB 4.0+ (реплика‑сет), MongoDB 4.2+ (шардинг).
- two styles: startTransaction + commit/abort vs session.withTransaction.
- create() внутри транзакции возвращает массив доков.
Чеклисты по ролям
Разработчик:
- Убедиться, что код использует { session } в каждом запросе внутри транзакции.
- Ограничить время транзакции и число операций.
- Покрыть тестами сценарии успешного commit и отката.
DevOps / SRE:
- Развернуть реплика‑сет или использовать Atlas.
- Настроить мониторинг задержек и конфликтов транзакций.
- Обеспечить резервное копирование и планы восстановления.
QA:
- Проверить поведение при сбое мастера/реплики.
- Симулировать конфликтующие транзакции.
- Проверить согласованность данных после rollback.
Критерии приёмки
- Транзакции корректно фиксируют все изменения при нормальном ходе.
- При ошибке все изменения откатываются и данные остаются в согласованном состоянии.
- Логи содержат информацию об id сессии/транзакции для отладки.
- Нагрузочные тесты показывают приемлемое время ответа под ожидаемой нагрузкой.
Тесты и приемочные сценарии
- Успешная транзакция: создаём пользователя и вакансию, проверяем, что job id добавлен в user.jobs.
- Откат транзакции: симулируем исключение после создания вакансии — вакансия не должна быть в коллекции или в списке пользователя.
- Параллельные транзакции: два клиента одновременно пытаются добавить одну и ту же вакансию — проверить изоляцию и отсутствие дублирования.
Безопасность и соответствие
- Не храните чувствительные данные в явном виде — шифруйте или применяйте токенизацию.
- Транзакции не заменяют политики доступа и аудит.
- Для GDPR: реализуйте процесс удаления/экспортирования данных с учётом того, что транзакции кратковременны, но операции удаления должны должным образом логироваться.
Краткая методология внедрения транзакций
- Проанализируйте модель данных: можно ли сделать атомарной одну операцию на документе?
- Если нужны многодокументные операции — убедитесь в наличии реплика‑сета.
- Реализуйте и логируйте транзакции в dev‑окружении.
- Напишите сценарии отката и покрытие тестами.
- Перенесите в production и мониторьте показатели SLO/латентности.
Когда использовать withTransaction, а когда startTransaction
- withTransaction: удобен для компактной логики и автоматического commit/abort.
- startTransaction/commit/abort: нужен, если требуется межфункциональный контроль и дополнительные шаги вне колбэка.
Итог
Транзакции в MongoDB дают возможность обеспечить ACID для мультидокументных операций, но требуют корректной архитектуры: реплика‑сета, внимательного проектирования модели данных и тестирования. В большинстве случаев стоит стремиться к одно‑документной атомарности, а многодокументные транзакции применять взвешенно.
Важное: транзакции не решают всех проблем. Оценивайте влияние на производительность и рассматривайте альтернативы (денормализация, саги, event sourcing) при проектировании приложений.
Похожие материалы
Как уменьшить размер JPEG — быстрые способы
Установка MATE и возврат к GNOME 2 в Ubuntu
Папка Safe в Files — как защитить файлы
Twixtor на iPhone: плавное замедление и velocity
Как вернуть найденный потерянный телефон