Опирается на правила:
R-DIST-TX-X1…R-DIST-TX-X3иR-DIST-TX-1…R-DIST-TX-3из Distributed Patterns Style Guide → раздел 7. Distributed transactions — что НЕ делать.
Важно знать
- 2PC через JTA/XA в нашем стеке запрещён — Kafka не поддерживает XA, не масштабируется, transaction coordinator — single point of failure, плохо живёт в Kubernetes.
ChainedTransactionManagerSpring — best-effort, не атомарность. Failure между commit-ами двух DS оставляет inconsistency без recovery.- Альтернатива 1: Saga с локальными транзакциями + compensation — стандарт UCP для cross-service flow.
- Альтернатива 2: Outbox + idempotent consumer для event-driven синхронизации без rollback'ов.
- Альтернатива 3: Modular monolith — несколько Bounded Context в одном PG, локальный
@Transactionalработает.- «Но мне нужна immediate consistency между двумя сервисами» — обычно означает либо неверную границу BC, либо реальную EC, которую надо задекларировать.
Двухфазный коммит — старая идея из мира enterprise application servers (JBoss, WebLogic), которая в эпоху Kafka и Kubernetes плохо работает. UCP делает явный отказ: никаких JTA-конфигов в Spring, никаких XADataSource, никаких ChainedTransactionManager.
Почему 2PC/JTA/XA не подходит
R-DIST-TX-X1: причины запрета.
Kafka не поддерживает XA
Наш main broker — Kafka. Apache Kafka не реализует XA-протокол. Это фундаментально: невозможно сделать атомарный «commit в PostgreSQL + send в Kafka» через distributed transaction. Любая попытка построить такое — самообман.
То же касается Redis, ClickHouse, ElasticSearch, S3 — почти весь современный data stack не поддерживает XA. Старые JMS-брокеры (ActiveMQ, IBM MQ) и старые RDBMS (Oracle, DB2) поддерживают, но мы не на них.
2PC synchronous block — не масштабируется
Two-phase commit держит synchronous lock на участвующих ресурсах между фазой prepare и фазой commit. Если в саге 4 шага через 2PC — все 4 ресурса залочены на длительность всей операции (десятки/сотни миллисекунд при сетевых hops).
Под нагрузкой это становится bottleneck-ом: throughput падает в разы, latency растёт, deadlock между перекрывающимися 2PC становится регулярным.
Transaction coordinator — single point of failure
XA требует transaction coordinator — отдельный сервис, который ведёт state каждой distributed transaction (prepare-ready, committed, aborted). Если координатор упал в момент COMMITTED prepare, не отправленный COMMIT — все участвующие ресурсы остаются в IN_DOUBT состоянии до восстановления координатора. Это знаменитые «hanging XID» в Oracle.
В Kubernetes с rolling restart, частыми migration-ами, network partition — coordinator-сервис ломается часто.
Сложно operate в Kubernetes
Конфиг JTA + XA datasources + transaction recovery store + правильный transaction-log-dir на persistent volume + recovery после рестарта — это десятки часов настройки и отладки на проде, плюс новый класс инцидентов, которые команда раньше не видела.
Сложность не покрывается выгодой — потому что есть рабочие альтернативы.
ChainedTransactionManager — не вариант
R-DIST-TX-X3: Spring предоставляет ChainedTransactionManager для multi-datasource — выглядит как distributed transaction, но это best-effort sequence, не атомарность.
// ОПАСНО — выглядит атомарно, но не атомарно
@Bean
public PlatformTransactionManager transactionManager(
@Qualifier("dataSource1") PlatformTransactionManager tm1,
@Qualifier("dataSource2") PlatformTransactionManager tm2
) {
return new ChainedTransactionManager(tm1, tm2);
}
Как это работает:
- Begin tx1, begin tx2.
- Бизнес-логика делает write в обе DS.
- Commit tx2 (вложенный).
- Commit tx1 (внешний).
Что ломается:
- Если commit tx2 прошёл, а commit tx1 упал — у DS2 уже committed данные, у DS1 — rollback. Inconsistency, recovery нет.
- Никаких prepare-фаз, никакого rollback'а уже-committed tx2.
- Документация Spring сама помечает класс
@Deprecated since Spring 5.3.
Использование ChainedTransactionManager для multi-datasource — анти-паттерн. Либо один PG (modular monolith), либо saga (cross-service).
Альтернативы — что использовать вместо 2PC
1. Saga с локальными транзакциями
R-DIST-TX-1: стандарт UCP для cross-service flow. Каждый шаг — локальная @Transactional в одном PG, compensation на сбое. Подробнее — Saga.
[order-service] [payment-service] [inventory-service]
@Transactional @Transactional @Transactional
↓ ↓ ↓
local commit local commit local commit
При сбое: orchestrator вызывает refundPayment, cancelOrder — semantic compensation
Свойства:
- Каждый шаг ACID локально.
- Eventual consistency между сервисами.
- Recovery через
saga_<name>state-таблицу. - Масштабируется горизонтально.
2. Outbox + idempotent consumer
R-DIST-TX-2: для event-driven sync без rollback'ов. Write commits в БД + INSERT в outbox атомарно (одна локальная транзакция в PG), relay публикует в Kafka. Подробнее — Outbox.
@Transactional (одна PG-транзакция)
↓
INSERT order + INSERT outbox_event
↓ commit
[outbox-relay async] → Kafka
↓
receiver (idempotent через processed_event)
Подходит когда compensation не нужна: «опубликовали OrderConfirmed, downstream обновил read-model или отправил уведомление». Если downstream упал — retry в Kafka, receiver идемпотентен.
3. Modular monolith
R-DIST-TX-3: для tight coupling — один процесс, один PG, локальный @Transactional.
@Component
@Transactional
public class CreateOrderHandler {
private final OrderRepository orderRepository;
private final PaymentService paymentService; // same process
private final InventoryService inventoryService; // same process
public Order handle(CreateOrderCommand command) {
var order = orderRepository.save(...);
paymentService.charge(order.id(), command.amount());
inventoryService.reserve(order.id(), command.items());
return order;
}
}
Все три операции в одной PG-транзакции — ACID локально. Никаких саг, никаких compensation, никакого distributed-сложности. Деплой один.
Когда команда вырастает или нужна независимая масштабируемость — разделяем на сервисы, тогда появляются саги. До этого порога monolith проще и надёжнее.
«Но мне нужна immediate consistency между сервисами»
Это требование почти всегда означает одно из трёх:
- Неверная граница BC — две операции, требующие immediate consistency, скорее всего относятся к одному Bounded Context. Объединить в один сервис.
- Реальная eventual consistency — реально immediate не нужна, нужна быстрая (<1s) EC + декларация в API. См. Eventual consistency.
- Read-your-writes — клиенту нужно сразу увидеть свой результат, но не нужна атомарность с другим сервисом. Решается специальным endpoint-ом из write-side.
«Хочу 2PC» — почти всегда симптом, не requirement.
Что запрещено — таблица
| Антипаттерн | Правило | Что взамен |
|---|---|---|
| JTA / XA / 2PC для cross-service | R-DIST-TX-X1 | saga с локальными транзакциями |
JtaTransactionManager в @Configuration | R-DIST-TX-X1 | стандартный JpaTransactionManager |
XADataSource для PG + Kafka | R-DIST-TX-X1 | outbox |
ChainedTransactionManager multi-DS | R-DIST-TX-X3 | один PG (modular monolith) или saga |
@Transactional поверх HTTP-вызова другого сервиса | R-DIST-TX-X1 | outbox + saga |
| «Distributed transaction через Try-Cancel-Confirm руками» | R-DIST-TX-X1 | стандартизованная saga |
Куда дальше
- Distributed Patterns → раздел 7. Distributed transactions — нормативные формулировки.
- Saga — главная альтернатива 2PC.
- Outbox + Inbox — атомарный «commit + publish» через локальную транзакцию.
- Eventual consistency — что делать с требованием «immediate».
- Когда нужны распределённые паттерны — modular monolith как альтернатива.
- Архитектурный выбор: монолит или микросервисы — критерии разделения.