Раздел 36 · MongoDB с нуля

Многодокументные транзакции

junior~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В прошлом уроке мы держали всё нужное в одном документе, и атомарность доставалась бесплатно. Но заказ лежит в 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 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.