MongoDB долго был синонимом базы без транзакций. Быстрый, гибкий, бессхемный, но без атомарных гарантий поверх нескольких документов. С четвёртой версии всё изменилось: появились многодокументные транзакции, а к седьмой и восьмой версиям они стали привычным инструментом со знакомым по реляционным базам синтаксисом. И тут многие команды попадают в ловушку. Получив привычный молоток, они начинают видеть вокруг одни гвозди, заворачивают в транзакции всё подряд и упираются в тормоза, конфликты записи и таймауты. После чего нередко возвращаются к модели, где данные согласуются не мгновенно. Разберём честно, где транзакции реально буксуют, а где претензии к ним это недопонимание.
Что такое транзакция в MongoDB и почему она вообще не бесплатна
Стоит сразу прояснить терминологию. В MongoDB любая операция над одним документом и так атомарна и обладает всеми свойствами надёжной транзакции. Можно одной операцией обновить несколько вложенных документов и элементов массива внутри одного документа, и MongoDB гарантирует полную изоляцию: при ошибке всё откатывается, и клиент видит согласованную картину. Когда говорят о транзакциях MongoDB, имеют в виду именно многодокументные, дающие атомарность поверх нескольких документов и коллекций через механизм сессий.
И вот они уже не бесплатны. Для отслеживания незафиксированных изменений и поддержания состояния транзакции расходуется дополнительная память. Изоляция строится на снимке данных через оптимистичное управление конкурентным доступом, и именно отсюда растут конфликты записи. Важно понимать архитектурное ограничение: транзакции работают только на репликасете или шардированном кластере. Одиночный сервер их не поддерживает, потому что механизм опирается на журнал операций репликации.
Откуда берётся миф о медленных транзакциях и где правда
Утверждение "транзакции в MongoDB медленные" звучит часто и обычно оказывается недопониманием. Реальность тоньше. Многодокументные транзакции не быстрее и не медленнее однодокументных операций сами по себе, всё зависит от устройства. Более того, они батчат изменения в меньшее число записей журнала операций, поэтому в ряде случаев дают меньшую задержку, чем россыпь отдельных однодокументных операций.
Можно было бы ждать большого выигрыша от такой групповой надёжности, но однодокументные операции уже сильно оптимизированы. MongoDB умеет упаковывать несколько вставок в одну запись журнала, а движок хранения батчит сбросы на диск, так что несколько транзакций делят один синхронизирующий вызов. При настройках по умолчанию база часто запускает сброс журнала, не дожидаясь его, опираясь на подтверждение репликации для гарантии надёжности.
Вывод инженеров MongoDB прямолинеен: используйте транзакции тогда, когда этого требуют границы атомарности вашего приложения, а не из страха или по привычке. Производительность зависит не от самого факта транзакции, а от того, лежат ли документы на одном шарде или транзакция растягивается на несколько.
Где транзакции действительно начинают буксовать
Теперь к настоящим узким местам, которых хватает. Первое и главное это межшардовые транзакции. Транзакция, затрагивающая несколько шардов, несёт заметно большую стоимость, потому что операции координируются между несколькими узлами по сети. Это уже не локальное действие, а распределённый протокол с сетевыми задержками на каждом шаге.
Второе узкое место это число вовлечённых коллекций. Каждая дополнительная коллекция в транзакции добавляет накладные расходы на координацию: MongoDB обязан поддерживать согласованность по всем, и это проседает по скорости. Третье это размер данных. Крупные документы раздувают использование памяти и замедляют фиксацию, особенно когда заблокировано несколько документов, что провоцирует откаты и борьбу за блокировки. Отдельно бьёт неиндексированный поиск: транзакция замедляется, если запросы внутри неё сканируют большие коллекции, поэтому поля в условиях фильтрации и обновления обязаны быть проиндексированы.
И четвёртое, самое коварное, это горячие документы. Если множество параллельных транзакций бьётся за один и тот же документ, начинается лавина конфликтов записи.
Что именно ломается при конфликте записи и как это лечится
Конфликт записи возникает, когда две одновременные транзакции пытаются изменить один документ. Движок хранения использует оптимистичное управление конкурентным доступом: он позволяет параллельные операции, но при обнаружении конфликта прерывает одну из них с ошибкой конфликта записи, у которой код 112.
MongoServerError: WriteConflict error: this operation conflicted with
another operation. Please retry your operation or multi-document transaction.
Code: 112
ErrorLabel: TransientTransactionError
Ключевую роль здесь играет метка временной ошибки транзакции. Она сообщает драйверу и приложению, что транзакцию безопасно повторить целиком с самого начала. Это фундамент правильной работы с транзакциями: их нужно проектировать так, чтобы их можно было перезапускать. Классический антипример это глобальный счётчик. Транзакция переводит деньги между счетами, но сперва инкрементирует единый счётчик всех операций. Каждая транзакция в системе пытается обновить этот счётчик, и множество из них наталкивается на конфликты и уходит в повтор. Один общий горячий документ способен превратить нагруженную систему в череду бесконечных перезапусков.
Правильная обработка строится на повторах с возрастающей задержкой. Современные драйверы предоставляют готовый помощник, который сам перезапускает тело транзакции при получении временной ошибки.
async function withTransaction(client, txnFunc, maxRetries = 5) {
let attempt = 0;
while (attempt < maxRetries) {
const session = client.startSession();
try {
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});
await txnFunc(session);
await session.commitTransaction();
return;
} catch (err) {
await session.abortTransaction();
const retryable = err.errorLabels && (
err.errorLabels.includes("TransientTransactionError") ||
err.errorLabels.includes("UnknownTransactionCommitResult")
);
if (!retryable) throw err;
attempt++;
const backoff = Math.pow(2, attempt) * 50;
await new Promise(r => setTimeout(r, backoff));
} finally {
session.endSession();
}
}
throw new Error(`Транзакция не удалась после ${maxRetries} попыток`);
}
Здесь важны две метки ошибок. Временная ошибка транзакции означает, что повторять нужно всю транзакцию с нуля, сюда же попадает конфликт записи. А метка неизвестного результата фиксации означает, что повторить нужно только саму фиксацию, потому что неясно, прошла она или нет. Разумная практика это не более трёх-пяти попыток, экспоненциальная задержка с небольшим случайным разбросом против эффекта толпы, а тело транзакции делают идемпотентным на случай, если фиксация прошла, но подтверждение до клиента не дошло.
Почему ограничение в шестьдесят секунд это не мелочь
У транзакций есть жёсткий лимит времени жизни, по умолчанию шестьдесят секунд. Транзакцию, которая работает дольше, MongoDB прерывает. Это не настройка, которую стоит бездумно крутить вверх, а сигнал к тому, чтобы дробить длинные операции на короткие транзакции. Истёкшая транзакция возвращает отдельную ошибку, и попытка обработать тысячи записей в одной транзакции почти гарантированно упрётся в этот предел.
// Поменять лимит можно, но обычно это лечение симптома, а не болезни
db.adminCommand({ setParameter: 1, transactionLifetimeLimitSeconds: 30 });
Сюда же примыкают и другие подводные камни. Создавать коллекцию внутри транзакции нельзя, эту операцию провести не получится, поэтому коллекции готовят заранее. Сессия может истечь, и тогда транзакция перестаёт быть валидной. Всё это подводит к одной мысли: транзакции хороши для коротких, сфокусированных, заранее подготовленных операций, а не для долгих пакетных перелопачиваний данных.
Почему многие осознанно возвращаются к отложенной согласованности
И вот ключевой разворот, который проделывают зрелые команды. Документация MongoDB прямо говорит: в большинстве случаев распределённая транзакция стоит дороже однодокументной записи, и наличие транзакций не должно подменять собой грамотное проектирование схемы. Для множества сценариев денормализованная модель со встроенными документами и массивами остаётся оптимальной. Иными словами, правильное моделирование данных минимизирует саму потребность в транзакциях.
Это и есть та развилка, на которой проекты возвращаются к отложенной согласованности. Если связанные сущности можно уложить в один документ, то операция над ними снова становится атомарной по своей природе, без всякой многодокументной транзакции и её накладных расходов. Перевод между счетами требует транзакции, потому что счета это разные сущности. А вот заказ вместе со своими позициями часто прекрасно живёт одним документом, и тогда транзакция не нужна вовсе.
Те, кто проектирует осознанно, перестраивают данные так, чтобы связанные сущности жили вместе, и тем самым убирают целые классы транзакций. Там, где разнести данные всё же приходится, а немедленная согласованность не критична, выбирают модель, в которой система приходит в согласие чуть позже, через фоновое выравнивание. Расплата за это в том, что короткое окно рассогласования становится допустимым, зато исчезают конфликты записи, межшардовая координация и борьба за горячие документы.
Какой стратегии придерживаться в реальном проекте
Транзакции в MongoDB это не зло и не панацея, а инструмент с узкой, но важной нишей. Они уместны там, где границы атомарности приложения честно требуют изменить несколько независимых сущностей разом и потеря согласованности недопустима даже на миг. Финансовые переводы, списание со склада с одновременной записью заказа, операции, где половинчатый результат означает порчу данных, это их законная территория.
Рабочий набор правил складывается из нескольких пунктов. Держать транзакции короткими и узкими по охвату, чтобы сократить блокировки и борьбу за ресурсы. Индексировать поля, по которым идёт поиск и обновление внутри транзакции. Избегать горячих документов, которые тянут к себе множество параллельных записей. Всегда оборачивать транзакции в логику повторов с экспоненциальной задержкой и не давать единичному конфликту записи уронить операцию насовсем. И, что важнее всего, сначала спрашивать себя, нельзя ли вообще обойтись без транзакции, переложив связанные данные в один документ.
Зрелость в работе с MongoDB наступает не тогда, когда осваиваешь синтаксис транзакций, а когда понимаешь, что лучшая транзакция это та, которой удалось избежать грамотной схемой. Тот, кто заворачивает в транзакции всё подряд, рано или поздно встречает таймаут на шестидесятой секунде или лавину конфликтов на горячем счётчике. А тот, кто моделирует данные под характер операций, получает и атомарность там, где она нужна, и скорость там, где транзакция была бы лишней обузой.