1. Transactional Outbox. Как не потерять сообщения
  2. Two-Phased Commit и eXtended Architecture

В мире монолитных приложений всё было просто: одна база, один коннект, одна транзакция - BEGIN, COMMIT или ROLLBACK - и вы получаете предсказуемый ACID.

Современная архитектура сложнее: микросервисы, шарды, брокеры сообщений.
Как сделать так, чтобы изменения в нескольких ресурсах (например, в двух разных БД) прошли атомарно - либо везде, либо нигде?
Классический ответ на этот вопрос - двухфазный коммит (2PC) и спецификация XA.

Коротко по терминам

  • ACID - здесь мы боремся в основном за A (атомарность), так как глобальная I (изоляция) в 2PC без дополнительных блокировщиков часто не гарантируется.
  • AP (Application Program) - приложение, логика которого требует транзакции.
  • TM (Transaction Manager, координатор) - «дирижёр», который помнит состояние глобальной транзакции и раздаёт команды.
  • RM (Resource Manager, участник) - БД или очередь, поддерживающая протокол XA.
  • XID - глобальный ID транзакции, общий для всех участников.

Что такое 2PC (Two-Phase Commit)

2PC — это протокол атомарного коммита для глобальной транзакции, в которой участвуют несколько ресурсов.
Его цель — согласованно принять решение: COMMIT или ROLLBACK для всей транзакции.

В протоколе участвуют:

  • Координатор (TM) — управляет процессом.
  • Участники (RM) — базы данных, брокеры сообщений и т.д.

2PC — не общий алгоритм распределённого консенсуса в смысле Paxos или Raft; он решает узкую задачу - согласование одного коммит/отката - и предполагает, что TM и RM знают, как взаимодействовать для этого сценария.

Процесс делится на две фазы.

Two-Phase Commit

Фаза 1: Подготовка (Prepare Phase)

Координатор (TM) опрашивает всех участников: «Готовы ли вы закоммитить XID?».

  1. Участники выполняют работу, сбрасывают данные в лог (WAL) и ставят необходимые блокировки.
  2. Важно: RM обязан гарантировать, что если он ответил YES, то он сможет выполнить коммит даже после внезапной перезагрузки.
  3. Если хоть один RM ответил NO или не ответил по тайм-ауту — всё отменяется.

Фаза 2: Фиксация (Commit Phase)

Если все ответили YES, координатор сначала записывает решение в свой собственный лог, а затем рассылает команду COMMIT.

  1. Если кто-то из RM упал, TM будет повторять попытки коммита до победного.
  2. Если упал сам TM, он поднимется, прочитает свой лог и завершит начатое.

eXtended Architecture - спецификация для реализации 2PC

Если 2PC - это алгоритм, то XA (eXtended Architecture) — это стандарт API (интерфейс).
Если ваша база данных поддерживает XA, значит, TM может управлять ею через стандартные вызовы: xa_prepare, xa_commit, xa_rollback.

Пример XA

Представим, что мы переводим деньги из Банка А (RM1) в Банк Б (RM2).

  1. AP просит TM начать глобальную транзакцию; TM выдает уникальный ID транзакции (XID).
  2. AP вызывает RM1: “Сними 100 рублей” (в контексте XID).
  3. AP вызывает RM2: “Зачисли 100 рублей” (в контексте XID).
  4. AP говорит TM: “Я закончил, давай коммитить”.
  5. TM делает xa_prepare для RM1. RM1 отвечает: “ОК”.
  6. TM делает xa_prepare для RM2. RM2 отвечает: “ОК”.
  7. TM (видя два “ОК”) записывает решение в свой лог и вызывает xa_commit для обоих ресурсов.

Пример в PostgreSQL

Postgres поддерживает двухфазный коммит через SQL-команды. Это позволяет использовать его в XA-цепочках:

-- Шаг 1: Работаем как обычно
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

-- Шаг 2: Вместо COMMIT говорим "подготовиться"
PREPARE TRANSACTION 'my_global_xid_42';

-- На этом этапе данные записаны в лог, но не видны другим транзакциям.
-- База гарантирует, что коммит будет возможен.

-- Шаг 3: Когда координатор даст отмашку:
COMMIT PREPARED 'my_global_xid_42';
-- Или, если случилась беда:
ROLLBACK PREPARED 'my_global_xid_42';

Транзакция в состоянии PREPARED удерживает блокировки и живет вечно, даже если ваше приложение упало. Если её не закрыть (забыть XID), она не даст очищать таблицу (Vacuum), что приведет к деградации всей базы.

Проблемы и недостатки 2PC/XA

  1. Блокировки: Пока транзакция находится в состоянии in-doubt (между фазами), данные заблокированы.
  2. Проблема Изоляции: 2PC не гарантирует, что клиент не увидит промежуточное состояние. Один RM может уже сделать COMMIT, а второй еще нет.
  3. Производительность: Минимум два сетевых RTT и две принудительные записи на диск (fsync).
  4. Сложность эксплуатации: Нужно мониторить «зависшие» (orphaned) транзакции на всех RM.

Альтернативы: когда 2PC не подходит

Если ваши ресурсы не поддерживают XA (как Redis, Kafka или сторонние API), используйте другие подходы:

  • Saga - последовательность локальных транзакций с компенсирующими операциями. Подходит для eventual consistency; есть два стиля: choreography и orchestration.
  • TCC (Try-Confirm-Cancel) - более строгая модель: Try резервирует ресурсы, Confirm применяет, Cancel освобождает; требует явной реализации confirm/cancel.
  • Outbox Pattern - атомарное сохранение события в таблице outbox в рамках локальной транзакции, затем отдельный процесс (Debezium/CDC) публикует событие в брокер; устраняет необходимость XA между DB и брокером.
  • idempotency + retries + deduplication - практические техники для обеспечения надёжности при eventual consistency.

Комментарии в Telegram-группе!