Атомарность на уровне документа
Главная гарантия MongoDB, на которой держится почти всё: одна операция над одним документом — это всё или ничего.
Атомарность документа — свойство MongoDB, при котором любое изменение единичного документа (даже если оно затрагивает десятки вложенных полей и массивов) применяется целиком и неделимо: другой клиент никогда не увидит документ «на полпути».
Зачем это нужно на практике
В реляционных базах, чтобы согласованно изменить заказ и его позиции, вы открываете транзакцию, потому что строки лежат в разных таблицах. В MongoDB заказ вместе с позициями часто хранится в одном документе — и тогда обновление всего заказа целиком уже атомарно, без всякой транзакции. Это не мелкая деталь, а основа модели данных: правильно спроектировав документ, вы получаете целостность «бесплатно».
Разберём типичную ситуацию — счётчик лайков. Если читать значение в приложение, прибавлять единицу и писать обратно, два параллельных запроса затрут друг друга, и один лайк потеряется. MongoDB решает это операторами-модификаторами, которые выполняются на сервере атомарно.
// два клиента почти одновременно жмут «лайк»
db.posts.updateOne({ _id: 42 }, { $inc: { likes: 1 } })
db.posts.updateOne({ _id: 42 }, { $inc: { likes: 1 } })
// результат всегда +2, потому что $inc считается на сервере, а не в приложении
Вывод:
likes увеличится ровно на 2 — ни один инкремент не потеряется
Встраивание вместо транзакций
Самый частый совет в моделировании MongoDB: данные, которые меняются вместе и читаются вместе, держите в одном документе. Тогда «согласованная» правка нескольких сущностей превращается в одну атомарную операцию. Сравните два подхода к корзине покупок.
{
"_id": "cart_9",
"userId": "u_1",
"items": [
{ "sku": "A-100", "qty": 2, "price": 590 },
{ "sku": "B-200", "qty": 1, "price": 1490 }
],
"total": 2670
}
Добавить товар и пересчитать итог можно одним обновлением — корзина никогда не окажется в состоянии «товар добавлен, а сумма ещё старая», потому что обе правки попадают в один документ за одну операцию.
db.carts.updateOne(
{ _id: "cart_9" },
{
$push: { items: { sku: "C-300", qty: 1, price: 300 } },
$inc: { total: 300 }
}
)
Какие операторы атомарны
Атомарны все стандартные модификаторы внутри одной операции updateOne / findOneAndUpdate над одним документом.
| Оператор | Что делает атомарно |
$inc | прибавляет число (счётчики, остатки, баланс) |
$push / $pull | добавляет/удаляет элемент массива |
$set / $unset | задаёт/удаляет поле, в т.ч. вложенное |
$addToSet | добавляет в массив без дублей |
$min / $max | обновляет, только если значение меньше/больше |
Условные обновления и оптимистичная блокировка
Атомарность можно усилить условием прямо в фильтре. Классика — списать товар со склада, только если его хватает. Условие qty: { $gte: 1 } и сам декремент проверяются и применяются неделимо: если двое одновременно покупают последнюю единицу, выиграет ровно один.
const res = db.stock.updateOne(
{ sku: "A-100", qty: { $gte: 1 } },
{ $inc: { qty: -1 } }
)
// res.modifiedCount === 1 — успели; === 0 — товара уже нет
Такой приём называют оптимистичной блокировкой: вы не держите замок, а полагаетесь на то, что условие отсечёт «опоздавших». Поле-версию (version) добавляют, когда нужно гарантировать, что документ не изменился между чтением и записью.
Как это работает под капотом
В движке WiredTiger каждая запись документа проходит через его внутренний контроль конкуренции (MVCC): сервер сериализует конкурирующие изменения одного документа, поэтому со стороны клиента они выглядят как выполненные по очереди, а не вперемешку. Промежуточное состояние документа другим читателям не видно. Именно поэтому «прочитал — изменил — записал» в приложении опасно (между шагами вклинится чужая запись), а серверный модификатор вроде $inc — безопасен: чтение и запись происходят на сервере в одной атомарной единице.
Важная граница: атомарность распространяется на ОДИН документ. Как только операция трогает два документа (или две коллекции), без транзакции между этими правками может вклиниться чужое чтение — об этом следующие уроки.
Частые ошибки
- Read-modify-write в коде. Прочитать
likes, прибавить в приложении и записать — гонка. Используйте$inc. - Ожидать атомарности между документами. Два отдельных
updateOneпо разным документам атомарны каждый сам по себе, но не вместе. - Разносить тесно связанные данные по коллекциям без причины. Тогда теряется бесплатная целостность встраивания и приходится тянуть транзакции.
- Безоглядно раздувать документ. Встраивание хорошо до лимита 16 МБ и пока массив не растёт безгранично; иначе — отдельная коллекция и ссылки.
Итоги
- Любая операция над одним документом атомарна — целиком применяется или не применяется вовсе.
- Встраивание связанных данных превращает «согласованную правку» в одну атомарную операцию без транзакций.
$inc,$push,$setи другие модификаторы считаются на сервере и безопасны при конкуренции.- Условие в фильтре (
$gte, версия) даёт оптимистичную блокировку без явных замков. - Граница атомарности — один документ; за её пределами нужны транзакции.