Hard📖Теория3 min

Распределённые транзакции

2PC, 3PC, Saga pattern (choreography vs orchestration), TCC -- паттерны согласованности данных между сервисами

Распределённые транзакции

Проблема распределённых транзакций

В монолите с одной БД транзакция гарантирует ACID. В микросервисной архитектуре данные разбросаны по нескольким сервисам и БД. Как обеспечить согласованность?

Перевод денег: Account A → Account B

Монолит:                          Микросервисы:
┌────────────────────┐            ┌──────────┐  ┌──────────┐
│  BEGIN              │            │ Service A │  │ Service B │
│  UPDATE accounts   │            │ DB_A      │  │ DB_B      │
│    SET balance -100│            └─────┬─────┘  └─────┬─────┘
│    WHERE id = A;   │                  │              │
│  UPDATE accounts   │                  │   Как быть?  │
│    SET balance +100│                  │              │
│    WHERE id = B;   │            Если A списал,       │
│  COMMIT;           │            а B не зачислил?     │
└────────────────────┘

Подходы к распределённым транзакциям

Подход Согласованность Доступность Сложность
2PC Strong Низкая (блокирующий) Средняя
3PC Strong Выше (не блокирующий) Высокая
Saga Eventual Высокая Средняя
TCC Eventual (с резервированием) Высокая Высокая

Two-Phase Commit (2PC)

Протокол

2PC -- классический протокол атомарного коммита, координируемый одним узлом (координатором).

Coordinator         Participant A      Participant B
     │                    │                   │
     │ ── Phase 1: PREPARE ──────────────────│
     │                    │                   │
     │  PREPARE           │                   │
     │───────────────────>│                   │
     │───────────────────────────────────────>│
     │                    │                   │
     │  VOTE YES          │                   │
     │<───────────────────│                   │
     │<───────────────────────────────────────│
     │                    │                   │
     │ ── Phase 2: COMMIT ───────────────────│
     │                    │                   │
     │  COMMIT            │                   │
     │───────────────────>│                   │
     │───────────────────────────────────────>│
     │                    │                   │
     │  ACK               │                   │
     │<───────────────────│                   │
     │<───────────────────────────────────────│
     │                    │                   │
     ▼  Transaction committed ✓              ▼

Фаза 1: Prepare (Голосование)

  1. Координатор отправляет PREPARE всем участникам
  2. Каждый участник:
    • Выполняет все операции транзакции
    • Записывает в WAL (write-ahead log)
    • Блокирует ресурсы
    • Отвечает YES (готов) или NO (не готов)

Фаза 2: Commit/Abort (Решение)

  • Если все ответили YES → координатор отправляет COMMIT
  • Если хотя бы один ответил NO → координатор отправляет ABORT

Проблемы 2PC

Coordinator         Participant A      Participant B
     │                    │                   │
     │  PREPARE           │                   │
     │───────────────────>│                   │
     │───────────────────────────────────────>│
     │                    │                   │
     │  VOTE YES          │                   │
     │<───────────────────│                   │
     │<───────────────────────────────────────│
     │                    │                   │
     X (Coordinator crashes!)                 │
                          │                   │
                     Participant A:       Participant B:
                     Ресурсы заблокированы!  Ресурсы заблокированы!
                     Ждёт решения...      Ждёт решения...
                     (ЗАБЛОКИРОВАН)        (ЗАБЛОКИРОВАН)
Проблема Описание
Blocking Если координатор упал после PREPARE -- участники заблокированы
Single point of failure Координатор -- единая точка отказа
Latency Минимум 2 roundtrip (4 сообщения на участника)
Lock duration Ресурсы заблокированы на время всего протокола

Где используется 2PC

  • XA Transactions -- стандарт распределённых транзакций (Java EE, .NET)
  • PostgreSQL -- PREPARE TRANSACTION / COMMIT PREPARED
  • MySQL -- XA transactions между InnoDB instances

Three-Phase Commit (3PC)

3PC добавляет промежуточную фазу для предотвращения блокировки.

Coordinator      Participant A      Participant B
     │                │                   │
     │ Phase 1: PREPARE                   │
     │───────────────>│                   │
     │───────────────────────────────────>│
     │  VOTE YES      │                   │
     │<───────────────│                   │
     │<───────────────────────────────────│
     │                │                   │
     │ Phase 2: PRE-COMMIT                │
     │───────────────>│                   │
     │───────────────────────────────────>│
     │  ACK           │                   │
     │<───────────────│                   │
     │<───────────────────────────────────│
     │                │                   │
     │ Phase 3: COMMIT                    │
     │───────────────>│                   │
     │───────────────────────────────────>│

Ключевое отличие: PRE-COMMIT фаза гарантирует, что если координатор упал, участники знают: решение было "commit". Они могут завершить транзакцию самостоятельно по таймауту.

3PC vs 2PC

Аспект 2PC 3PC
Блокировка при отказе координатора Да Нет
Число раундов 2 3
Сложность Средняя Высокая
Network partitions Проблема Может привести к несогласованности
Использование в практике Широкое Редкое

Вывод: 3PC решает проблему блокировки, но не решает проблему network partitions. На практике используется редко -- Saga pattern предпочтительнее.

Saga Pattern

Saga -- паттерн, в котором длинная транзакция разбивается на последовательность локальных транзакций. Каждая локальная транзакция имеет компенсирующее действие для отката.

Концепция

Saga: Бронирование путешествия

T1: Забронировать рейс        C1: Отменить бронь рейса
T2: Забронировать отель        C2: Отменить бронь отеля
T3: Забронировать машину       C3: Отменить бронь машины
T4: Списать оплату             C4: Вернуть деньги

Успешный путь:
T1 → T2 → T3 → T4 ✓

Отказ на T3:
T1 → T2 → T3(fail) → C2 → C1 (compensating transactions)

Choreography Saga (Хореография)

Каждый сервис слушает события и решает, что делать дальше. Нет центрального координатора.

┌──────────┐    OrderCreated     ┌──────────┐
│  Order   │ ──────────────────> │ Payment  │
│ Service  │                     │ Service  │
└──────────┘                     └────┬─────┘
     ▲                                │
     │                          PaymentCompleted
     │                                │
     │                                ▼
     │                          ┌──────────┐
     │   ShippingCompleted      │ Shipping │
     │ <─────────────────────── │ Service  │
     │                          └──────────┘
     │
     ▼ OrderCompleted

При ошибке -- обратная цепочка:
ShippingFailed → PaymentService.refund() → OrderService.cancel()

Плюсы хореографии:

  • Нет единой точки отказа
  • Слабая связанность между сервисами
  • Простая для 2-4 шагов

Минусы хореографии:

  • Сложно отследить общий статус
  • Циклические зависимости
  • Сложно добавить новый шаг
  • Трудно тестировать end-to-end

Orchestration Saga (Оркестрация)

Центральный оркестратор управляет последовательностью шагов.

                    ┌──────────────┐
                    │  Saga        │
                    │ Orchestrator │
                    └──────┬───────┘
                           │
           ┌───────────────┼───────────────┐
           │               │               │
           ▼               ▼               ▼
     ┌──────────┐   ┌──────────┐   ┌──────────┐
     │ Payment  │   │ Inventory│   │ Shipping │
     │ Service  │   │ Service  │   │ Service  │
     └──────────┘   └──────────┘   └──────────┘

Orchestrator:
1. → PaymentService.charge()      ✓
2. → InventoryService.reserve()   ✓
3. → ShippingService.ship()       ✗ FAIL
4. → InventoryService.release()   (compensate)
5. → PaymentService.refund()      (compensate)

Плюсы оркестрации:

  • Легко отследить статус саги
  • Простая логика компенсации
  • Проще добавить новые шаги
  • Легко тестировать

Минусы оркестрации:

  • Оркестратор -- потенциальная точка отказа
  • Более сильная связанность
  • Риск "god service"

Choreography vs Orchestration

Критерий Choreography Orchestration
Связанность Слабая Умеренная
Центральная точка отказа Нет Оркестратор
Наблюдаемость Сложная Простая
Добавление шагов Сложно Просто
Подходит для 2-4 простых шагов 4+ шагов, сложная логика
Примеры Простые event-driven Order processing, booking

Semantic Lock (Семантическая блокировка)

Проблема Saga: между шагами данные видны другим транзакциям (нет изоляции). Решение -- semantic lock:

Order Status Flow:

PENDING → PROCESSING → COMPLETED
              │
              └──→ COMPENSATING → CANCELLED

Другие сервисы проверяют статус:
if (order.status == PROCESSING) {
    // Wait or reject -- order is in saga
}

TCC (Try-Confirm/Cancel)

TCC -- паттерн, в котором каждый сервис реализует три операции:

┌─────────────────────────────────────────────┐
│                    TCC                       │
│                                              │
│  Try:     Резервировать ресурсы              │
│           (но НЕ выполнять действие)         │
│                                              │
│  Confirm: Подтвердить резервирование         │
│           (выполнить действие)               │
│                                              │
│  Cancel:  Отменить резервирование            │
│           (освободить ресурсы)               │
│                                              │
└─────────────────────────────────────────────┘

Пример: покупка билета

Coordinator        TicketService       PaymentService
     │                   │                    │
     │ ── Try Phase ─────│────────────────────│
     │                   │                    │
     │  Try              │                    │
     │──────────────────>│                    │
     │  reserve(seat=5A) │                    │
     │  Reserved ✓       │                    │
     │<──────────────────│                    │
     │                   │                    │
     │──────────────────────────────────────>│
     │                   │   Try              │
     │                   │   hold($100)       │
     │                   │   Held ✓           │
     │<──────────────────────────────────────│
     │                   │                    │
     │ ── Confirm Phase ─│────────────────────│
     │                   │                    │
     │  Confirm          │                    │
     │──────────────────>│                    │
     │  assign(seat=5A)  │                    │
     │──────────────────────────────────────>│
     │                   │   Confirm          │
     │                   │   charge($100)     │
     │                   │                    │
     ▼  Transaction done ✓                   ▼

TCC vs Saga

Аспект TCC Saga
Изоляция Через резервирование Нет (semantic lock)
Откат Cancel (чистая отмена) Compensating tx (может быть сложно)
Бизнес-логика Try/Confirm/Cancel в каждом сервисе Forward + Compensating в каждом сервисе
Подходит для Резервирование ресурсов Длинные бизнес-процессы
Сложность API Высокая (3 эндпоинта) Средняя (2 операции)

Outbox Pattern

Outbox Pattern решает проблему атомарной публикации событий вместе с записью в БД.

Проблема:
1. UPDATE orders SET status='paid'     ← успех
2. PUBLISH event 'OrderPaid'            ← сервис упал!
   Событие потеряно!

Решение -- Outbox:
┌─────────────────────────────────────────┐
│  BEGIN TRANSACTION                       │
│    UPDATE orders SET status='paid';     │
│    INSERT INTO outbox_events            │
│      (type, payload)                    │
│      VALUES ('OrderPaid', '...');       │
│  COMMIT;                                │
└─────────────────────────────────────────┘

Relay Process (CDC / polling):
  outbox_events → Kafka/RabbitMQ

Способы чтения Outbox

Способ Описание Плюсы Минусы
Polling Периодический SELECT из outbox Простой Задержка, нагрузка на БД
CDC (Debezium) Чтение WAL БД Real-time, нет нагрузки Сложная инфраструктура
Listen/Notify PostgreSQL LISTEN/NOTIFY Быстро Только PostgreSQL

Idempotency (Идемпотентность)

В распределённых транзакциях сообщения могут дублироваться. Каждый шаг должен быть идемпотентным.

// Idempotency key ensures exactly-once processing
POST /api/payments
Headers:
  Idempotency-Key: "order-123-payment-v1"

Body: { "amount": 100, "order_id": "123" }

First call:  Process payment → 200 OK
Second call: Return cached result → 200 OK (no double charge)

Что говорить на интервью

  1. 2PC -- для strong consistency между 2-3 сервисами, но blocking
  2. Saga -- для eventual consistency, предпочтительнее 2PC в микросервисах
  3. Orchestration -- для сложных flow (5+ шагов)
  4. Choreography -- для простых flow (2-3 шага)
  5. TCC -- когда нужна изоляция через резервирование
  6. Outbox -- для гарантии публикации событий
  7. Idempotency -- обязательна для любого подхода

Проверь себя

🧪

Какую проблему решает Outbox Pattern?

🧪

Чем Saga Orchestration отличается от Choreography?

🧪

Когда Saga Choreography предпочтительнее Orchestration?

🧪

Что делает фаза Try в паттерне TCC?

🧪

Какая главная проблема протокола 2PC?