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

Как сделаешь атомарной логику взаимодействия с микросервисами

2.0 Middle🔥 161 комментариев
#REST API и микросервисы#Брокеры сообщений

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

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

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

Атомарность в микросервисной архитектуре

Атомарность между микросервисами — это одна из самых сложных проблем в распределённых системах. Идеальный ACID транспорт невозможен, поэтому используются различные паттерны для достижения консистентности данных.

Проблема: классическая транзакция не работает

// В монолите это просто
@Transactional
public void createOrder(OrderRequest request) {
    Order order = orderRepository.save(request.toOrder());
    inventoryService.reserve(order.getItems());  // Если упадёт, откатится
    paymentService.charge(order.getPayment());   // Если упадёт, откатится
}

// В микросервисах это невозможно
// OrderService и InventoryService имеют разные БД
// Нельзя выполнить транзакцию через сеть

Паттерн 1: Saga Pattern (Сага)

Saga — это последовательность шагов, где каждый шаг — это отдельная транзакция в разных сервисах.

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

Сервисы общаются через события, каждый реагирует на события других.

// Order Service
@Service
public class OrderService {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Transactional
    public void createOrder(OrderRequest request) {
        // Шаг 1: Создаём заказ (локальная транзакция)
        Order order = new Order();
        order.setItems(request.getItems());
        order.setStatus("PENDING");
        order = orderRepository.save(order);  // Коммитится
        
        // Шаг 2: Публикуем событие для других сервисов
        OrderCreatedEvent event = new OrderCreatedEvent(order.getId(), order.getItems());
        rabbitTemplate.convertAndSend("order-exchange", "order.created", event);
    }
}

// Inventory Service слушает и реагирует
@Service
public class InventoryService {
    
    @Autowired
    private InventoryRepository inventoryRepository;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @RabbitListener(queues = "inventory-queue")
    @Transactional
    public void handleOrderCreated(OrderCreatedEvent event) {
        try {
            // Шаг 1: Резервируем товар (локальная транзакция)
            for (OrderItem item : event.getItems()) {
                Inventory inv = inventoryRepository.findByProductId(item.getProductId())
                    .orElseThrow();
                
                if (inv.getAvailable() < item.getQuantity()) {
                    // Не хватает товара!
                    throw new OutOfStockException();
                }
                
                inv.setAvailable(inv.getAvailable() - item.getQuantity());
                inventoryRepository.save(inv);  // Коммитится
            }
            
            // Шаг 2: Публикуем событие успеха
            InventoryReservedEvent reserved = new InventoryReservedEvent(event.getOrderId());
            rabbitTemplate.convertAndSend("order-exchange", "inventory.reserved", reserved);
            
        } catch (OutOfStockException e) {
            // Шаг 2: Публикуем событие отката
            OrderFailedEvent failed = new OrderFailedEvent(event.getOrderId(), "Out of stock");
            rabbitTemplate.convertAndSend("order-exchange", "order.failed", failed);
        }
    }
}

// Payment Service также слушает
@Service
public class PaymentService {
    
    @Autowired
    private PaymentRepository paymentRepository;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @RabbitListener(queues = "payment-queue")
    @Transactional
    public void handleInventoryReserved(InventoryReservedEvent event) {
        try {
            // Шаг 1: Обрабатываем платёж (локальная транзакция)
            Payment payment = new Payment();
            payment.setOrderId(event.getOrderId());
            payment.setStatus("PROCESSING");
            paymentRepository.save(payment);
            
            // Имитируем обработку платежа
            boolean success = chargeCard(event.getOrderId());
            
            if (success) {
                payment.setStatus("SUCCESS");
                paymentRepository.save(payment);
                
                // Шаг 2: Публикуем событие успеха
                PaymentSuccessEvent pse = new PaymentSuccessEvent(event.getOrderId());
                rabbitTemplate.convertAndSend("order-exchange", "payment.success", pse);
            } else {
                throw new PaymentFailedException();
            }
            
        } catch (PaymentFailedException e) {
            // Шаг 2: Публикуем событие отката
            PaymentFailedEvent pfe = new PaymentFailedEvent(event.getOrderId());
            rabbitTemplate.convertAndSend("order-exchange", "payment.failed", pfe);
        }
    }
}

// Order Service слушает результаты
@RabbitListener(queues = "order-completion-queue")
@Transactional
public void handlePaymentSuccess(PaymentSuccessEvent event) {
    Order order = orderRepository.findById(event.getOrderId()).orElseThrow();
    order.setStatus("CONFIRMED");
    orderRepository.save(order);
}

@RabbitListener(queues = "order-failure-queue")
@Transactional
public void handlePaymentFailed(PaymentFailedEvent event) {
    Order order = orderRepository.findById(event.getOrderId()).orElseThrow();
    order.setStatus("FAILED");
    orderRepository.save(order);
    
    // ОТКАТ: нужно вернуть товар на склад!
    OrderRollbackEvent rollback = new OrderRollbackEvent(event.getOrderId());
    rabbitTemplate.convertAndSend("order-exchange", "order.rollback", rollback);
}

Проблемы Choreography:

  • Сложно отследить поток операций
  • Циклические зависимости между сервисами
  • Трудно добавлять новые сервисы

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

Центральный Saga Orchestrator координирует все шаги.

// Saga Orchestrator
@Service
public class OrderSagaOrchestrator {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private InventoryService inventoryService;
    
    @Autowired
    private PaymentService paymentService;
    
    @Autowired
    private SagaRepository sagaRepository;
    
    public Order executeOrderSaga(OrderRequest request) {
        // Создаём запись о Саге (для отката)
        Saga saga = new Saga();
        saga.setOrderId(UUID.randomUUID().toString());
        saga.setStatus("STARTED");
        sagaRepository.save(saga);
        
        try {
            // Шаг 1: Создаём заказ
            Order order = orderService.createOrder(request);
            saga.addStep("ORDER_CREATED", order.getId());
            sagaRepository.save(saga);
            
            // Шаг 2: Резервируем товар
            inventoryService.reserveInventory(order.getId(), request.getItems());
            saga.addStep("INVENTORY_RESERVED", order.getId());
            sagaRepository.save(saga);
            
            // Шаг 3: Обрабатываем платёж
            paymentService.chargePayment(order.getId(), order.getAmount());
            saga.addStep("PAYMENT_PROCESSED", order.getId());
            
            // Все успешно
            saga.setStatus("COMPLETED");
            sagaRepository.save(saga);
            
            return order;
            
        } catch (Exception e) {
            // ОТКАТ в обратном порядке
            rollbackSaga(saga);
            throw e;
        }
    }
    
    private void rollbackSaga(Saga saga) {
        // Откатываем в обратном порядке
        List<SagaStep> steps = saga.getSteps();
        
        for (int i = steps.size() - 1; i >= 0; i--) {
            SagaStep step = steps.get(i);
            
            try {
                if ("PAYMENT_PROCESSED".equals(step.getName())) {
                    paymentService.refundPayment(step.getOrderId());
                } else if ("INVENTORY_RESERVED".equals(step.getName())) {
                    inventoryService.releaseInventory(step.getOrderId());
                } else if ("ORDER_CREATED".equals(step.getName())) {
                    orderService.cancelOrder(step.getOrderId());
                }
            } catch (Exception rollbackError) {
                // Логируем ошибку отката (для ручного вмешательства)
                saga.addFailedRollback(step.getName(), rollbackError.getMessage());
            }
        }
        
        saga.setStatus("ROLLED_BACK");
        sagaRepository.save(saga);
    }
}

Паттерн 2: Compensating Transactions (Компенсирующие транзакции)

Для каждой успешной операции есть компенсирующая операция на случай отката.

@Service
public class CompensatingTransactionService {
    
    // Основная операция
    @Transactional
    public void chargePayment(String orderId, double amount) {
        Payment payment = new Payment();
        payment.setOrderId(orderId);
        payment.setAmount(amount);
        payment.setStatus("CHARGED");
        paymentRepository.save(payment);
    }
    
    // Компенсирующая операция (откат)
    @Transactional
    public void refundPayment(String orderId) {
        Payment payment = paymentRepository.findByOrderId(orderId).orElseThrow();
        payment.setStatus("REFUNDED");
        paymentRepository.save(payment);
        
        // Возвращаем деньги клиенту
        Refund refund = new Refund();
        refund.setPaymentId(payment.getId());
        refund.setAmount(payment.getAmount());
        refundRepository.save(refund);
    }
}

Паттерн 3: Event Sourcing + CQRS

Сохраняем все события в Event Store, можем воспроизвести состояние в любой момент.

// Событие
public class OrderEvent {
    private String orderId;
    private String eventType;  // OrderCreated, PaymentProcessed, etc.
    private String payload;
    private Instant timestamp;
}

@Service
public class OrderEventService {
    
    @Autowired
    private EventStore eventStore;
    
    @Transactional
    public void createOrder(OrderRequest request) {
        // Сохраняем событие (вместо состояния)
        OrderEvent event = new OrderEvent();
        event.setOrderId(UUID.randomUUID().toString());
        event.setEventType("OrderCreated");
        event.setPayload(json(request));
        event.setTimestamp(Instant.now());
        
        eventStore.save(event);
        
        // Публикуем для других сервисов
        eventPublisher.publish(event);
    }
    
    public Order getOrderState(String orderId) {
        // Воспроизводим состояние из всех событий
        List<OrderEvent> events = eventStore.findByOrderId(orderId);
        Order order = new Order();
        
        for (OrderEvent event : events) {
            if ("OrderCreated".equals(event.getEventType())) {
                order.setStatus("PENDING");
            } else if ("PaymentProcessed".equals(event.getEventType())) {
                order.setStatus("PAID");
            } else if ("OrderConfirmed".equals(event.getEventType())) {
                order.setStatus("CONFIRMED");
            }
        }
        
        return order;
    }
}

Паттерн 4: Idempotency Keys (Идемпотентность)

Если один сервис отправит запрос дважды, результат должен быть одинаковым.

@Service
public class PaymentService {
    
    @Autowired
    private PaymentRepository paymentRepository;
    
    @Transactional
    public PaymentResult processPayment(String orderId, double amount, String idempotencyKey) {
        // Проверяем, не обработали ли уже с этим ключом
        PaymentResult existing = paymentRepository.findByIdempotencyKey(idempotencyKey)
            .orElse(null);
        
        if (existing != null) {
            // Возвращаем старый результат (идемпотентно)
            return existing;
        }
        
        // Первый раз: обрабатываем платёж
        PaymentResult result = new PaymentResult();
        result.setOrderId(orderId);
        result.setAmount(amount);
        result.setIdempotencyKey(idempotencyKey);
        result.setStatus("SUCCESS");
        
        paymentRepository.save(result);
        return result;
    }
}

// Использование
public ResponseEntity<?> createOrder(@RequestBody OrderRequest request) {
    String idempotencyKey = request.getIdempotencyKey(); // UUID от клиента
    PaymentResult result = paymentService.processPayment(
        order.getId(),
        order.getAmount(),
        idempotencyKey
    );
    return ResponseEntity.ok(result);
}

Таблица сравнения паттернов

ПаттернСложностьОткатКонсистентностьКогда использовать
Saga ChoreographyСредняяКомпенсирующиеEventualПростые процессы
Saga OrchestrationВысокаяЯвный откатEventualСложные процессы
Event SourcingВысокаяПолная историяStrongКритичные данные
IdempotencyНизкаяАвтоматическийStrongВсе REST API

Best Practice: Комбинирование паттернов

@Service
public class RobustOrderService {
    
    // 1. Используем Saga для оркестрации
    @Autowired
    private OrderSagaOrchestrator sagaOrchestrator;
    
    // 2. Используем идемпотентность для безопасности
    @Transactional
    public Order createOrder(OrderRequest request, String idempotencyKey) {
        // Проверяем, не создали ли уже
        Order existing = orderRepository.findByIdempotencyKey(idempotencyKey)
            .orElse(null);
        
        if (existing != null) {
            return existing;  // Идемпотентный результат
        }
        
        // 3. Выполняем Saga
        Order order = sagaOrchestrator.executeOrderSaga(request);
        order.setIdempotencyKey(idempotencyKey);
        
        return orderRepository.save(order);
    }
}

Итог

Полная атомарность между микросервисами невозможна. Нужно выбрать один из паттернов или комбинацию:

  1. Saga — для последовательных операций
  2. Event Sourcing — для критичных данных
  3. Idempotency — для безопасности API
  4. Compensating transactions — для отката

Цель — достичь eventual consistency: в конечном итоге система будет в согласованном состоянии, даже если временно данные разошлись.

Как сделаешь атомарной логику взаимодействия с микросервисами | PrepBro