Модели согласованности и retryable writes
Что именно увидит клиент в распределённом кластере: свои же записи, согласованную причинно-следственную картину — и где данные могут оказаться слегка устаревшими.
Причинная согласованность (causal consistency) — гарантия, что связанные операции наблюдаются в порядке их причинно-следственной связи: если запись B логически следует за чтением/записью A, ни один клиент в рамках сессии не увидит B раньше A.
Зачем на практике
В репликасете чтения можно разгрузить на вторичные узлы — это масштабирует нагрузку. Но вторичные отстают от primary на величину репликационного лага, и наивное чтение оттуда легко приводит к парадоксам: пользователь сохранил профиль, обновил страницу и не увидел своих изменений. Модели согласованности и retryable-механизмы дают предсказуемость без отказа от масштабирования.
Read your writes и причинная согласованность
Гарантия «read your writes» означает: клиент всегда видит результат собственной завершённой записи при последующем чтении. MongoDB обеспечивает это через каузально-согласованные сессии. Сессия переносит между операциями «отметку времени кластера»: следующее чтение ждёт, пока выбранный узел догонит как минимум до момента вашей записи.
const s = db.getMongo().startSession({ causalConsistency: true })
const users = s.getDatabase("app").users
// записали профиль с majority
users.updateOne({ _id: "u_1" }, { $set: { name: "Аня" } },
{ writeConcern: { w: "majority" } })
// в той же сессии гарантированно увидим своё изменение,
// даже если чтение уйдёт на вторичный узел
users.find({ _id: "u_1" }).readPref("secondaryPreferred")
Без каузальной сессии то же чтение с вторичного узла могло бы вернуть старое имя, попав на отставшую реплику. Каузальная согласованность гарантирует ещё и порядок: если вы прочитали комментарий, то ответ на него, написанный позже, не «опередит» сам комментарий в вашей картине мира.
Чтение со вторичных и устаревание
Read preference выбирает, с каких узлов читать. Это рычаг масштабирования, но у него есть цена — устаревшие данные (stale reads).
| readPreference | Поведение |
primary | только primary; самые свежие данные (по умолчанию) |
primaryPreferred | primary, а если недоступен — вторичные |
secondary | только вторичные; разгружает primary, но данные могут отставать |
secondaryPreferred | вторичные, при их отсутствии — primary |
nearest | узел с наименьшей задержкой сети (любой роли) |
Параметр maxStalenessSeconds ограничивает, насколько сильно вторичный узел может отставать, чтобы вообще участвовать в чтении. Это защита от выбора совсем «протухшей» реплики.
// аналитика терпит лёгкое отставание, но не больше 90 секунд
db.events.find({ type: "click" })
.readPref("secondary", [], { maxStalenessSeconds: 90 })
Правило простое: данные, для которых критична свежесть (баланс, статус заказа), читайте с primary или в каузальной сессии; аналитику и фоновые отчёты можно отдать вторичным.
Retryable writes и reads
Сеть в распределённой системе ненадёжна: запрос может не дойти или ответ — потеряться. Retryable writes позволяют драйверу один раз безопасно повторить запись, не создав дубликат.
Retryable write — запись, которую драйвер при сетевом сбое или смене primary автоматически повторяет ровно один раз; сервер по идентификатору операции распознаёт повтор и не применяет её дважды.
Идемпотентность обеспечивает сервер: каждой повторяемой записи драйвер присваивает уникальный идентификатор транзакции, и при повторе сервер видит, что операция уже применялась, и возвращает прежний результат вместо второго выполнения. Поэтому повтор updateOne или insertOne не задвоит данные. По умолчанию retryable writes включены в современных драйверах (retryWrites=true в строке подключения).
// строка подключения с включёнными повторами записей
mongodb://host1,host2,host3/app?replicaSet=rs0&retryWrites=true
Аналогично работают retryable reads: упавшее из-за сети чтение драйвер повторит автоматически (тоже включено по умолчанию). Замечание: повторяются единичные операции; для многошаговой логики через несколько документов нужен механизм транзакций с ретраями из урока про многодокументные транзакции.
Как это работает под капотом
Каузальная сессия хранит два «маяка»: последний наблюдённый operationTime (для чтений) и cluster time. Перед чтением драйвер сообщает узлу «не отвечай, пока не догонишь до этой отметки» — узел дожидается репликации до нужной позиции oplog. Для retryable writes сервер ведёт учёт идентификаторов сессии и номера транзакции: получив повтор с тем же номером, он находит сохранённый результат предыдущего применения и отдаёт его, гарантируя «ровно один раз» вместо «как минимум один раз».
Частые ошибки
- Чтение профиля сразу после записи с secondary без каузальной сессии. Классический «пропавший апдейт» из-за лага реплики.
- secondary для критичных к свежести данных. Баланс и статус заказа читайте с primary или в каузальной сессии.
- Считать, что retryable writes повторяют всё. Они повторяют одну операцию, а не вашу многошаговую бизнес-логику.
- Игнорировать maxStalenessSeconds. Без него чтение может попасть на сильно отставший узел.
- Путать причинную и сильную согласованность. Causal гарантирует порядок связанных операций в сессии, но не «всегда самое последнее значение глобально».
Итоги
- Каузально-согласованные сессии дают read-your-writes и сохраняют порядок связанных операций даже при чтении с реплик.
- Read preference масштабирует чтения через вторичные узлы ценой возможного устаревания данных.
maxStalenessSecondsотсекает слишком отставшие реплики; свежесть-критичное читайте с primary.- Retryable writes/reads безопасно повторяют единичную операцию при сетевом сбое и не создают дублей.
- Причинная согласованность — это про порядок связанных событий в сессии, а не про глобально «самое последнее» значение.