Цели главы. Атомарность «всё или ничего» поверх нескольких узлов — исторически самое больное место распределённых систем. Разберём двухфазную фиксацию с её врождённой блокирующей природой, посмотрим, как современные распределённые СУБД вылечили её консенсусом и работой со временем (Percolator, Spanner, HLC-семейство), и спустимся к прагматике микросервисного мира: саги с компенсациями и transactional outbox. Сквозная линия главы — за каждым решением стоит теорема из главы 3, и честный инженер знает, какую цену и за что он платит.
Транзакция затрагивает данные на нескольких узлах (шардирование — глава 8, разные сервисы со своими БД). Требования классические — ACID, но в распределённом контексте их надо расщепить, чтобы не путать (обещали в главе 4): атомарность — либо все узлы применили свои части, либо ни один; изоляция — конкурентные транзакции не видят промежуточных состояний друг друга (уровни: read committed, snapshot isolation, serializable); согласованность реплик из главы 4 — третья, отдельная ось. Глава в основном об атомарности и изоляции; их переплетение со временем — в 9.4.
Классика (Грей, 1978). Координатор ведёт транзакцию с участниками:
Рис. 9.1. После ответа READY участники зависят от доступности устойчиво записанного глобального вердикта.
Протокол корректен — и блокирующий: участник, сказавший «готов» и потерявший координатора, застрял. Откатить нельзя (обещал), зафиксировать нельзя (решения не знает — вдруг сосед ответил «не готов»), остаётся ждать восстановления координатора или иного источника его устойчиво записанного решения — с удержанными блокировками, останавливая всё, что касается тех же данных. Это не мелкая недоработка: атомарная фиксация тесно связана с консенсусом, но не совпадает с ним — у неё другое условие обоснованности (голос «не готов» обязан привести к откату). Трёхфазный 3PC устраняет некоторые блокировки лишь при более сильных временных предположениях и отсутствии сетевых разделений. В обычном 2PC координатор единолично записывает глобальный вердикт; пока этот вердикт недоступен, подготовившийся участник не может безопасно угадать исход.
Практические следствия: legacy-механизмы XA (2PC между разнородными системами — БД + брокер сообщений) в эксплуатации регулярно оставляют «зависшие» транзакции, требующие ручного вердикта администратора; современная практика избегает 2PC между организациями и разнородными системами, оставляя его там, где обе фазы — внутри одной управляемой системы. Что подводит к правильной починке.
Диагноз ясен — единственная точка истины; практическое лечение из главы 6 — реплицировать её. В архитектуре Spanner (и родственных распределённых СУБД) каждый шард — не узел, а группа консенсуса (Paxos/Raft): и состояние участника «готов», и решение координатора — записи в реплицируемых журналах, переживающие отказ меньшинства. После отказа лидера роль координатора может принять новый лидер группы вместе с журналом. Это убирает блокировку при одиночном отказе, но не превращает атомарную фиксацию в безусловно неблокирующую: если группа координатора или участника не может собрать большинство, транзакция не продвигается. Цена доступности решения — кворумы, частичная синхронность и работоспособные большинства.
Атомарность — полдела; конкурентным транзакциям нужна изоляция, а её распределённая реализация упирается в знакомый по главе 2 вопрос: что значит «раньше» между шардами?
Итог раздела для архитектора: «распределённый ACID» в 2026 году — реальность, но порядок транзакций всегда имеет цену. Системы по-разному сочетают логических координаторов и раздатчики меток, аппаратное время, гибридные часы, ожидания и перезапуски; свести все реализации ровно к трём взаимоисключающим вариантам нельзя.
Микросервисный мир (у каждого сервиса своя БД, между ними — сеть и чужие команды) от 2PC отказался почти повсеместно — блокировки через границы сервисов недопустимы ни технически, ни организационно. Ответ — сага (Garcia-Molina, Salem, 1987): длинная операция = последовательность локальных транзакций T₁…Tₙ, каждой из которых сопоставлена компенсация C₁…Cₙ; при провале Tₖ исполняются C(k−1)…C₁ в обратном порядке. Бронирование поездки: T₁ — списать деньги, T₂ — билет, T₃ — отель; отель не вышел — вернуть билет, вернуть деньги. Координация — оркестрация (явный управляющий процесс с журналом состояния саги; проще отлаживать) либо хореография (сервисы реагируют на события друг друга; меньше связность, труднее видеть целое).
Цена, о которой обязана знать команда: сага не даёт ACID-атомарности и не изолирована. Между T₁ и компенсацией мир видит промежуточное состояние (деньги списаны, поездки нет), другие транзакции могут на него опереться; компенсация обычно является новой бизнес-операцией, а не буквальным возвратом прошлого состояния. Приёмы смягчения — семантические: резервирование вместо списания (T₁ «заморозить», подтверждение отдельным шагом), статусы «в обработке», запрет действий над объектами незавершённых саг. Каждый шаг и каждая компенсация должны быть идемпотентны, а их выполнение — записано в устойчивом журнале и повторяться после сбоев. Неустранимая ошибка компенсации требует сверки и ручного разрешения; необратимые шаги по возможности ставят последними.
Самая частая микро-версия проблемы главы: сервис должен атомарно изменить свою БД и отправить событие в брокер (глава 10). Наивные порядки ломаются оба: «БД, потом брокер» теряет событие при падении между ними; «брокер, потом БД» рождает событие о несостоявшемся факте. 2PC между БД и брокером (XA) — см. 9.2. Каноническое решение — outbox: событие пишется в ту же БД, в ту же локальную транзакцию, в таблицу-исходящую; отдельный доставщик (поллер или чтение журнала БД — CDC, Debezium) переносит записи в брокер и помечает отправленными. Атомарность куплена локальной транзакцией; доставка — at-least-once, потребитель обязан быть идемпотентным (дежавю? — это ровно связка «at-least-once + дедупликация» из главы 1, ставшая паттерном). Outbox — мост в главу 10, где журналы событий станут главным героем.
Ответы и указания. 1: в данном исполнении участники ждут, пока станет доступна запись COMMIT. Их локальные состояния неотличимы от другого исполнения, где те же двое готовы, третий проголосовал против и координатор записал ABORT; поэтому решение по опросу только готовых соседей небезопасно. 2: предложивший 0 голосует «не готов», предложивший 1 — «готов»; COMMIT отображается в 1, ABORT — в 0. При всех единицах получается 1, а при любом нуле решение 0 предложено хотя бы одним узлом. Неблокирующий решатель атомарной фиксации тем самым решил бы бинарный консенсус, что противоречит FLP в указанной модели. 3: A видит версии с commit_ts ≤ start_ts: при 105 запись невидима, при 98 видима. Снимок не смешивает состояния одной зафиксированной транзакции. 4: Spanner выбирает метку фиксации не раньше TT.now().latest и ждёт до TT.after(commit_ts); фактическое ожидание определяется текущей неопределённостью, а не механически всегда равно одной константе. Если истинное время выйдет из заявленного интервала, внешний порядок может нарушиться. Системы с HLC вместо TrueTime контролируют максимальный сдвиг часов и в зависимости от протокола ждут, продвигают метки или перезапускают конфликтующие операции. 5: резерв товара компенсируется снятием резерва, авторизация оплаты — отменой или возвратом, создание доставки — отменой задания до передачи перевозчику. Физически отправленная посылка необратима и потому идёт последней; остаток в середине показывает «зарезервировано» отдельно от «доступно», а конкурентные операции не расходуют резерв повторно. 6: outbox содержит event_id, aggregate_id, payload, created_at и sent_at; бизнес-транзакция меняет предметную таблицу и вставляет outbox-строку. Доставщик выбирает непомеченные строки с блокировкой, публикует event_id и затем ставит sent_at. Падение после публикации до sent_at создаёт повтор, поэтому потребитель атомарно записывает event_id вместе со своим эффектом и на повторе ничего не делает.