← Назад к вопросам

Сталкивался ли с распределенными транзакциями микросервисной архитектуры

3.0 Senior🔥 191 комментариев
#REST API и микросервисы#SOLID и паттерны проектирования

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Сталкивался ли с распределёнными транзакциями микросервисной архитектуры?

Это отличный вопрос для выяснения практического опыта. Да, я сталкивался с этим вызовом, и это одна из самых сложных проблем в микросервисной архитектуре.

Реальная ситуация: Система заказов с оплатой

Контекст: Приложение электронной коммерции состояло из 3 сервисов:

  1. OrderService — создание заказов
  2. PaymentService — обработка платежей
  3. InventoryService — управление складом

При создании заказа нужно:

  1. Создать заказ в БД OrderService
  2. Зарезервировать товар в InventoryService
  3. Провести платёж в PaymentService

Проблема: Что если платёж прошёл, а товар зарезервировать не удалось?

Client → OrderService: создать заказ
         ↓ успех
         PaymentService: провести платёж
         ↓ успех
         InventoryService: зарезервировать товар
         ↓ ОШИБКА!

Результат: товара нет, но деньги сняты!

Попытка 1: Использование распределённых транзакций (2-phase commit / 2PC)

Сначала я попробовал использовать 2-Phase Commit:

// Плохой пример: координатор всё контролирует
@Service
public class OrderService {
    private OrderRepository orderRepository;
    private PaymentServiceClient paymentClient;
    private InventoryServiceClient inventoryClient;
    
    @Transactional(propagation = Propagation.REQUIRED)
    public Order createOrder(OrderRequest request) {
        try {
            // Phase 1: Prepare
            Order order = orderRepository.save(new Order(request));
            paymentClient.preparePayment(request.getAmount());
            inventoryClient.prepareReservation(request.getItems());
            
            // Phase 2: Commit
            paymentClient.commitPayment();
            inventoryClient.commitReservation();
            
            return order;
        } catch (Exception e) {
            // Rollback
            paymentClient.rollback();
            inventoryClient.rollback();
            throw e;
        }
    }
}

Проблемы с этим подходом:

  1. Deadlock: InventoryService заблокирован, ждёт подтверждения
  2. Network partition: если PaymentService не отвечает, InventoryService ждёт бесконечно
  3. Performance: всё блокирующее, синхронное
  4. Масштабируемость: плохо масштабируется с ростом числа сервисов
  5. Теорема CAP: не гарантирует согласованность в случае сбоев сети

Попытка 2: Saga Pattern (Choreography)

Я переходить на Saga Pattern с Choreography (через события):

// OrderService
@Service
public class OrderService {
    private EventPublisher eventPublisher;
    private OrderRepository orderRepository;
    
    public Order createOrder(OrderRequest request) {
        Order order = orderRepository.save(new Order(request));
        // Публикуем событие, не ждём ответа
        eventPublisher.publish(new OrderCreatedEvent(order.getId(), request.getItems()));
        return order;
    }
}

// InventoryService слушает события
@Component
public class InventoryEventListener {
    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        try {
            inventoryRepository.reserve(event.getItems());
            eventPublisher.publish(new ItemsReservedEvent(event.getOrderId()));
        } catch (Exception e) {
            eventPublisher.publish(new ReservationFailedEvent(event.getOrderId()));
        }
    }
}

// PaymentService слушает события
@Component
public class PaymentEventListener {
    @EventListener
    public void onItemsReserved(ItemsReservedEvent event) {
        try {
            payment.charge(event.getOrderId(), event.getAmount());
            eventPublisher.publish(new PaymentSuccessEvent(event.getOrderId()));
        } catch (Exception e) {
            eventPublisher.publish(new PaymentFailedEvent(event.getOrderId()));
            // Нужно откатить резервацию!
        }
    }
}

// OrderService слушает итоговые события
@Component
public class OrderSagaOrchestrator {
    @EventListener
    public void onPaymentSuccess(PaymentSuccessEvent event) {
        orderRepository.updateStatus(event.getOrderId(), OrderStatus.CONFIRMED);
    }
    
    @EventListener
    public void onPaymentFailed(PaymentFailedEvent event) {
        // Нужна компенсирующая транзакция!
        eventPublisher.publish(new CancelReservationEvent(event.getOrderId()));
    }
}

Проблемы:

  1. Компенсирующие транзакции: что если отмена платежа тоже не удалась?
  2. Eventually consistent: данные не согласованы в момент времени
  3. Сложная отладка: много событий, трудно понять последовательность

Попытка 3: Saga с Orchestrator (лучше)

Я использовал явного Orchestrator, который контролирует процесс:

@Service
public class OrderSagaOrchestrator {
    private OrderRepository orderRepository;
    private PaymentServiceClient paymentClient;
    private InventoryServiceClient inventoryClient;
    private EventPublisher eventPublisher;
    
    public void executeOrderSaga(OrderRequest request) {
        Order order = orderRepository.save(
            new Order(request, OrderStatus.PENDING)
        );
        
        try {
            // Шаг 1: Резервирование товара
            InventoryResponse inventoryResponse = inventoryClient.reserve(request.getItems());
            if (!inventoryResponse.isSuccess()) {
                throw new BusinessException("Items not available");
            }
            order.setStatus(OrderStatus.INVENTORY_RESERVED);
            orderRepository.save(order);
            
            // Шаг 2: Платёж
            PaymentResponse paymentResponse = paymentClient.charge(
                request.getAmount(),
                request.getPaymentDetails()
            );
            if (!paymentResponse.isSuccess()) {
                throw new BusinessException("Payment failed");
            }
            order.setStatus(OrderStatus.PAID);
            orderRepository.save(order);
            
            // Шаг 3: Подтверждение заказа
            order.setStatus(OrderStatus.CONFIRMED);
            orderRepository.save(order);
            
            eventPublisher.publish(new OrderConfirmedEvent(order.getId()));
            
        } catch (Exception e) {
            // Компенсирующие транзакции в обратном порядке
            executeCompensation(order);
        }
    }
    
    private void executeCompensation(Order order) {
        if (order.getStatus() == OrderStatus.PAID) {
            // Отмена платежа
            try {
                paymentClient.refund(order.getPaymentId());
            } catch (Exception e) {
                // Логируем, нужна ручная обработка
                logger.error("Failed to refund payment", e);
            }
        }
        
        if (order.getStatus().ordinal() >= OrderStatus.INVENTORY_RESERVED.ordinal()) {
            // Отмена резервации
            try {
                inventoryClient.cancelReservation(order.getId());
            } catch (Exception e) {
                logger.error("Failed to cancel reservation", e);
            }
        }
        
        order.setStatus(OrderStatus.FAILED);
        orderRepository.save(order);
        eventPublisher.publish(new OrderFailedEvent(order.getId()));
    }
}

Лучший подход: Коммерческий инструмент (Outbox Pattern)

Я также использовал Outbox Pattern для гарантированной доставки:

// OrderService
@Service
public class OrderService {
    private OrderRepository orderRepository;
    private OutboxRepository outboxRepository;
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        // Создаём заказ И запись в outbox в одной транзакции
        Order order = orderRepository.save(new Order(request));
        
        outboxRepository.save(new OutboxEvent(
            EventType.ORDER_CREATED,
            order.getId(),
            order.toJson()
        ));
        
        return order;
    }
}

// Separate process читает outbox и публикует события
@Component
public class OutboxPoller {
    @Scheduled(fixedDelay = 100)
    public void pollOutbox() {
        List<OutboxEvent> events = outboxRepository.findUnpublished();
        for (OutboxEvent event : events) {
            try {
                eventPublisher.publish(event);
                outboxRepository.markAsPublished(event.getId());
            } catch (Exception e) {
                // Повторим позже
            }
        }
    }
}

Преимущества:

  • Гарантированная доставка события (exactly-once)
  • Можно повторить, если сервис был недоступен
  • Работает с любым Message Broker (RabbitMQ, Kafka)

Практические выводы

1. Чего избегать:

  • ❌ Распределённые транзакции (2PC) для микросервисов
  • ❌ Синхронные call-to-call взаимодействие
  • ❌ Трудолюбивые откаты без контроля

2. Что использовать:

  • ✓ Saga Pattern с Orchestrator
  • ✓ Event Sourcing для сложных процессов
  • ✓ Outbox Pattern для гарантированной доставки
  • ✓ Идемпотентность операций (можно повторить безопасно)

3. Мониторинг и алерты:

// Отслеживаем сагу
@Service
public class SagaMetrics {
    private MeterRegistry meterRegistry;
    
    public void recordSagaStart(String sagaId) {
        meterRegistry.counter("saga.started", "type", "order").increment();
    }
    
    public void recordSagaSuccess(String sagaId, long duration) {
        meterRegistry.counter("saga.completed").increment();
        meterRegistry.timer("saga.duration").record(duration, TimeUnit.MILLISECONDS);
    }
    
    public void recordSagaFailure(String sagaId) {
        meterRegistry.counter("saga.failed").increment();
        // Алерт: ручная обработка требуется!
    }
}

Итоги

Распределённые транзакции в микросервисной архитектуре — это один из самых сложных вызовов. Нет серебряной пули, но Saga Pattern с компенсирующими транзакциями — это проверенное на практике решение. Ключ — это eventual consistency и готовность обрабатывать отказы