Согласованность в системе: что проверять на ревью
Сервис может вернуть старое состояние, дважды списать деньги или потерять событие даже при исправном коде. На ревью нужно проверять не отдельные транзакции, а инварианты, границы записи и поведение системы при сбоях.

Сервис может вернуть старое состояние, дважды списать деньги или потерять событие даже при исправном коде. На ревью нужно проверять не отдельные транзакции, а инварианты, границы записи и поведение системы при сбоях. Ниже — практический чек-лист для распределённых систем с реляционной базой, очередью сообщений, кешем и несколькими экземплярами приложения.
Начинайте не с базы, а с инвариантов
Согласованность — не требование «все данные должны обновляться одновременно». Для разных частей системы допустима разная задержка. Нельзя лишь нарушать правила, от которых зависит бизнес.
Сначала зафиксируйте инварианты в форме проверяемых утверждений:
- остаток товара не становится отрицательным;
- один платёж не переводится в состояние «успешно» дважды;
- заказ не получает статус «доставлен» раньше статуса «оплачен»;
- событие о создании заказа не публикуется до фиксации самого заказа;
- повторная доставка одного сообщения не меняет итоговый результат.
Расплывчатая формулировка «данные должны быть актуальными» для ревью бесполезна. Замените её на допустимое отставание, порядок переходов и действие при конфликте.
Хорошее ревью согласованности отвечает на вопрос не «где стоит транзакция», а «что произойдёт, если следующий шаг выполнится дважды, не выполнится вовсе или придёт раньше предыдущего».
Инвариант[1] — условие, которое система должна сохранять после каждой завершённой операции, включая повторную обработку и восстановление после сбоя.
Пройдите путь данных целиком
Ошибка часто возникает не внутри одного запроса, а на стыке компонентов. На ревью нарисуйте путь изменения: HTTP-запрос, запись в базу, публикация события, обработчик, кеш и чтение клиентом. Для каждого перехода задайте четыре вопроса.
- Где появляется источник истины? У заказа, платежа и профиля должен быть понятный владелец состояния. Если один и тот же статус меняют база, кеш и обработчик очереди, конфликт почти неизбежен.
- Что фиксируется атомарно? Проверьте, какие записи входят в одну транзакцию. Если заказ сохраняется отдельно от резервирования товара, между двумя операциями есть окно сбоя.
- Как передаётся изменение? Уточните, может ли событие потеряться после коммита базы и до отправки сообщения. Простая последовательность «записали строку, затем отправили событие» этого не решает.
- Что видит читатель? Чтение с реплики, из кеша или через отдельный поисковый индекс может вернуть старое состояние. Это допустимо только с явным контрактом.
Если ревьюер не может по схеме ответить, кто меняет поле и кто имеет право отклонить переход, система уже слишком неявная для безопасных изменений.
Что проверять в записи
Границы транзакции
Транзакция должна охватывать минимальный набор изменений, который обязан быть целостным. Слишком короткая транзакция оставляет промежуточное состояние. Слишком длинная удерживает блокировки, повышает вероятность дедлоков и ухудшает пропускную способность.
Особенно внимательно смотрите на код между операциями базы: сетевые вызовы, запросы к платёжному провайдеру и публикацию сообщений. Внешний сервис не становится частью транзакции базы только потому, что вызван внутри её блока.
Уровень изоляции и конкуренция
Проверьте, какие параллельные операции могут читать и менять одну строку. В PostgreSQL 16 уровни изоляции и их эффекты нужно сверять с документацией, а не выводить из названия транзакции. Ошибка «прочитали остаток, потом уменьшили» требует отдельного решения: блокировки, атомарного обновления с условием или иной модели.
На ревью должны появиться ответы на вопросы:
- что произойдёт при двух одновременных списаниях;
- может ли операция использовать устаревшее чтение;
- кто повторяет транзакцию после конфликта;
- как ограничено число повторов и как обрабатывается дедлок;
- какая ошибка считается безопасной для повторного запуска.
Повторы, сообщения и идемпотентность
Очередь обычно гарантирует не «ровно один раз», а доставку с возможным повтором или потерей в зависимости от настроек и сценария сбоя. Поэтому обработчик нужно оценивать вместе с механизмом подтверждения сообщения и записью результата.
Идемпотентный обработчик[2] после повторного запуска оставляет тот же бизнес-результат. Для заказа это может быть уникальный идентификатор операции в таблице, проверка уже применённого события или условный переход статуса.
На ревью ищите конкретные ответы:
- где хранится идентификатор операции;
- какой ключ защищает от повторного списания;
- что происходит, если обработчик записал результат, но не подтвердил сообщение;
- что будет, если сообщение придёт в неправильном порядке;
- как система отделяет временную ошибку от постоянной.
Связка записи в базу и отправки события часто требует паттерна transactional outbox: событие сначала сохраняется рядом с бизнес-изменением, а отдельный процесс публикует его повторно до успешного подтверждения. Это не решает порядок событий во всех случаях и не отменяет идемпотентность потребителя.
Таблица ревью: подход, выигрыш и цена
| Подход | Что защищает | Цена и риск | Что спросить на ревью |
|---|---|---|---|
| Атомарное обновление с условием | Конкурирующее изменение одной записи | Не покрывает внешние вызовы и несколько хранилищ | Проверяется ли результат обновления и что означает ноль изменённых строк |
| Блокировка в транзакции | Критическую последовательность чтения и записи | Ожидание, дедлоки, падение пропускной способности | Какие строки блокируются и сколько живёт транзакция |
| Transactional outbox | Потерю события между коммитом и публикацией | Дополнительная таблица, фоновые повторы, дубликаты | Есть ли повторная отправка и идемпотентный потребитель |
| Версионирование записи | Перезапись более нового состояния старым | Конфликт нужно показать или разрешить явно | Что получает клиент при несовпадении версии |
Кеш, реплики и порядок чтения
Кеш превращает согласованность в контракт. Если после изменения профиля пользователь видит старое имя 30 секунд, это может быть приемлемо. Если после оплаты экран показывает неоплаченный заказ, тот же механизм становится дефектом.
На ревью проверьте три момента:
- когда запись в кеше становится недействительной;
- что происходит, если инвалидировать кеш не удалось;
- может ли критичный запрос читать только что изменённые данные с реплики.
Опасная схема выглядит так: запись ушла в основную базу, следующий запрос попал на отстающую реплику, приложение решило, что изменения не было, и повторило действие. Для критичного чтения нужен явный маршрут: чтение из источника истины, требование заданной версии или другой механизм, описанный в контракте.
Что ломается первым
Тестировать нужно не только успешный сценарий. Минимальный набор для ревью и интеграционных тестов:
- два одинаковых запроса приходят одновременно;
- клиент повторяет запрос после тайм-аута, хотя сервер уже выполнил операцию;
- процесс завершается после записи в базу, но до публикации события;
- сообщение доставляется повторно или позже следующего события;
- реплика и кеш временно отстают;
- один из внешних сервисов отвечает медленно, а затем возвращает ошибку.
Для каждого сценария в ревью должна быть не общая фраза «обработаем ошибку», а наблюдаемое поведение: какой статус получит клиент, что запишется в журнал, будет ли повтор, где окажется операция для ручного разбора.
Замеры вместо уверенности
Если автор говорит «транзакция быстрая» или «очередь почти сразу доставляет событие», попросите измерение. Без данных нельзя честно выбрать между блокировкой, очередью и версионированием.
Минимальный план наблюдений:
- время транзакции и время ожидания блокировки;
- доля конфликтов, дедлоков и повторных попыток;
- задержка от коммита записи до обработки события;
- число повторно обработанных сообщений;
- доля чтений со старой версией и возраст кеша;
- число операций, застрявших в состоянии «неизвестно».
Снимайте показатели на нагрузке, близкой к продакшену: с тем же размером данных, числом экземпляров приложения и конфигурацией реплик. Если замеров пока нет, это не повод придумывать цифры — это отдельное замечание к наблюдаемости.
Как формулировать замечания
Слабое замечание: «здесь возможна гонка». Сильное: «два обработчика читают остаток 1, оба проходят проверку, затем уменьшают значение; покажите атомарное условие или блокировку и тест с двумя параллельными операциями».
Ещё несколько рабочих формулировок:
- «После коммита процесс может завершиться до отправки события. Где хранится событие для повторной публикации?»
- «Повтор после тайм-аута может создать второй платёж. Какой ключ делает операцию повторяемой?»
- «Обработчик меняет статус без проверки предыдущего состояния. Что произойдёт при событии, пришедшем не по порядку?»
- «Запрос читает реплику сразу после записи. Какой контракт допускает отставание и кто его видит?»
- «Внешний вызов находится внутри транзакции. Что будет с блокировкой при задержке провайдера?»
Что делать дальше
Возьмите один критичный поток — оплату, резервирование или выдачу доступа — и составьте таблицу: инвариант, источник истины, граница транзакции, повтор, порядок событий, допустимое отставание и метрика. Затем добавьте тесты на два параллельных запроса, повтор после тайм-аута и остановку процесса между записью и публикацией события. В продакшене первым делом поставьте измерения задержки репликации, повторов и зависших операций: именно они покажут, где теоретический риск стал реальным.
[1] Инвариант — правило, которое должно оставаться истинным после операции. Например, «баланс не может быть меньше нуля».
[2] Идемпотентность — свойство операции, при котором повтор с теми же данными не меняет итоговый результат после первого успешного выполнения.
Аналитик звонков: Разбор записи звонка по этапам сделки: где сорвалось, что упущено, три исправляемые рекомендации с цитатами.
Спросить за 90 ₽Источники: PostgreSQL 16 Documentation: Transaction Isolation, Martin Kleppmann — Designing Data-Intensive Applications
Частые вопросы
Нужно ли добиваться строгой согласованности всех данных?
Достаточно ли обернуть все операции в одну транзакцию?
Что проверить, если обработчик сообщения может запуститься дважды?
Как понять, что чтение с реплики опасно?
Что будет, если процесс упадёт после записи в базу?
Можно ли использовать блокировки для всех проблем согласованности?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.