title: Статья 09 — Согласованность и распределённые транзакции source: подготовлено 2026-09-17, TASK-45.49; каркас — brief.md §09; развёртка knowledge-base.md §7; источники — DDIA 2-е изд. гл. 8, 10 (TASK-45.39); отсылки — статьи 05, 07, 08, 10, 11 серии, классические разборы classic-designs.md
Статья 09: согласованность и распределённые транзакции
Раздел о том, на каком языке мы договариваемся о гарантиях данных. Статья 05 дала механику: реплики, лаг, кворумы, failover. Статья 06 — копии в кэше. Здесь появляется то, что над этими механизмами: модели согласованности (какие аномалии пользователь может увидеть), транзакции в распределённой системе (как несколько систем пишут вместе) и консенсус (как система вообще может одинаково договориться о чём-то). Критерий «умею, если» из брифа: объясняю, какую согласованность выбираю для конкретного кейса и чем плачу.
Верхний уровень глубины для интервью — именно он заявлен: «уметь упомянуть на правильном уровне», а не прочитать мини-курс по CAP. Что это значит для нас: не сдавать раздел лекцией, а продавать выбором. Ответ на «какую согласованность выбираете?» строится по трём шагам: (1) назвать нужную гарантию, (2) назвать цену, (3) сказать, где явно от неё отказываемся и чем компенсируем. Формула раздела: «кейс → гарантия → цена». Глубина живёт в ../knowledge-base.md §7; полные конспекты глав — ../materials/ddia-2ed/ch-08-transactions.md и ../materials/ddia-2ed/ch-10-consistency-consensus.md; эталоны, где выбор звучит целиком, — ../classic-designs.md.
Место раздела в секции: на этапе 4 (детали и БД) мы выбираем модель данных и хранилище — и обязаны сказать, какую согласованность даёт выбранная схема; на этапе 6 (эксплуатация) отвечаем, что происходит при отказах нод с данными. Раздел 09 — язык для обоих. Он же пропитывает весь дизайн: мульти-ДЦ из раздела 08 почти всегда означает «между ДЦ — AP, внутри — C»; очереди из 07 — «at-least-once + идемпотентность» вместо распределённых транзакций.
1. Словарь: три оси, на которых стоят все гарантии
Прежде чем говорить о конкретных механизмах, три рамки, в которые укладывается всё, что дальше.
Ось 1 — модели согласованности (что видит читающий). От самой строгой к самой слабой: linearizability (система выглядит как одна копия: после завершения записи все читают новое) → sequential consistency (порядок операций один и тот же для всех наблюдателей, но не обязан совпадать с реальным временем) → causal consistency (причинно связанные операции видят все в одном порядке, параллельные — как получится) → eventual consistency (когда-нибудь догонит). Сессионные гарантии (read-your-writes, monotonic reads, consistent prefix) — не отдельная модель, а ослабление, которое приложение может обеспечить само; статья 05 §3 уже это разобрала — здесь хватает ссылки.
Ось 2 — изоляция транзакций (что делает БД внутри одной системы): read committed → snapshot isolation → serializable. Это про честность конкурентных транзакций, разбираем в §4. Часто путают с осью 1 — и это ловушка (§3): linearizability про свежесть, serializability про порядок.
Ось 3 — атомарность многих систем (как несколько сервисов/БД пишут «вместе»): атомарный коммит (2PC), сага, outbox (§5).
Каждая ось отвечает свой вопрос; неумение их разделять — главный источник «каши» на секции. Простой способ держать их в голове: ось 1 про чтение, ось 2 про одну БД, ось 3 про несколько систем.
2. CAP и PACELC: формулировки без мифов
CAP знают все, но на секции он часто звучит как заклинание. Правильная подача — две фразы.
CAP буквально: при сетевом разделении (partition — P случается всегда, это не опция, это сеть) система должна выбрать: либо Consistency — то есть linearizable чтения/записи (все ноды видят одну свежую копию), либо Availability — каждая нода продолжает принимать запросы (даже если отвечает устаревшим состоянием). Вне разделения CAP молчит — и поэтому бесполезен в обычной жизни: разделения редки (в исследованиях сетевые партиции — это малая доля инцидентов), а плата за согласованность идёт всегда.
PACELC честнее: Partition → Availability vs Consistency; в оставшееся время (Else) Latency vs Consistency. То есть даже когда сеть цела, мы платим латентностью за строгую согласованность: линейзуемость требует синхронного кворума или round-trip к одной точке, а это дополнительные ходы по сети. Причём это не инженерный выбор, а теоретический предел (Attiya–Welch): линейзуемость по своей природе стоит минимум RTT — строгая согласованность принципиально медленнее слабой, предел существует даже для RAM многоядерного CPU.
Формулировки для доски (из KB §7), которые звучат в разы большим пониманием, чем «возьмём AP»:
- «В одном ДЦ почти всегда C»: партиции внутри ДЦ редки и кратковременны, а за линейзуемость внутри ДЦ мы платим десятки миллисекунд — приемлемо. Цена в проде: мульти-ДЦ «active-active» с синхронной репликацией между континентами — каждая запись ждёт RTT между ДЦ (Москва–NY ~120 мс). Внутри ДЦ C дёшев, между ДЦ — дорог.
- «Между ДЦ — вынужденно AP с eventual»: тот же сюжет из статьи 08 §9 — между ДЦ допускаем отставание реплик (split brain лечим fencing'ом, а не синхронным кворумом). Отсюда же классика: глобальный сервис с локальными копиями данных (CDN, кэш по регионам) — это осознанное AP.
- «Консистентность — это обычно компромисс двух осей сразу»: внутри ДЦ выбрали C и платим латентностью; между ДЦ выбрали доступность и платим свежестью и сложностью конфликтов.
Лакмус понимания CAP/PACELC на секции: если интервьюер спрашивает «а что выберете — C или A?», ответ не должен быть «C!» — должен быть «при разделении я выбираю…, а в остальное время…; вот мой кейс». Готовая реплика: «для денежных операций — в одном ДЦ C с полусинхронной репликацией и внятным failover; для лайков — eventual + идемпотентные счётчики, пользователь простит лаг в секунды, если счётчик не капризничает каждый раз».
3. Linearizability vs eventual: лестница и что на ней случается
Самый частый сюжет раздела — выбор между «свежо всегда» и «свежо когда-нибудь». Чтобы выбор был осознанным, надо видеть не два конца, а лестницу с промежуточными ступенями:
| Модель | Что гарантирует | Цена | Где живёт на нашей схеме |
|---|---|---|---|
| Linearizability | Система как одна копия: запись завершилась → все последующие чтения видят новое значение | Синхронный кворум/одна точка записи; латентность ≥ RTT; единственная точка при шардировании | Деньги, инвентарь, лидерство, uniqueness (замок, ключ) |
| Sequential | Порядок операций одинаков для всех, время может врать | Дёшевле линейзуемости, но семантика «порядок есть, свежести нет» почти нигде не нужна сама по себе | Теоретическая ступень; на практике упоминается как «знаю, что есть» |
| Causal | Причинно связанные записи видят все в одном порядке; параллельные — в произвольном | Лаги, упорядочение по причинности (ключам/партициям) | Лента друзей: комментарий-ответ после комментария-вопроса; «что я писал, вижу в правильном порядке» |
| Eventual | Когда-нибудь догонит; никакой гарантии момента | Дешёвая репликация, высокая доступность; нужны сессионные гарантии отдельно | Лайки, просмотры, профили, поисковые проекции |
Важные оговорки, которые превращают перечисление в инженерное знание:
- Eventual не значит «вечно рассинхронно»: eventual подразумевает, что при остановке новых записей все реплики в итоге сойдутся. Это свойство, которое надо обеспечивать (read repair, anti-entropy из статьи 05 §4), а не подразумевать. Фраза: «eventual — это контракт „когда-нибудь“, но „когда“ я делаю предсказуемым: лаг в секунды, иначе пользователь видит аномалии».
- Каждая более слабая ступень — это более сильные требования к приложению: eventual означает, что приложение готово видеть аномалии (потерянный лайк, старая версия, откат комментария). Если на этом чтении бизнес делает решающее действие (списать, зарезервировать) — eventual недостаточен.
- Сессионные гарантии — дешёвый способ съесть разницу между ступенями: статья 05 §3 показывает, что read-your-writes и monotonic reads обеспечиваются маршрутизацией, а не консенсусом. Именно поэтому «лента» (из статьи 11) может честно говорить: «лента eventual, но свои посты читаю с лидера, а порядок ленты прибит к партиции пользователя — аномалии, которые пользователь может поймать, сводятся к почти нулю».
- Версии и часы (кратко, на уровне «упомянуть»): сравнение «кто свежее» по wall-clock — это last-write-wins, который тихо теряет данные (часы врут, NTP прыгает — статья 08 §2); параллельные версии детектируют vector clocks (счётчики по узлам — дороже по размеру), total order дают Lamport/HLC. Достаточно на секции сказать: «LWW по времени машины — знаю, что теряет данные; параллельность детектируют векторные часы; конфликт отдаём пользователю или мерджим». Это уже «правильный уровень», дальше — только если интервьюер сам полезет.
3.1 Вопрос-ловушка: linearizability ≠ serializability
Вопрос, который ставят кандидату, чтобы проверить, не механически ли выучены термины: «линеаризуемость и сериализуемость — это одно и то же?» Правильный ответ — нет:
- Linearizability — гарантия свежести одной операции: как только запись завершилась (или чтение началось после него), все последующие чтения видят новое значение. Про транзакции и их внутренний порядок ничего не говорит.
- Serializability — гарантия изоляции транзакций: результат эквивалентен какому-то последовательному (одна за другой) исполнению — какой именно порядок, не важно. Про время и свежесть не говорит: транзакции могут быть «свежими» и при этом давними.
- Вместе — strict serializability (Spanner, FoundationDB): и изоляция, и свежесть — транзакции имеют общий порядок, согласованный с реальным временем.
Следствие, которое любит интервьюер: кворумы W+R>N НЕ дают linearizability (обсуждено в статье 05 §4): они дают свежесть после завершённой записи, но параллельные записи и read repair не сериализуют. А single-leader репликация даёт линейзуемость только пока лидер настоящий (и жив) — как только включается failover со split brain, гарантия исчезает. Из этого следует, где linearizability реально нужна: лок/выборы лидера, uniqueness-ограничения, «cross-channel timing» (запись в одном канале, проверка в другом). А где не нужна: лайки, ленты, кэши — там важны дешевизна и доступность, а не общий порядок, и платить RTT за свежесть каждой записи нечем.
4. Изоляция транзакций: второй эшелон глубины
Уровень, на который уходит интервьюер, когда спрашивает «а что у вас внутри БД?» или «а как вы решаете гонки?». Не обязателен для каждого дизайна, но обязателен как словарь — чтобы не называть snapshot-изоляцию «сериализуемостью». Лестница:
- Read committed — минимум, обещают почти все СУБД: нет dirty read (не видим незакоммиченное) и dirty write (не перезаписываем чужое незакоммиченное). Реализация: MVCC (читатели видят последнюю закоммиченную версию) + row-lock на запись до коммита.
- Snapshot isolation (в Postgres фактически это repeatable read) — транзакция читает согласованный снапшот на момент старта: все чтения внутри транзакции видят одну версию мира, даже если другие транзакции параллельно коммитят. Дёшево, потому что ничего не блокирует читателей.
- Serializable — самый строгий: результат как при каком-то последовательном исполнении. Двумя путями: 2PL (блокировки до конца транзакции — просто, но дедлоки и низкая конкурентность) или SSI (serializable snapshot isolation — MVCC + отслеживание опасных rw-антизависимостей, abort вместо блокировок; современный стандарт — Postgres 9.1+, CockroachDB, TiDB, FoundationDB).
Три гонки — называть по имени и с лечением:
| Гонка | Что происходит | Лечение |
|---|---|---|
| Lost update | Две транзакции делают read-modify-write одного счётчика, обе записывают, одна перезаписывает другую | Атомарная запись (UPDATE ... SET x = x + 1), SELECT ... FOR UPDATE, CAS (UPDATE ... WHERE old_value), автоматический конфликт-детект в MVCC |
| Write skew | Две транзакции читают пересекающийся набор строк и пишут разные строки — инвариант сломан (оба врача сняли себя с дежурства, думая, что второй останется) | Serializable, материализация конфликта (общая строка-счётчик), constraint (как правило — только serializable честно решает) |
| Phantom | Предикатный запрос возвращает разные строки внутри транзакции (кто-то вставил подходящую строку) | Snapshot закрывает фантомы чтений; для serializable — предикатные/range-lock или SSI |
Формулировка для секции: «дефолт — snapshot isolation: дёшево и честно для чтения; гонки счётчиков — атомарной записью/CAS; там, где инвариант бизнеса (деньги, лимиты, дежурства), а не просто арифметика — serializable через SSI или материализованный конфликт». Уровень изоляции — только про одну БД: как только операция затрагивает несколько систем, мы переходим в раздел 5.
5. Распределённые транзакции: 2PC, сага, outbox
Вопрос «как атомарно записать в несколько систем?» имеет три ответа — и порядок выбора на секции примерно такой: сначала outbox (двух систем не атомарно — делаем одну), потом сага (атомарность не нужна — нужна согласованность шагов), 2PC — избегаем умышленно и говорим почему.
5.1 2PC: механизм существует, но в high-load мы его избегаем
Two-phase commit: координатор спрашивает у всех участников prepare (заморозились, гарантируем коммит), потом commit всем. Красиво в теории — атомарность в распределённой системе достигнута. На практике три причины, почему на секции правильнее сказать «избегаю»:
- In-doubt транзакции: участник, получивший prepare и упавший до commit, держит лока (а вместе с ними читатели и конкурентные писатели) — «зависшие» транзакции разбирают вручную, когда координатор придёт в себя. Это реальность XA в проде.
- Координатор — SPOF и точка синхронизации: упал координатор — вся транзакция стоит; «heuristic decisions» (участник сам решил, коммитить или откатывать) ломают атомарность тихо — ремонт дороже, чем отказ.
- Цена блокировок на весь срок транзакции — в распределённой транзакции он непредсказуем (сеть, очередь, паузы GC — статья 08 §2), а блокировки держатся до конца.
Об этом есть честная, готовая фраза: «2PC — это атомарность ценой точки отказа и блокировок; в высоконагруженных системах я избегаю 2PC и применяю саги» — и сразу перевернуть в конструктив: «вместо атомарности многих систем — одна атомарность (БД + outbox), остальное — компенсации».
5.2 Сага: не атомарно, но согласованно
Saga — последовательность локальных транзакций (каждая в своей системе), где на каждый шаг есть компенсирующее действие: order → reserve → charge → ship; откат — cancel → refund → restock. Ключевое отличие от 2PC: атомарности нет — на короткое время состояние «частично выполнено» видно наружу, но благодаря компенсациям в итоге система приходит в согласованное состояние. Цена — компенсации надо проектировать с первого дня (не бывает «отменить резерв» задним числом) и идемпотентность повторных шагов (ретраи в саге — норма).
Два способа управлять сагой — называть оба и выбирать по кейсу:
- Оркестрация: центральный координатор (сага-менеджер) вызывает шаги по очереди и компенсирует при отказе. Плюс — всё видно в одном месте, простое мышление о бизнес-процессе. Минус — координатор сам становится узким местом/SPOF (но это stateless-оркестратор с перезапуском процесса — дешевле, чем 2PC-координатор, потому что не держит блокировки).
- Хореография: шаги связаны событиями через шину: «OrderCreated → PaymentService резервирует → ReservedEvent → ShipmentService…». Плюс — нет центральной точки, сервисы слабо связаны. Минус — поток процесса размазан по событиям, читать и отлаживать его сложнее, компенсации тоже события. Для простых цепочек (2–3 шага) хореография избыточна.
Уровень для интервью — не «как реализовать сагу», а «когда нужна сага и чем плачу»: «мульти-сервисная запись без атомарности: шаги локально транзакционны, откат — компенсации, повтор — идемпотентность; оркестрация для управляемого процесса, хореография для слабосвязанных сервисов».
5.3 Transactional outbox: стандартный ответ вместо двойной записи
Механизм из статьи 07 §10 — здесь он превращается в ответ на 2PC-соблазн: «БД записала, событие потерялось» решается не распределённой транзакцией, а композицией. Событие пишется в ту же транзакцию, что и данные (outbox-таблица), публикатор (воркер или CDC — Debezium) доставляет в Kafka и помечает прочитанным. Атомарность не между системами, а в одной БД — чего достаточно, потому что точно-once между системами не существует: есть at-least-once + дедупликация.
Фраза, которой закрывается любой вопрос «а как две системы пишут вместе?»: «двойную запись не делаю: данные и событие — в одной транзакции через outbox; доставка — at-least-once + idempotency key; если нужен согласованный бизнес-процесс из нескольких шагов — сага с компенсациями; 2PC с XA избегаю». Рядом — обязательный пас в статью 07 §5: идемпотентность потребителя (таблица обработанных message-ID в локальной транзакции) — это и есть «exactly-once без распределённых транзакций», так делает Kafka Streams.
Упомянуть одним словом (по запросу): NewSQL (Spanner, CockroachDB, TiDB, FoundationDB) — системы, которые дают внутренние распределённые транзакции через реплицированный координатор на консенсусе — без XA-болей, но с ценой в консенсусе на каждый коммит и ограничением географии (Spanner — синхронные реплики в пределах региона/континента). На секции: «если БД сама умеет распределённые транзакции (NewSQL) — атомарность многошардовая возможна, но дорогая; выбираю её только там, где бизнес реально требует атомарности шире одной шарда».
6. Консенсус: как несколько нод договариваются одинаково
Консенсус — фундамент, о котором на секции достаточно сказать правильные вещи на правильном уровне: зачем, что гарантирует, как устроен, какие сервисы дают его готовым. Не больше.
Зачем. Консенсус — класс эквивалентных задач: выбор единственного значения, линейзуемый CAS, shared log (total order broadcast — «все ноды видят одно и то же в одном порядке»), atomic commit, fetch-and-add. На практике системы дают shared log, а поверх — state machine replication (все реплики применяют одну последовательность команд → одинаковое состояние). Из этого вырастает: линейзуемые транзакции (etcd-подход), fencing tokens (эпоха), leader election, координация вообще. Для секции достаточно связи: «консенсус необходим там, где нужен общий порядок или общее единственное значение — выборы лидера из 05 §5, fencing из 08 §8, разделяемый лок».
Свойства. Три safety-свойства держатся всегда: agreement (все договорились об одном значении), integrity (дважды не договариваются), validity (значение не выдумано). Плюс termination (liveness) — «система в итоге договорится», и вот оно — требует живого большинства: 3 ноды терпят 1 отказ, 5 нод — 2. Это та самая арифметика majority из статьи 08 §9, здесь она называется своим именем.
Механика в одну фразу. Raft: term/эпоха — уникальный лидер эпохи; два голосования — выборы лидера и коммит записи — на пересекающихся кворумах (разные большинства в разные моменты обязательно пересекаются); каждая запись синхронно реплицируется в кворум, поэтому не теряется. Отличие от 2PC в одной фразе: «2PC требует согласия всех (и умирает от одного отказа), консенсус — кворума; поэтому консенсус даёт и отказоустойчивость, и линейзуемость». Цена: консенсус синхронен (каждая запись — round-trip к кворуму), таймауты чувствительны к вариативности сети, больше нод — медленнее кворум.
Практика. Никто не пишет свой Raft на секции: координационные сервисы — ZooKeeper, etcd, Consul (по образцу Chubby) — аутсорсят консенсус на 3–5 нод для тысяч шардов. Что они дают из коробки: locks/leases + fencing tokens (таймаут-аренды, защита от зомби-лидера — статья 08 §8), failure detection (сессии, ephemeral nodes), change notifications, service discovery (который, кстати, кэшировать, а не читать синхронно — линейзуемость для discovery не нужна, KB §13.5). Классика применения в наших схемах: лидерство фоновых воркеров (fan-out ленты, пересчёт кэша), дедуп критичных операций, fencing при failover. Распределённые локи: Redis/Redlock — быстро, но компромисс по надёжности; ZK/etcd — кворумные и надёжные, но дороже; при локах всегда проговаривать fencing token и «лок ≠ транзакция».
Альтернативы консенсусу — назвать, чтобы показать, что мы умеем выбирать: single-leader репликация даёт линейзуемость, пока лидер жив и настоящий (дешевле консенсуса — несинхронный кворум не нужен; но failover уже нелинейзуем — статья 05 §5); кворумы W+R>N дают свежесть, не linearizability (статья 05 §4, доказательство — DDIA гл. 10). Вывод-фраза: «консенсус — последняя инстанция: беру, когда без общего порядка никак (выборы, fencing, атомарный CAS, общий лог); в остальном хватает single-leader и кворума с известными оговорками».
7. Мини-таблица: кейс → уровень согласованности → чем плачу
Ядро раздела — критерий брифа. Таблица — не «наизусть», а мысленный шаблон: для любого кейса провести строку — что гарантирую, каким механизмом, чем плачу. Примеры разобраны по эталонам (§8) и по типам данных секции.
| Кейс | Уровень согласованности | Механизм | Чем плачу |
|---|---|---|---|
| Деньги: баланс, списание, перевод | Linearizable на операцию + serializable в БД | Одна БД с лидером (single-writer), snapshot/SSI, идемпотентность (idempotency key для платёжных вызовов) | Латентность записи, одна точка записи на шард; мульти-ДЦ — только active–passive (запись живёт в одном ДЦ) |
| Инвентарь/лимиты (бронь, остатки) | Атомарная запись с условием (CAS) | UPDATE ... SET n = n − 1 WHERE n > 0, SELECT ... FOR UPDATE |
Конкурентность: конфликты → ретраи и UX «попробуйте ещё раз» |
| Лента (посты друзей) | Eventual + причинный порядок по подпискам; свои посты — read-your-writes | Async-репликация + маршрутизация «своё с лидера» (05 §3) | Лаг в секунды; «лента на момент открытия» — пользователь прощает |
| Счётчики (лайки, просмотры, followers) | Eventual, в итоге догонит; допускается временный рассинхрон | Async-инкремент, агрегация в фоне (таблица счётчиков или агрегатор на Kafka, 07), идемпотентность по (user, post) | Счётчик «врёт» в моменте; повторы гасятся дедуп-ключом |
| Профиль пользователя (имя, аватар, настройки) | Read-your-writes своей записи, остальным eventual | Свои данные — с лидера; чужие — реплики/кэш с TTL | Латентность свежести чужих профилей; кэш-инвалидация |
| Поисковый индекс | Eventual, лаг минуты; консистентность индекса не требуется | CDC/проекция (07 §10) | Индекс отстаёт; search работает на «почти свежих» данных — норма |
| Уникальность: username, short link, order id | Linearizable (fetch-and-add) — уникальный ключ обязан быть уникален всегда | Single-source: лидер/последовательность в БД, уникальный индекс, либо консенсус (ZK/etcd) | Централизация или консенсус; распределённые ID (снежинка) — только если уникальность без порядка (статья 10) |
| Выборы лидера / лидерство фонового воркера | Strong (линейзуемый CAS/выборы) + fencing | ZK/etcd: ephemeral-узел/лидер-эпоха; fencing token при каждом действии | Внешняя зависимость: кластер ZK/etcd из 3–5 нод и его латентность на каждое действие |
| Сессия пользователя/кэш | Устаревшая версия допустима — eventual с TTL | Кэш + TTL + инвалидация (06) | Промах = лишний round-trip, а не ошибка |
| Аналитика/метрики | Eventual, потеря части допустима (best-effort) | Очередь + батчи (07), потеря при переполнении чистая | Минус точности метрик |
Правило чтения таблицы: строка означает осознанный выбор, а не «вот это всегда так». Изменение кейса меняет строку: если бизнесу нужна мгновенная глобальная консистентность счётчиков — строка «Счётчики» переезжает на сильный уровень, и цена вырастает на порядок. Три аксиомы, которые стоят за таблицей:
- Слабее согласованность — шире класс аномалий, которые видит пользователь; выбирать её можно только с пониманием, какие именно аномалии допустимы.
- Каждая сильная гарантия — это где-то больше латентности/меньше доступности (PACELC) — называть цену всегда.
- Идемпотентность — общая валюта: в каждой строке, где появляются ретраи и повторы (платёж, лайк, бронь), идемпотентный приём повторов дешевле, чем linearizability ради отсутствия повторов.
8. Прикладное: как это звучит в эталонах
Лента новостей (главный билет — ../classic-designs.md §2). Классика «какую согласованность выбрали»: свой пост — линеаризуемая запись + read-your-writes (виден сразу — читаем с лидера), ленты подписчиков — eventual с лагом в секунды через fan-out в асинхронные проекции; счётчики лайков — eventual с дедупом; посты селебрити — fan-out on read. При сбое — деградация «лента стареет» (08 §7), а не строгая запись. Цена названа явно: «пользователь не видит пост друга секунду-две — терпимо; видит вечно исчезающие счётчики — нет, поэтому лайки идемпотентны». Это и есть ответ на «какую согласованность выбираете».
Мессенджер (§3). Порядок сообщений — фактически causal по переписке (порядок в пределах чата): чат прибит к одной партиции по chat_id, порядок — порядок в партиции, по построению; офлайн-доставка — eventual-проекция. Уникальность message id — без консенсуса (snowflake — статья 10). Отдельная нота: «порядок сообщений — это не linearizability, а согласованность в пределах беседы; глобального порядка нет, и он не нужен».
URL shortener (эталон Яндекса, §1). Чтение доминирует, ничего строгого нет: KV-зеркало + кэш — eventual (статья 11), запись короткой ссылки — попадает в БД (leader) и проекцией в KV; уникальность короткого ключа обязана быть — поэтому генерация ключа на центральном сервисе/последовательности с уникальным индексом (линеаризуемо для ключа, остальное eventual). Согласованность «стоит» только на ключе — и это осознанный размен: «99 % запросов — чтение; свежесть ключа нужна только через секунду после создания — её и гарантируем».
Медиа-хостинг (§4). Метаданные записи — one leader БД; сам файл в S3, его «последняя версия» — eventual по CDN, и это единственный допустимый режим мульти-регион; подтверждение «файл загружен» — по записи в БД, а не по немедленной доступности с CDN. Никаких распределённых транзакций в пайплайне: outbox для событий «upload → transcode → publish», сага для компенсаций при падении транскодера (07 §10/08 §12).
Сквозной приём — после выбора согласованности почти всегда задать второй вопрос себе: «а что видит пользователь, если гарантия не выполнилась?» Если ответ «ничего страшного» — выбор правильный; если «баг» — гарантия сильнее или компенсация.
9. Ошибки этого раздела
| Ошибка | Как выглядит | Противоядие |
|---|---|---|
| CAP как заклинание | «Возьмём AP» без объяснения, в каком разделении и зачем | CAP — только при разделении; PACELC честнее; формула «кейс → гарантия → цена» (§2) |
| Не названа цена | «Нужна strong consistency» — а платить нечем | Цена всегда: латентность/блокировки/меньше доступности/дороже инфраструктура (§2, §7) |
| Linearizability = serializability | Слил два термина в «это когда всё сразу свежее и правильно» | Развести: свежесть vs порядок; strict serializability — оба (§3.1) |
| «У нас eventual» как флаг | Куда ни ткни — «eventual», без выбора кейсов | Таблица §7: eventual — осознанное решение для конкретного типа данных с перечислением аномалий |
| 2PC «по умолчанию» | «Запишем атомарно через 2PC» на эталонном дизайне | Избегаю + три причины; конструктив — outbox + сага (§5.1–5.3) |
| Сага без компенсаций | «Сделаем сагу» — а компенсации не спроектированы | Компенсирующие действия — часть дизайна с первого дня; идемпотентность шагов (§5.2) |
| Кворум = linearizability | «W+R>N — значит сильная согласованность» | Кворум даёт свежесть; linearizability — консенсус/single-leader (§6, 05 §4) |
| Консенсус = «сложно, не надо» | Никогда не упоминается, хотя выборы лидера есть в схеме | Назвать, где консенсус в схеме фактически: failover, fencing, лидерство воркеров (§6) |
| «Поставим свой Raft» | Рисуем свой консенсус в дизайне ленты | Координационные сервисы (ZK/etcd) — готовый консенсус, не изобретаем (§6) |
| Без сессионных гарантий | Eventual, а пользователь видит «потерянный» свой пост | Своё — с лидера, монотонные чтения маршрутизацией (05 §3) — дёшево, а эффект велик |
| LWW по часам | «Свежее по таймстампу» — и тихая потеря данных | Vector clocks или отказ от параллельной записи (§3) |
10. Что назвать на секции (чек-лист раздела)
Обязательные фразы и действия. Полный чек-лист — ../methodology.md §7; канон — ../knowledge-base.md §7.
На этапе 1 (требования): из нефункциональных требований вытащить консистентность: «какую свежесть допускает пользователь?» — это часть требований, а не наш вкус. Зафиксировать на доске рядом с доступностью и latency-SLO.
На этапе 4 (детали + БД): - Назвать выбор согласованности явно с ценой для каждой таблицы/проекции: «посты линейзуемо (свой — с лидера), лента eventual — лаг секунды, счётчики eventual + идемпотентность». Ссылаться на таблицу §7. - Если в схеме есть асинхронная проекция/событие — сказать «outbox в одной транзакции с данными, доставка at-least-once, потребитель идемпотентен» (§5.3, подробно 07). - Если несколько систем пишут процесс — «сага, шаги локально транзакционны, компенсации спроектированы, оркестрация» (§5.2). - Изоляция: «snapshot по умолчанию; счётчики — атомарной записью; инварианты — serializable/SSI» (§4).
На этапе 6 (эксплуатация/отказы): - Выборы и fencing: «детект кворумом → выборы самой свежей реплики → fencing-токен от зомби» (05 §5/08 §8) — и назвать консенсус под этим: «перевыборы лидера — это консенсус на ZK/etcd, эпоха — это fencing». - Мульти-ДЦ: «внутри ДЦ C, между ДЦ — AP с eventual; синхронная запись между континентами недопустима — латентность» (§2, 08 §9). - На «кворумы дают strong» — поправить: «свежесть чтения дают, линейзуемость нет — там консенсус/single-leader» (§6).
Сквозные формулы раздела: - «Кейс → гарантия → цена: называю уровень согласованности, механизм и чем плачу в каждом узле». - «2PC избегаю: точка блокировки и отказа; вместо атомарности многих систем — одна атомарность (БД + outbox), остальное — сага с компенсациями и идемпотентностью». - «Linearizability — свежесть; serializability — порядок; строгие обе гарантии только вместе (strict serializable)». - «Консенсус — там, где нужен общий порядок: выборы, fencing, атомарный CAS; всё остальное — single-leader + кворум с известными оговорками». - «Между ДЦ — AP; внутри ДЦ — C; за любую C платим латентностью (PACELC)».
11. Самопроверка «умею, если»
Критерий раздела из брифа: объясняю, какую согласованность выбираю для конкретного кейса и чем плачу. Проверка с закрытыми материалами: взять три кейса из таблицы §7 (деньги, лента, уникальность ID) и для каждого вслух провести формулу «гарантия → механизм → цена» за 60 секунд; затем ответить на три зонда: (1) «чем linearizability отличается от serializability?», (2) «почему не 2PC?», (3) «где у вас консенсус в схеме?». Маркеры готовности: (1) на «какую согласованность выберете?» вперёд вылетает строка «кейс → гарантия → цена», а не название системы; (2) определение PACELC произносится автоматически вместе с ценой; (3) анкета «а что видит пользователь при рассинхроне?» — обычный ход, а не сюрприз; (4) опасные места (2PC, LWW, кворум ≠ linearizability) называются предупреждающе, без подсказки. Протокол тренировок и рубрика 0–3 — ../methodology.md §6; сверка с эталонами — ../classic-designs.md §1–4. Если таблица §7 «проходит» по любым трём кейсам за минуту каждый и разговор про согласованность всегда заканчивается ценой — раздел готов.
Связанные документы
00-overview.md— обзорная статья по всем 12 разделам брифа (TASK-45.40); §09 — карта раздела01-interview-framework.md— фиксация требований (свежесть как NFR) на этапе 1; возврат к листу на этапе 602-estimations.md— латентности для оценки цены согласованности (RTT, меж-ДЦ)05-replication-sharding.md— механика: лог, sync/async/semi-sync, replication lag и сессионные гарантии (§3), кворумы ≠ linearizability (§4), failover/выборы/fencing (§5)07-queues-streams.md— outbox/CDC (§10), семантики доставки и идемпотентность (§5, §7)08-fault-tolerance.md— отказы нод с данными, fencing, majority-коммит и мульти-ДЦ (§8–9)11-reference-designs.md— эталоны, где выбор согласованности звучит целиком (лента §3, мессенджер §4, медиа §5, shortener §2)../brief.md— бриф: раздел 09 с критерием «умею, если»knowledge-base.md— §7 консистентность и распределённые транзакции (CAP/PACELC, модели, изоляции, 2PC/saga/outbox, консенсус, часы), §13.5 распределённые локиmethodology.md— §4 топ-10 ошибок, §6 протокол тренировок, §7 карточка на секциюclassic-designs.md— §1 shortener, §2 лента, §3 мессенджер, §4 медиаmaterials/ddia-2ed/ch-08-transactions.md— источник: изоляции, гонки, 2PC, NewSQL, zero-2PC (TASK-45.39)materials/ddia-2ed/ch-10-consistency-consensus.md— источник: linearizability vs serializability, CAP/PACELC, консенсус, координационные сервисы, fencing (TASK-45.39)- Соседние статьи:
08-fault-tolerance.md(отказы и fencing — переиспользует язык консенсуса) ·10-components-patterns.md(распределённая генерация ID — обходит консенсус) ·12-operations-observability.md(метрики рассинхрона: replication lag, лаг проекций)