В проблемах распределённых систем и согласованности и консенсусе мы разбирали механизмы: кворумы, fencing-токены, линеаризуемость, консенсус. Эта статья — про то, что стоит над механизмами: как вообще рассуждать о корректности приложения.
Главный вывод неочевиден и меняет то, как вы проектируете: надёжные нижние уровни (транзакции, TCP, контрольные суммы) сами по себе не гарантируют, что приложение работает правильно. Это статья «для кругозора» — общий зонтик над всеми тактическими паттернами вроде идемпотентности и outbox.
Сквозной аргумент: почему транзакции недостаточно
Классическая ловушка: «мы используем сериализуемые транзакции — значит, данные корректны». Нет. Сериализуемая транзакция гарантирует лишь, что параллельные транзакции не мешают друг другу. Но если приложение по ошибке (баг в коде или повтор запроса) записало неверные данные — транзакция честно и надёжно их зафиксирует.
Разберём на примере. Клиент отправляет перевод денег, теряет соединение до ответа сервера и повторяет запрос. Каждый запрос сам по себе — корректная транзакция, но денег списалось вдвое:
BEGIN;
UPDATE accounts SET balance = balance + 11 WHERE id = 1234;
UPDATE accounts SET balance = balance - 11 WHERE id = 4321;
COMMIT;
Ни база, ни TCP тут не спасут. TCP отбрасывает дубликаты внутри одного соединения — но клиент переподключился, и для базы это уже новый запрос. Дедупликация нужна сквозная — от клиента до самого хранилища.
Это и есть сквозной аргумент (end-to-end argument, сформулирован Saltzer, Reed и Clark ещё в 1984-м): функцию корректности можно полностью обеспечить только с участием конечных точек системы. Промежуточные уровни (сеть, транзакция) полезны для скорости, но не заменяют проверку на концах. Отсюда решение — идентификатор операции, который клиент генерирует один раз и протаскивает через все звенья:
ALTER TABLE requests ADD UNIQUE (request_id);
BEGIN;
INSERT INTO requests (request_id, ...) VALUES ('0286FD..', ...); -- повтор упадёт на UNIQUE
UPDATE accounts SET balance = balance + 11 WHERE id = 1234;
UPDATE accounts SET balance = balance - 11 WHERE id = 4321;
COMMIT;
Повторный запрос с тем же request_id не пройдёт вставку — и перевод выполнится ровно один раз. Причём это ограничение UNIQUE реляционная база держит даже на слабых уровнях изоляции. Это фундамент под всей идемпотентностью: «ровно один раз» (exactly-once) — это не магия брокера, а сквозной ключ операции плюс дедупликация на приёмнике.
Ограничения без глобальной координации
Строгое ограничение уникальности (два человека не займут один ник или одно место в самолёте) требует координации — все конкурирующие запросы должны пройти через одну точку, которая решает, кто первый. В распределённой системе это дорого: единый ведущий узел, синхронный кворум, падение пропускной способности.
Но многим приложениям строгая уникальность и не нужна — им хватает более слабого ограничения, которое можно нарушить и исправить постфактум. Продали больше мест, чем есть? Овербукинг у авиакомпаний — это осознанное нарушение с компенсирующей транзакцией: отмена брони, возврат денег, апгрейд. Списали дважды? Отменить один платёж. Стоимость такого «извинения» обычно невелика, и бизнес закладывает её как часть процесса. Это прямо связано с компенсацией и конечной согласованностью: не координировать всё подряд, а разрешать конфликты позже.
Целостность против своевременности
Самая полезная рамка тут — разложить размытое слово «согласованность» на два разных требования:
- Своевременность (timeliness) — пользователь видит актуальное состояние. Нарушение своевременности временно: почитали устаревшую реплику, но потом всё сойдётся. Это линеаризуемость и «читаем свои же записи».
- Целостность (integrity) — данные не повреждены: нет потерь, нет противоречий, нет «денег, которые списали, но никто не получил». Нарушение целостности постоянно: само оно не исправится, нужна явная проверка и восстановление.
Формула короткая: нарушение своевременности — это «согласованность иногда», а нарушение целостности — «вечная несогласованность». И вывод: в большинстве приложений целостность важнее. Транзакция по карте, не появившаяся за 24 часа, — терпимо (банки сводят операции асинхронно). А вот если баланс не равен сумме операций или деньги исчезли — это уже катастрофа.
Практический смысл: системы на журналах событий (CDC, потоковая обработка, outbox) разводят эти два свойства. Своевременность они по умолчанию не дают (потребители асинхронны), но целостность держат железно — через идемпотентную доставку и дедупликацию по request_id. Часто это и есть правильный размен: отказаться от дорогой синхронной координации, сохранив целостность и приняв временную несвоевременность.
Доверяй, но проверяй
Последняя мысль: даже корректный код работает на неидеальном железе. Биты на диске портятся, повреждение в сети иногда проскакивает мимо контрольных сумм TCP, у самих баз бывают баги в уровнях изоляции. Зрелые системы не верят в надёжность слепо: HDFS и S3 фоново перечитывают файлы и сверяют их с репликами. Отсюда — аудитопригодность: системы на неизменяемых событиях позволяют проследить происхождение любого производного состояния и пересобрать его заново для проверки, а криптографические инструменты (деревья Меркла) — подтвердить, что данные не подменили. Проверяйте целостность периодически, а не узнавайте о повреждении тогда, когда бэкап уже испорчен.
Где это применяется
Как только в системе появляются повторные запросы, несколько хранилищ или асинхронные потребители — вы обязаны рассуждать о корректности отдельно от «база же надёжная». Практическая рамка: генерируйте сквозной идентификатор операции на клиенте и дедуплицируйте на приёмнике (это и есть exactly-once); разделяйте своевременность и целостность и защищайте в первую очередь целостность; где строгая координация дорога — разрешайте слабое ограничение с компенсацией; периодически проверяйте целостность, не доверяя слепо.
Где спотыкаются начинающие:
- «У нас сериализуемые транзакции, значит всё корректно». Транзакция не спасёт от повтора запроса или бага приложения. Нужен сквозной
request_id. - Надеются на дедуп TCP или брокера. Он работает на одном звене; при переподключении клиента дубликат проходит. Дедупликация нужна сквозная, на приёмнике.
- Путают своевременность и целостность. Гонятся за линеаризуемостью там, где хватило бы конечной согласованности с гарантией целостности, и платят за координацию зря.
- Считают, что «база надёжная» = данные не испортятся. Без периодической проверки повреждение всплывает тогда, когда чинить уже поздно.
Что почитать дальше: проблемы распределённых систем — механизмы, на которых стоит корректность; согласованность и консенсус — что такое линеаризуемость; идемпотентность — тактика exactly-once в коде; компенсация — как исправлять слабые ограничения постфактум; потоковая обработка — где целостность и своевременность разведены.