Как обеспечивается транзакционность в микросервисной архитектуре?

Ответ

В микросервисах классические ACID-транзакции через границы сервисов невозможны. Вместо этого применяются паттерны BASE (Basically Available, Soft state, Eventually consistent) и следующие подходы:

1. Паттерн Saga: Последовательность локальных транзакций, где каждый следующий шаг зависит от успеха предыдущего. При неудаче выполняются компенсирующие транзакции.

// Пример реализации Saga с событиями
@Service
public class OrderSagaService {

    @Transactional
    public void createOrder(Order order) {
        // 1. Локальная транзакция: создать заказ в статусе PENDING
        orderRepository.save(order);

        // 2. Отправить событие для резервирования товара
        eventPublisher.publish(new ReserveItemsEvent(order.getId()));
    }

    // Обработчик успешного резервирования
    @TransactionalEventListener
    public void handleItemsReserved(ItemsReservedEvent event) {
        // 3. Локальная транзакция: списать деньги
        paymentService.charge(event.getOrderId());
        // Отправить событие об успешном списании
    }

    // Компенсирующее действие при ошибке оплаты
    @TransactionalEventListener
    public void handlePaymentFailed(PaymentFailedEvent event) {
        // Откат: освободить зарезервированные товары
        inventoryService.releaseItems(event.getOrderId());
        // Обновить статус заказа на FAILED
        orderRepository.updateStatus(event.getOrderId(), OrderStatus.FAILED);
    }
}

2. Паттерн Outbox: Гарантирует доставку событий в рамках локальной транзакции.

-- В рамках одной транзакции с бизнес-данными
BEGIN TRANSACTION;
  INSERT INTO orders (id, amount) VALUES (123, 100.00);
  INSERT INTO outbox (id, aggregate_id, event_type, payload) 
  VALUES (gen_random_uuid(), 123, 'OrderCreated', '{"amount":100}');
COMMIT;
-- Отдельный процесс (CDC, polling) читает outbox и публикует в брокер

3. Паттерн TCC (Try-Confirm-Cancel):

// Фаза Try: предварительное резервирование ресурсов
paymentService.reserve(orderId, amount); // Резервируем, но не списываем
inventoryService.reserve(orderId, items); // Резервируем товары

// Фаза Confirm: фиксация (идемпотентная)
paymentService.confirm(orderId); // Списание зарезервированных средств
inventoryService.confirm(orderId); // Подтверждение резерва

// Фаза Cancel: отмена (идемпотентная)
paymentService.cancel(orderId); // Отмена резерва
inventoryService.cancel(orderId); // Освобождение товаров

4. Технологии и инструменты:

  • Брокеры сообщений: Kafka (с идемпотентным продюсером и exactly-once семантикой), RabbitMQ
  • Оркестраторы: Camunda, Temporal, AWS Step Functions
  • Мониторинг: Distributed tracing (Jaeger, Zipkin) для отслеживания потока транзакции

5. Критические требования:

  • Идемпотентность: Все операции должны безопасно повторяться
  • Компенсация: Для каждого действия должен быть определен компенсирующий action
  • Мониторинг: Необходима видимость состояния распределенной транзакции
  • Таймауты и ретраи: Стратегии обработки временных сбоев

Ответ 18+ 🔞

А, слушай, про микросервисы и транзакции! Ну это вообще отдельная песня, пиздец. Представь: раньше у тебя одна жирная база, и ты мог просто BEGIN TRANSACTION, нахуячить кучу INSERT-ов и потом COMMIT. Или ROLLBACK, если всё пошло по пизде. Красота, ACID, всё как у людей.

А теперь у тебя эти распиздяйские микросервисы, каждый со своей базой, как ёбаный феодал в своём замке. И как ты скажешь базе инвентаря и базе платежей: «Ребята, давайте синхронно, на раз-два, обновитесь, а если что — откатитесь»? Да нихуя! Это ж разные процессы, разные машины, сеть между ними, которая может в любой момент накрыться медным тазом.

Так что про классические ACID-транзакции через сервисы можно сразу забыть, как про сон. Вместо них тут царствует BASE — Basically Available, Soft state, Eventually consistent. То есть, в переводе на русский: «Вроде работает, состояние плавающее, но в конце концов, блядь, всё сойдётся». Надежда, ебать, как стратегия.

И вот какие фокусы для этого придумали, просто ёперный театр:

1. Сага (Saga) Это когда твоя транзакция — это цепочка локальных шагов. Каждый следующий пинок зависит от успеха предыдущего. А если на каком-то этапе пиздец, то надо бегать назад и откатывать всё, что наделал. Эти откаты называются «компенсирующие транзакции». Представь, ты заказал пиццу, потом пиво, потом решил посмотреть кино. А денег на кино нет. Так что придётся отменить пиво, потом пиццу, и сидеть грустный.

// Пример реализации Saga с событиями
@Service
public class OrderSagaService {

    @Transactional
    public void createOrder(Order order) {
        // 1. Локальная транзакция: создать заказ в статусе PENDING
        orderRepository.save(order);

        // 2. Отправить событие для резервирования товара
        eventPublisher.publish(new ReserveItemsEvent(order.getId()));
    }

    // Обработчик успешного резервирования
    @TransactionalEventListener
    public void handleItemsReserved(ItemsReservedEvent event) {
        // 3. Локальная транзакция: списать деньги
        paymentService.charge(event.getOrderId());
        // Отправить событие об успешном списании
    }

    // Компенсирующее действие при ошибке оплаты
    @TransactionalEventListener
    public void handlePaymentFailed(PaymentFailedEvent event) {
        // Откат: освободить зарезервированные товары
        inventoryService.releaseItems(event.getOrderId());
        // Обновить статус заказа на FAILED
        orderRepository.updateStatus(event.getOrderId(), OrderStatus.FAILED);
    }
}

Видишь? Создали заказ, отправили событие «зарезервируй шмотки». Если резервирование прошло — пытаемся списать бабки. Если при списании денег — пизда, то держись, собачка Муму: запускаем компенсацию и освобождаем товары. Трагедия, блядь, но что поделать.

2. Аутбокс (Outbox) Этот паттерн — просто гениальная, блядь, заглушка для совести. Проблема-то в чём? Ты в транзакции сохранил заказ в свою базу и отправил событие в Кафку. А потом твоя транзакция коммитится, а Кафка лежит. Или наоборот: событие улетело, а транзакция откатилась. Полная неконсистентность, пиздец.

Аутбокс решает это просто: ты пишешь событие в ту же самую транзакцию и в ту же самую базу, в специальную табличку. А потом уже отдельный, ебушки-воробушки, процесс (типа CDC или просто поллинг) выгребает из этой таблицы и тащит в брокер. Всё, проблема поехавших событий решена.

-- В рамках одной транзакции с бизнес-данными
BEGIN TRANSACTION;
  INSERT INTO orders (id, amount) VALUES (123, 100.00);
  INSERT INTO outbox (id, aggregate_id, event_type, payload) 
  VALUES (gen_random_uuid(), 123, 'OrderCreated', '{"amount":100}');
COMMIT;
-- Отдельный процесс (CDC, polling) читает outbox и публикует в брокер

Красота! Или закоммитилось всё, или ничего. Никаких полумер.

3. TCC (Try-Confirm-Cancel) Это как трёхфазный commit, но для бедных и умных. Вместо одной операции — три:

  • Try: Предварительно зарезервируй ресурсы. Деньги на карте заморозь, товары на складе отложи в сторону. Но ещё ничего окончательно.
  • Confirm: Если все этапы Try прошли успешно — финализируй. Спиши замороженные деньги, отдай отложенные товары. Эта операция должна быть идемпотентной, потому что её могут вызвать сто раз.
  • Cancel: Если на любом этапе Try что-то пошло не так — отмени все резервы. Разморозь деньги, верни товары на полку. Тоже идемпотентно, ясен хуй.
// Фаза Try: предварительное резервирование ресурсов
paymentService.reserve(orderId, amount); // Резервируем, но не списываем
inventoryService.reserve(orderId, items); // Резервируем товары

// Фаза Confirm: фиксация (идемпотентная)
paymentService.confirm(orderId); // Списание зарезервированных средств
inventoryService.confirm(orderId); // Подтверждение резерва

// Фаза Cancel: отмена (идемпотентная)
paymentService.cancel(orderId); // Отмена резерва
inventoryService.cancel(orderId); // Освобождение товаров

4. Чем всё это ебашить?

  • Брокеры: Кафка (с её идемпотентным продюсером и exactly-once — о, мечта!), RabbitMQ.
  • Оркестраторы: Camunda, Temporal, AWS Step Functions — чтобы эти саги не расползались как тараканы.
  • Мониторинг: Distributed tracing (Jaeger, Zipkin) — чтобы когда всё ебнулось, можно было проследить, в каком именно сервисе и почему начался пиздец.

5. Без чего нихуя не получится?

  • Идемпотентность: Это святое. Любой вызов, любое событие должно быть безопасно обработано дважды, трижды, сто раз. Иначе ретраи тебя просто убьют.
  • Компенсация: На каждое действие должен быть чёткий, прописанный откат. Не «авось пронесёт», а конкретный сценарий: «если списание денег не прошло, то освободи товары».
  • Мониторинг: Ты должен видеть, в каком состоянии твоя распределённая транзакция-зомби. Иначе будешь как Герасим, который только «Муму» мычать может.
  • Таймауты и ретраи: Сеть — она такая, сука. Сегодня есть, завтра нет. Надо уметь ждать и повторять, но не до бесконечности, а то задушишь систему.

Вот так, блядь, и живём. Сложно? Овердохуища. Но другого выхода нет, если хочешь масштабироваться, а не сидеть на одной монолитной базе, которая рано или поздно ляжет под нагрузкой, как загнанная лошадь.