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

Ответ

Распределенная транзакция — это операция, которая охватывает несколько независимых ресурсов (разные базы данных, очереди сообщений, микросервисы) и должна быть выполнена как единое атомарное действие (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.

Ответ 18+ 🔞

А, распределённые транзакции! Ну это же классика, блядь, вечная головная боль. Представь, тебе надо денег перевести, но так, чтобы и со счёта списалось, и в лог записалось, и получателю зачислилось, и всё это в разных, ёпта, местах — одна база тут, другая там, а третья вообще на другом континенте лежит. И надо, чтобы либо всё разом сработало, либо, если где-то пиздец случился, чтобы всё как было осталось. Вот это и есть «всё или ничего», атомарность, только уже на уровне всей системы, а не одной базульки.

Старый добрый способ: Двухфазный коммит (2PC)

Это как свадьба, блядь, с координатором-тамадой. Протокол прямолинейный, как лом.

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

  1. Координатор (этот самый тамада) орет всем участникам: «Мужики, готовьтесь! Запись в лог, всё посчитали, но хуй вам, не фиксируйте пока!».
  2. Участники делают свою работу локально, но держат всё на замке, ресурсы блокируют. Потом кричат в ответ: «ДА, готов!» или «НЕТ, нихуя не выйдет».
  3. Если хоть один гаркнул «НЕТ» — всё, пиздец, отмена.

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

  • Если все ответили «ДА», координатор командует: «Ну всё, женитесь!». Все фиксируют изменения.
  • Если хотя бы один — «НЕТ», то кричит: «Расходимся, блядь!». Все откатывают свою работу.

Пример на Java с JTA (Java Transaction API): Смотри, как это выглядит в коде. Всё через UserTransaction, который и есть наш главный по таблеткам.

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: ВСЁ ОТЛИЧНО, КОММИТИМ!

        } catch (Exception e) {
            userTransaction.rollback(); // Фаза 2: ЧТО-ТО ПОШЛО НЕ ТАК, ОТКАТ ВСЕГО, БЛЯДЬ!
            throw e;
        }
    }
}

Да какие же тут проблемы, спросишь ты? А проблемы — овердохуища!

  • Блокировки: Пока все готовятся и ждут команды, ресурсы висят на замке. Это как очередь в один сортир на десять человек — производительность летит в пизду.
  • Координатор — единая точка отказа. Упал тамада — вся свадьба в ахуе. Кто коммитить-то будет?
  • Восстановление после сбоя — это отдельный пиздец. Если координатор сдох на фазе подготовки, когда все уже сказали «ДА», но команды «жениться» ещё не было, то участники так и будут висеть в подвешенном состоянии, пока кто-то не разберётся, что делать.

Поэтому сейчас, особенно в этих ваших микросервисах, 2PC часто посылают нахуй. Вместо него:

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

Короче, резюме: 2PC — это для монолитов, где несколько баз, но всё ещё более-менее под контролем. Для микросервисной вакханалии, где каждый творит что хочет, лучше смотреть в сторону Saga или смириться с конечной согласованностью. А то так и до инфаркта недалеко.