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

Read concern и Write concern

junior~20 мин

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

В прошлом уроке транзакция гарантировала, что заказ и списание применятся вместе. Но на replica set данные лежат на нескольких узлах, и «применилось» может значить разное: на одном сервере, на большинстве или на всех. Сегодня разберём две настройки, которыми ты выбираешь эту степень уверенности: write concern для записи и read concern для чтения. Все примеры сняты с настоящего replica set из трёх узлов, в песочнице этих настроек нет.

Заказное письмо

Обычное письмо почтальон бросил в ящик, и ты не знаешь, дошло ли оно. Заказное письмо вручают под подпись. А важный договор отправляют с уведомлением о вручении и копией у нотариуса. Чем больше подтверждений ты требуешь, тем дольше ждёшь, но тем меньше шансов потерять документ.

Write concern это именно такой выбор для записи: какого подтверждения ждать, прежде чем считать запись сделанной.

Три уровня подтверждения

Начнём с того, что сервер делает по умолчанию:

db.adminCommand({ getDefaultRWConcern: 1 })
{
  defaultReadConcern: { level: 'local' },
  defaultWriteConcern: { w: 'majority', wtimeout: 0 },
  defaultWriteConcernSource: 'implicit',
  defaultReadConcernSource: 'implicit',
  ...
}

Ответ сокращён, служебные поля мы убрали. По умолчанию запись ждёт большинства узлов, а чтение берёт локальные данные. Вот основные варианты write concern:

ЗначениеЖдём подтвержденияЧто может потеряться
w: 0ничего не ждёмвсё, мы даже не узнаем об ошибке
w: 1первичный узел принял записьзапись, если первичный упадёт до репликации
w: 'majority'большинство узловпочти ничего, запись переживёт выборы
j: trueзапись в журнале на дискедобавка к любому w: переживёт перезапуск узла

Задаётся он прямо в команде:

db.orders.insertOne(
  { _id: 6, customer_id: 3, created_at: new Date('2026-10-09'), status: 'new', items: [] },
  { writeConcern: { w: 'majority', j: true } }
)
{ acknowledged: true, insertedId: 6 }

Снаружи ответ ничем не отличается от обычного. Разница во времени ожидания и в том, что именно обещает этот ответ.

Ошибка, после которой запись осталась

Самое коварное в write concern то, как выглядят его ошибки. Попросим подтверждения от пяти узлов, хотя их всего три:

db.orders.insertOne({ _id: 7, customer_id: 1, status: 'new', items: [] }, { writeConcern: { w: 5 } })
Uncaught:
MongoWriteConcernError[UnsatisfiableWriteConcern]: Not enough data-bearing nodes
Additional information: { writeConcern: { w: 5, wtimeout: 0, provenance: 'clientSupplied' } }
Result: {
  n: 1,
  ...
}

Посмотри на n: 1 в результате. Запись прошла, просто сервер не смог дать обещанное подтверждение. И правда:

db.orders.findOne({ _id: 7 })
{ _id: 7, customer_id: 1, status: 'new', items: [] }

Это главное правило: ошибка write concern не значит, что записи нет. Она значит «не могу подтвердить, что запись надёжна». Приложение, которое в ответ на такую ошибку просто повторит вставку, получит дубль.

Теперь ситуация из жизни. Остановим один из трёх узлов и попросим подтверждения от большинства и от всех трёх, с ограничением ожидания в две секунды через wtimeout:

db.orders.insertOne({ _id: 8, customer_id: 2, status: 'new', items: [] }, { writeConcern: { w: 'majority', wtimeout: 2000 } })
db.orders.insertOne({ _id: 9, customer_id: 2, status: 'new', items: [] }, { writeConcern: { w: 3, wtimeout: 2000 } })
{ acknowledged: true, insertedId: 8 }
Uncaught:
MongoWriteConcernError[WriteConcernTimeout]: waiting for replication timed out
Additional information: {
  wtimeout: true,
  writeConcern: { w: 3, wtimeout: 2000, provenance: 'clientSupplied' }
}
Result: {
  n: 1,
  ...
}

Большинство это два узла из трёх, они живы, поэтому первая запись подтверждена сразу. Третий узел лежит, и вторая запись ждала его две секунды, а потом сдалась. Документ _id: 9 при этом всё равно в базе. Без wtimeout вторая команда ждала бы вечно, поэтому w больше единицы всегда пиши вместе с ограничением времени.

урок из курса

Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

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