Почему чтение сразу после записи может вернуть старое значение при использовании реплики базы данных?
Так происходит, когда запись подтверждена основной базой, а чтение направлено на реплику, которая ещё не успела применить изменения. При асинхронной репликации между фиксацией транзакции на основном узле и воспроизведением изменений на реплике существует временной разрыв.
Это не ошибка самой записи: запросы обращаются к узлам с разными состояниями данных. Чтобы обеспечить требуемую read-after-write consistency, нужно учитывать момент записи при выборе узла для последующего чтения.
Реплики появились как способ разгрузить основной узел базы данных, увеличить пропускную способность чтения и повысить доступность системы. Изменения обычно фиксируются на основном узле, после чего передаются на один или несколько узлов для воспроизведения.
Асинхронная схема часто выбирается из-за меньшей задержки записи и возможности работать при временной недоступности реплики. Цена этого решения — реплика может отставать от основного узла, поэтому разные запросы в один момент способны увидеть разные версии данных.
Пользователь изменяет адрес доставки, получает успешный ответ, а затем открывает страницу профиля. Запись уже зафиксирована на основном узле, но балансировщик направляет чтение на реплику, где изменение ещё не применено.
В результате пользователь видит старый адрес, повторно отправляет форму или считает операцию неуспешной. Для заказов, платежей, прав доступа и другого критичного состояния это может привести не только к путанице интерфейса, но и к некорректным бизнес-решениям.
Основной узел сначала фиксирует транзакцию. Затем изменения попадают в журнал или поток репликации, передаются реплике и применяются там отдельным процессом. Задержка возникает из-за сетевых задержек, очереди изменений, нагрузки на реплику или длительной транзакции, блокирующей применение.
Возможные стратегии:
Закрепление за основным узлом проще, но снижает эффективность распределения чтений и может создавать перегрузку. Ожидание конкретной позиции репликации точнее, однако требует поддержки со стороны хранилища и маршрутизации. Синхронная репликация уменьшает окно рассогласования, но увеличивает задержку записи и связывает доступность записи с доступностью подтверждающих узлов.
Важно различать read-after-write consistency для одного пользователя и глобальную строгую согласованность для всех читателей. Часто достаточно гарантировать, что автор операции некоторое время видит собственную запись, не делая все чтения в системе синхронными.
В сервисе управления заказами после смены статуса заказ сразу перечитывался для формирования ответа. Запись шла на основной узел, а чтение — через общий балансировщик на одну из реплик. При задержке репликации клиент иногда получал прежний статус и повторял действие.
Рассматривались три варианта. Постоянная маршрутизация всех запросов на основной узел давала простую гарантию, но лишала систему значительной части преимуществ реплик. Закрепление пользователя за основным узлом снижало вероятность ошибки, однако усложняло балансировку и не защищало фоновые запросы. Синхронная репликация обеспечивала более сильную гарантию, но увеличивала задержку подтверждения заказа.
Выбрали маршрутизацию чтения после изменения на основной узел на короткий период, а для фоновых сценариев — чтение с реплик с контролем их актуальности. Это соответствовало бизнес-требованию: автор заказа должен немедленно видеть новый статус, тогда как небольшая задержка для аналитических экранов была допустима. Число повторных операций уменьшилось без перевода всех чтений на основной узел.
Нет, она устраняет именно задержку подтверждения записи относительно подтверждённых реплик, если чтение выполняется с узлами, участвовавшими в требуемом подтверждении. Но остаются вопросы маршрутизации, отказов, кворума и того, какие узлы считаются допустимыми для чтения. Кроме того, синхронность повышает задержку и может сделать запись недоступной при потере необходимого узла.
Закрепление действует только там, где сохраняется информация о привязке. Другой сервис, фоновый обработчик или новый запрос после истечения привязки всё равно может прочитать реплику с устаревшим состоянием. Кроме того, такой подход ухудшает балансировку и может перегрузить основной узел при большом числе недавних записей.
Нужно сравнить требуемую позицию записи с позицией, до которой реплика уже применила изменения. Если реплика её достигла, чтение удовлетворяет требованию read-after-write; если нет, запрос следует отложить, направить на другой узел или вернуть с основного узла. Простая проверка времени последней синхронизации менее надёжна, потому что задержка может резко измениться, а временная метка не всегда точно описывает порядок применения конкретной записи.