Что такое распределенная транзакция в контексте баз данных и микросервисов?

«Что такое распределенная транзакция в контексте баз данных и микросервисов?» — вопрос из категории Архитектура, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Распределенная транзакция — это операция, которая охватывает несколько независимых ресурсов (разные базы данных, очереди сообщений, микросервисы) и должна быть выполнена как единое атомарное действие (ACID на уровне распределенной системы). Это означает «все или ничего»: либо все участники фиксируют изменения, либо все откатываются.

Классический подход: Двухфазный коммит (2PC)

Протокол, где координатор управляет процессом.

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

  1. Координатор отправляет всем участникам команду prepare.
  2. Каждый участник выполняет локальные операции (включая запись в лог), но не фиксирует их, блокируя ресурсы.
  3. Участник отвечает YES (готов к фиксации) или NO (не может выполнить).

Фаза 2: Фиксация или Откат (Commit/Rollback)

  • Если все ответили YES, координатор рассылает команду commit. Участники фиксируют изменения.
  • Если хотя бы один ответил NO, координатор рассылает команду rollback. Все участники откатывают изменения.

Пример на Java с JTA (Java Transaction API):

import javax.transaction.UserTransaction;
import javax.annotation.Resource;
import javax.persistence.*;

@Stateless
public class DistributedTransferService {

    @Resource
    private UserTransaction userTransaction; // Координатор транзакций

    @PersistenceContext(unitName = "AccountDB")
    private EntityManager emAccount; // Ресурс 1: База счетов

    @PersistenceContext(unitName = "AuditDB")
    private EntityManager emAudit;   // Ресурс 2: База аудита

    public void transferMoney(Long fromId, Long toId, BigDecimal amount) throws Exception {
        userTransaction.begin(); // Начало распределенной транзакции

        try {
            // 1. Списание со счета отправителя (Resource 1)
            Account from = emAccount.find(Account.class, fromId);
            from.withdraw(amount);

            // 2. Запись в лог аудита (Resource 2)
            AuditLog logEntry = new AuditLog("Transfer", fromId, toId, amount);
            emAudit.persist(logEntry);

            // 3. Зачисление на счет получателя (Resource 1)
            Account to = emAccount.find(Account.class, toId);
            to.deposit(amount);

            userTransaction.commit(); // Фаза 2: Commit, если все успешно

        } catch (Exception e) {
            userTransaction.rollback(); // Фаза 2: Rollback при любой ошибке
            throw e;
        }
    }
}

Проблемы 2PC и современные альтернативы

  • Блокировки ресурсов: Участники блокируют данные на время всей транзакции (проблема производительности).
  • Координатор — единая точка отказа.
  • Сложность восстановления после сбоя на фазе подготовки.

Современные паттерны для микросервисов:

  1. Saga: Длинная транзакция, состоящая из цепочки локальных транзакций. Каждая следующая транзакция запускается после успеха предыдущей. В случае ошибки выполняются компенсирующие транзакции (Compensating Transactions) для отката уже выполненных шагов.
  2. Eventual Consistency (Конечная согласованность): Отказ от немедленной атомарности. Система допускает временную несогласованность, но гарантирует, что в конечном счете все данные будут согласованы (через обмен событиями).

Когда использовать? 2PC подходит для монолитных приложений с несколькими БД. Для микросервисной архитектуры предпочтительны Saga или Eventual Consistency.