В прошлом уроке мы держали всё нужное в одном документе, и атомарность доставалась бесплатно. Но заказ лежит в orders, а остаток в stock, и списать книгу вместе с созданием заказа одной командой нельзя. В обзорном уроке ты уже видел withTransaction. Сегодня разберём транзакцию по шагам: что видят другие клиенты, пока она открыта, что происходит при конфликте двух транзакций и почему драйвер повторяет их сам.
Черновик и чистовик
Бухгалтер не правит сразу главную книгу. Он делает записи в черновике, сверяет, и только потом переносит всё в чистовик одним движением. Если в черновике нашлась ошибка, его просто выбрасывают, и главная книга не пострадала. Пока черновик не перенесён, коллеги видят главную книгу в прежнем виде.
Транзакция в MongoDB это такой черновик. Изменения копятся внутри неё, никто снаружи их не видит, а в конце они применяются все разом или не применяются вовсе. Всё, что ты знаешь про ACID из теории, здесь работает так же.
Все примеры урока сняты с настоящего replica set из трёх узлов, поэтому в приглашении mongosh у тебя будет rs0 [direct: primary] test>. На одиночном сервере транзакцию не начать, а песочница транзакций не умеет вовсе.
Транзакция вручную
Транзакция всегда живёт внутри сессии. В mongosh сессию открывают через соединение, а базу для операций берут из сессии, иначе команды пройдут мимо транзакции.
Положим на склад две книги и оформим заказ вручную, по шагам:
db.stock.insertMany([
{ _id: 7, title: 'Грокаем алгоритмы', qty: 3 },
{ _id: 8, title: 'Совершенный код', qty: 1 }
])
const session = db.getMongo().startSession()
const shop = session.getDatabase('test')
session.startTransaction()
shop.orders.insertOne({ _id: 6, customer_id: 3, created_at: new Date('2026-10-09'), status: 'new', items: [{ book_id: 7, title: 'Грокаем алгоритмы', qty: 1, price: 1290 }] })
shop.stock.updateOne({ _id: 7 }, { $inc: { qty: -1 } })
Команды ответили как обычно: insertedId: 6 и modifiedCount: 1. Теперь самое интересное. Спросим заказ дважды: изнутри транзакции и снаружи, через обычный db:
shop.orders.findOne({ _id: 6 }, { status: 1 })
db.orders.findOne({ _id: 6 }, { status: 1 })
{ _id: 6, status: 'new' }
null
Транзакция видит свои изменения, а весь остальной мир нет. Это изоляция: пока черновик не перенесён, коллеги смотрят в старую главную книгу. Фиксируем:
session.commitTransaction()
db.orders.findOne({ _id: 6 }, { status: 1 })
db.stock.findOne({ _id: 7 })
{ _id: 6, status: 'new' }
{ _id: 7, title: 'Грокаем алгоритмы', qty: 2 }
После commitTransaction заказ и списание появились одновременно. А теперь начнём второй заказ и передумаем:
session.startTransaction()
shop.orders.insertOne({ _id: 7, customer_id: 1, created_at: new Date('2026-10-09'), status: 'new', items: [] })
shop.stock.updateOne({ _id: 8 }, { $inc: { qty: -1 } })
session.abortTransaction()
db.orders.findOne({ _id: 7 })
db.stock.findOne({ _id: 8 })
session.endSession()
null
{ _id: 8, title: 'Совершенный код', qty: 1 }
abortTransaction выбросил черновик целиком: заказа нет, «Совершенный код» по-прежнему на складе.
урок из курса
Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.