- Transactional Outbox. Как не потерять сообщения
- 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 знают, как взаимодействовать для этого сценария.
Процесс делится на две фазы.
Фаза 1: Подготовка (Prepare Phase)
Координатор (TM) опрашивает всех участников: «Готовы ли вы закоммитить XID?».
- Участники выполняют работу, сбрасывают данные в лог (WAL) и ставят необходимые блокировки.
- Важно: RM обязан гарантировать, что если он ответил
YES, то он сможет выполнить коммит даже после внезапной перезагрузки. - Если хоть один RM ответил
NOили не ответил по тайм-ауту — всё отменяется.
Фаза 2: Фиксация (Commit Phase)
Если все ответили YES, координатор сначала записывает решение в свой собственный лог, а затем рассылает команду COMMIT.
- Если кто-то из RM упал, TM будет повторять попытки коммита до победного.
- Если упал сам TM, он поднимется, прочитает свой лог и завершит начатое.
eXtended Architecture - спецификация для реализации 2PC
Если 2PC - это алгоритм, то XA (eXtended Architecture) — это стандарт API (интерфейс).
Если ваша база данных поддерживает XA, значит, TM может управлять ею через стандартные вызовы: xa_prepare, xa_commit, xa_rollback.
Пример XA
Представим, что мы переводим деньги из Банка А (RM1) в Банк Б (RM2).
- AP просит TM начать глобальную транзакцию; TM выдает уникальный ID транзакции (XID).
- AP вызывает RM1: “Сними 100 рублей” (в контексте XID).
- AP вызывает RM2: “Зачисли 100 рублей” (в контексте XID).
- AP говорит TM: “Я закончил, давай коммитить”.
- TM делает
xa_prepareдля RM1. RM1 отвечает: “ОК”. - TM делает
xa_prepareдля RM2. RM2 отвечает: “ОК”. - 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
- Блокировки: Пока транзакция находится в состоянии in-doubt (между фазами), данные заблокированы.
- Проблема Изоляции: 2PC не гарантирует, что клиент не увидит промежуточное состояние. Один RM может уже сделать
COMMIT, а второй еще нет. - Производительность: Минимум два сетевых RTT и две принудительные записи на диск (fsync).
- Сложность эксплуатации: Нужно мониторить «зависшие» (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-группе!