Перейти к содержанию

2PC / XA

Что такое 2PC

2PC (Two-Phase Commit) — это протокол, который пытается атомарно завершить одну транзакцию сразу на нескольких участниках. Идея в том, что есть coordinator и participants: сначала coordinator спрашивает всех "сможешь commit'ить?", а потом, если все согласны, рассылает финальное решение COMMIT; если хотя бы один не готов, рассылается ROLLBACK.

Главная цель:

сделать так, чтобы все участники либо commit'нули, либо все откатились.


Где это вообще нужно

2PC нужен, когда одно логическое действие затрагивает:

  • несколько БД;
  • БД + JMS/XA-очередь;
  • несколько XA-совместимых ресурсов;
  • несколько resource manager'ов под управлением одного transaction manager.

Пример:

перевод денег:
  update DB-1
  update DB-2

хочется:
  либо обе записи committed
  либо ни одной

Внутри одной БД эта проблема решается обычной локальной транзакцией. Между независимыми ресурсами нужен протокол координации.


Кто участвует в 2PC

В 2PC есть две главные роли.

Coordinator / Transaction Manager:

  • открывает distributed transaction;
  • собирает участников;
  • инициирует prepare;
  • принимает финальное решение;
  • пишет его в свой лог;
  • рассылает commit или rollback.

В Java-мире это обычно роль JTA transaction manager.

Participants / Resource Managers:

  • принимают локальные изменения;
  • умеют отвечать на prepare;
  • умеют зафиксировать commit;
  • умеют сделать rollback;
  • удерживают нужные блокировки до финального решения.

В XA-мире это обычно:

  • XA-совместимая БД;
  • XA-совместимый JMS provider;
  • другой XA resource manager.

Что такое XA

XA — это стандартный интерфейс взаимодействия между transaction manager и resource manager. Идея XA в том, что coordinator не знает внутренних деталей конкретной БД, а работает через стандартный контракт; resource manager участвует в distributed transaction как XA participant.

В Java/Spring это обычно связано с:

  • JtaTransactionManager
  • XADataSource
  • XAConnectionFactory

Важно помнить:

  • XA + XA -> настоящий 2PC возможен;
  • XA + non-XA -> это уже компромисс, а не строгая глобальная атомарность;
  • non-XA + non-XA -> настоящий 2PC не получится.

Как работает 2PC

2PC состоит из двух фаз.

Фаза Prepare:

  • coordinator говорит всем участникам prepare;
  • каждый participant выполняет локальную подготовку;
  • пишет нужную информацию в свой лог;
  • проверяет, может ли гарантированно commit'нуть позже;
  • удерживает блокировки и ресурсы;
  • отвечает либо PREPARED, либо ABORT.

Смысл prepare в том, что участник еще не commit'нул окончательно, но уже обещает, что если придет COMMIT, он сможет его завершить.

Фаза Commit / Rollback:

  • если все участники ответили PREPARED, coordinator пишет в свой лог финальное решение COMMIT и рассылает всем commit;
  • если хотя бы один участник сказал ABORT, coordinator пишет ROLLBACK и рассылает всем rollback.

Happy path выглядит так:

Coordinator -> RM1: PREPARE
Coordinator -> RM2: PREPARE

RM1 -> Coordinator: PREPARED
RM2 -> Coordinator: PREPARED

Coordinator writes COMMIT decision

Coordinator -> RM1: COMMIT
Coordinator -> RM2: COMMIT

Почему 2PC считается тяжелым

2PC считается тяжелым по нескольким причинам.

Во-первых, это блокирующий протокол. После PREPARE участник уже не может просто забыть о транзакции, удерживает состояние до финального решения и часто держит блокировки и ресурсы. Если coordinator пропал в плохой момент, участник может зависнуть в prepared state и ждать.

Во-вторых, у него выше latency: нужны минимум две фазы взаимодействия, больше сетевых round-trip, больше дисковых записей в логи и больше coordination overhead.

В-третьих, хуже availability: если для commit нужен ответ и согласованность нескольких участников, то отказ любого из них влияет на весь flow.

В-четвертых, выше связанность: участники должны поддерживать XA, доверять общему coordinator и жить в общей модели distributed transaction.

Именно поэтому для независимых микросервисов это часто плохой fit.


Failure Modes

У 2PC есть несколько неприятных failure scenarios.

Если participant упал до ответа на PREPARE, coordinator не получает PREPARED и финальное решение — ROLLBACK. Это относительно простой случай.

Если participant упал после PREPARE, но до COMMIT, ситуация уже неприятнее: участник уже обещал commit, удерживает состояние и не знает финального решения, пока не восстановится и не узнает его у coordinator.

Самый известный плохой сценарий — сбой coordinator после того, как все ответили PREPARED. Если coordinator успел собрать PREPARED, но не все узнали финальное решение, участники могут зависнуть в uncertain state, ресурсы удерживаются, а доступность падает.

Именно поэтому 2PC называют blocking protocol.


Почему 2PC плохо ложится на микросервисы

Микросервисная архитектура обычно строится вокруг идей:

  • независимое владение данными;
  • слабая связанность;
  • асинхронность;
  • отказоустойчивость через автономность сервисов.

2PC идет в обратную сторону:

  • общий coordinator;
  • общее commit-решение;
  • общий failure domain;
  • блокировки и ожидание;
  • XA-поддержка у всех участников.

Поэтому в микросервисах 2PC часто считают:

  • слишком тяжелым;
  • слишком хрупким;
  • слишком дорогим по latency и ops complexity.

Это не значит, что 2PC "плохой всегда". Это значит, что для автономных сервисов он редко является дефолтным выбором.


Где 2PC еще может быть оправдан

2PC еще может быть оправдан там, где:

  • controlled environment;
  • ограниченное число участников;
  • реально нужны строгие atomic guarantees;
  • участники все внутри одной организации и под единым управлением;
  • все ресурсы XA-capable;
  • команда готова нести operational cost.

Типичные сценарии:

  • enterprise-система с несколькими XA-ресурсами;
  • старые integration use case;
  • отдельные финансовые процессы, где compromise unacceptable и среда строго контролируется.

Где 2PC обычно плохой выбор

2PC обычно плохой выбор там, где:

  • длинный бизнес-процесс идет минуты или часы;
  • есть микросервисы с независимым владением данными;
  • участвуют внешние HTTP API;
  • участвуют системы без XA;
  • используется Kafka/event-driven pipeline;
  • важны availability и decoupling.

Если один из шагов уже отправил email, дернул сторонний payment gateway или вызвал внешний HTTP API, идея "сделаем один глобальный rollback" обычно уже не соответствует реальности.


2PC vs Saga

2PC говорит:

не commit'им никого, пока не убедимся, что commit'ить могут все

Saga говорит:

каждый шаг делает свой локальный commit,
а при проблеме дальше выполняем компенсации

2PC сильнее по атомарности, но тяжелее по координации. Saga слабее по мгновенной консистентности, но лучше по автономности и микросервисному fit.


Что важно запомнить

  • 2PC пытается дать глобальную атомарность между несколькими участниками.
  • Для этого нужен coordinator и protocol prepare -> commit/rollback.
  • XA — стандартный интерфейс для участия ресурсов в такой транзакции.
  • 2PC блокирующий, дорогой по coordination и latency.
  • Именно поэтому в микросервисах чаще выбирают Saga + Outbox, а не XA.