Гид по технологиям

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

8 min read Базы данных Обновлено 13 Dec 2025
ACID в MongoDB: транзакции с Mongoose и Node.js
ACID в MongoDB: транзакции с Mongoose и Node.js

Ноутбук с логотипами MongoDB и JavaScript

Зачем нужны транзакции в 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 пользователя.

Схема коллекций User и Job

Ниже показаны основные шаги: подключение, модели, создание пользователя, создание вакансии в транзакции.

Подготовка и подключение (необходимая конфигурация)

  • Настройте 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: реализуйте процесс удаления/экспортирования данных с учётом того, что транзакции кратковременны, но операции удаления должны должным образом логироваться.

Краткая методология внедрения транзакций

  1. Проанализируйте модель данных: можно ли сделать атомарной одну операцию на документе?
  2. Если нужны многодокументные операции — убедитесь в наличии реплика‑сета.
  3. Реализуйте и логируйте транзакции в dev‑окружении.
  4. Напишите сценарии отката и покрытие тестами.
  5. Перенесите в production и мониторьте показатели SLO/латентности.

Когда использовать withTransaction, а когда startTransaction

  • withTransaction: удобен для компактной логики и автоматического commit/abort.
  • startTransaction/commit/abort: нужен, если требуется межфункциональный контроль и дополнительные шаги вне колбэка.

Итог

Транзакции в MongoDB дают возможность обеспечить ACID для мультидокументных операций, но требуют корректной архитектуры: реплика‑сета, внимательного проектирования модели данных и тестирования. В большинстве случаев стоит стремиться к одно‑документной атомарности, а многодокументные транзакции применять взвешенно.

Важное: транзакции не решают всех проблем. Оценивайте влияние на производительность и рассматривайте альтернативы (денормализация, саги, event sourcing) при проектировании приложений.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Как уменьшить размер JPEG — быстрые способы
Руководства

Как уменьшить размер JPEG — быстрые способы

Установка MATE и возврат к GNOME 2 в Ubuntu
Linux

Установка MATE и возврат к GNOME 2 в Ubuntu

Папка Safe в Files — как защитить файлы
Android.

Папка Safe в Files — как защитить файлы

Twixtor на iPhone: плавное замедление и velocity
Видео

Twixtor на iPhone: плавное замедление и velocity

Как вернуть найденный потерянный телефон
Телефоны

Как вернуть найденный потерянный телефон

Как создать и управлять несколькими аккаунтами Instagram
Социальные сети

Как создать и управлять несколькими аккаунтами Instagram