В проблемах распределённых систем и согласованности и консенсусе мы разбирали механизмы: кворумы, 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) — это не магия брокера, а сквозной ключ операции плюс дедуп на приёмнике.
Ограничения без глобальной координации
Строгое ограничение уникальности (два человека не займут один ник, одно место в самолёте) требует координации — все конкурирующие запросы должны пройти через одну точку, которая решает, кто первый. В распределённой системе это дорого: единый ведущий узел, синхронный кворум, падение пропускной способности.
Но многим приложениям строгая уникальность и не нужна — достаточно более слабого ограничения, которое можно нарушить и исправить постфактум. Продали больше мест, чем есть? Овербукинг авиакомпаний — это осознанное нарушение с компенсирующей транзакцией: отмена брони, возврат денег, апгрейд. Списали дважды? Отменить один платёж. Стоимость «извинения» обычно низкая, и бизнес закладывает её как часть процесса. Это прямо связано с компенсацией и eventual consistency: не координировать всё подряд, а разрешать конфликты позже.
Целостность против своевременности
Самая полезная рамка главы — разложить размытое «согласованность» на два разных требования:
- Своевременность (timeliness) — пользователь видит актуальное состояние. Нарушение своевременности временно: почитали устаревшую реплику, но потом всё сойдётся. Это линеаризуемость и «читаем свои же записи».
- Целостность (integrity) — данные не повреждены: нет потерь, противоречий, «денег, списанных, но не полученных». Нарушение целостности постоянно: само не исправится, нужна явная проверка и восстановление.
Формула из книги: нарушение своевременности — «согласованность иногда», нарушение целостности — «вечная несогласованность». И вывод: в большинстве приложений целостность важнее. Транзакция по карте, не появившаяся за 24 часа, — терпимо (банки сводят асинхронно). А вот если баланс не равен сумме операций или деньги исчезли — это катастрофа.
Практический смысл: системы на журналах событий (CDC, потоковая обработка, outbox) разводят эти два свойства. Своевременность они по умолчанию не дают (потребители асинхронны), но целостность держат железно — через идемпотентную доставку и дедуп по request_id. Часто это и есть правильный размен: отказаться от дорогой синхронной координации, сохранив целостность и приняв временную несвоевременность.
Доверяй, но проверяй
Последняя мысль: даже корректный код работает на неидеальном железе — биты на диске портятся, повреждение в сети проскакивает мимо контрольных сумм TCP, у БД бывают баги в уровнях изоляции. Зрелые системы не верят слепо: HDFS и S3 фоново перечитывают файлы и сверяют с репликами. Отсюда — аудитопригодность: системы на неизменяемых событиях позволяют проследить происхождение любого производного состояния и пересобрать его заново для проверки, а криптографические инструменты (деревья Меркла) — подтвердить, что данные не подменены. Проверяйте целостность периодически, а не узнавайте о повреждении, когда бэкап уже испорчен.
Где это применяется
Как только в системе есть повторные запросы, несколько хранилищ или асинхронные потребители — вы обязаны рассуждать о корректности отдельно от «база же надёжная». Практическая рамка: генерируйте сквозной идентификатор операции на клиенте и дедуплицируйте на приёмнике (это и есть exactly-once); разделяйте своевременность и целостность и защищайте в первую очередь целостность; где строгая координация дорога — разрешайте слабое ограничение с компенсацией; периодически проверяйте целостность, не доверяя слепо.
Где спотыкаются начинающие:
- «У нас сериализуемые транзакции, значит всё корректно» — транзакция не спасёт от повтора запроса или бага приложения. Нужен сквозной
request_id. - Надеются на дедуп TCP/брокера — он работает на одном хопе; при переподключении клиента дубликат проходит. Дедуп нужен сквозной, на приёмнике.
- Путают своевременность и целостность — гонятся за линеаризуемостью там, где хватило бы eventual consistency с гарантией целостности, и платят за координацию зря.
- Считают, что «база надёжная» = данные не испортятся — без периодической проверки повреждение всплывает, когда чинить уже поздно.
Что почитать дальше: проблемы распределённых систем — механизмы, на которых стоит корректность; согласованность и консенсус — что такое линеаризуемость; идемпотентность — тактика exactly-once в коде; компенсация — как исправлять слабые ограничения постфактум; потоковая обработка — где целостность и своевременность разведены. Первоисточник — Мартин Клеппман, «Высоконагруженные приложения», глава 12.