Как откатить операцию в микросервисной архитектуре
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Откатывание операций в микросервисной архитектуре
В микросервисной архитектуре нет встроенной ACID транзакции, которая охватывает несколько сервисов. Откатывание (rollback) операции требует специальных стратегий.
1. Сага-паттерн (Saga Pattern)
Сага — это распределённая транзакция, состоящая из последовательности локальных транзакций.
Хореография (Choreography)
Каждый сервис публикует события, другие сервисы слушают и реагируют.
// OrderService
@Service
public class OrderService {
@Autowired
private EventPublisher eventPublisher;
@Autowired
private OrderRepository orderRepository;
@Transactional
public void createOrder(OrderRequest request) {
Order order = new Order(request);
orderRepository.save(order);
// Опубликовать событие
eventPublisher.publish(
new OrderCreatedEvent(order.getId(), order.getTotalAmount())
);
}
}
// PaymentService слушает события
@Service
public class PaymentService {
@Autowired
private PaymentRepository paymentRepository;
@Autowired
private EventPublisher eventPublisher;
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
try {
Payment payment = processPayment(event.getOrderId(), event.getAmount());
paymentRepository.save(payment);
eventPublisher.publish(
new PaymentSuccessEvent(event.getOrderId())
);
} catch (PaymentException e) {
// Опубликовать событие отката
eventPublisher.publish(
new PaymentFailedEvent(event.getOrderId())
);
}
}
}
// OrderService слушает событие отката
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@EventListener
public void handlePaymentFailed(PaymentFailedEvent event) {
// Откатить заказ
Order order = orderRepository.findById(event.getOrderId()).orElseThrow();
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}
Преимущества:
- Свободная связь между сервисами
- Асинхронная обработка
Недостатки:
- Сложно отследить сагу
- Сложна обработка ошибок
Оркестрация (Orchestration)
Один сервис (Orchestrator) управляет шагами саги.
// OrderSagaOrchestrator
@Service
public class OrderSagaOrchestrator {
@Autowired
private OrderServiceClient orderService;
@Autowired
private PaymentServiceClient paymentService;
@Autowired
private ShipmentServiceClient shipmentService;
@Transactional
public void executeOrderSaga(OrderRequest request) {
try {
// Шаг 1: Создать заказ
Long orderId = orderService.createOrder(request).getId();
// Шаг 2: Обработать платёж
try {
paymentService.processPayment(orderId, request.getAmount());
} catch (PaymentException e) {
// Откатить заказ
orderService.cancelOrder(orderId);
throw new OrderSagaException("Payment failed", e);
}
// Шаг 3: Подготовить доставку
try {
shipmentService.prepareShipment(orderId);
} catch (ShipmentException e) {
// Откатить платёж и заказ
paymentService.refund(orderId);
orderService.cancelOrder(orderId);
throw new OrderSagaException("Shipment failed", e);
}
} catch (Exception e) {
// Обработка ошибки
logger.error("Order saga failed", e);
throw e;
}
}
}
Преимущества:
- Проще логика
- Лучше контроль потока
Недостатки:
- Tight coupling
- Single point of failure
2. Event Sourcing + Compensation
Хранить все события, использовать компенсирующие транзакции для отката.
// Событие платежа
@Entity
public class PaymentEvent {
@Id
private Long id;
private Long orderId;
private BigDecimal amount;
private PaymentStatus status; // PENDING, COMPLETED, COMPENSATED
private LocalDateTime timestamp;
private String eventType; // PAYMENT_CREATED, PAYMENT_REFUNDED
}
@Service
public class PaymentEventStore {
@Autowired
private PaymentEventRepository eventRepository;
@Transactional
public void recordPayment(Long orderId, BigDecimal amount) {
PaymentEvent event = new PaymentEvent();
event.setOrderId(orderId);
event.setAmount(amount);
event.setStatus(PaymentStatus.COMPLETED);
event.setEventType("PAYMENT_CREATED");
event.setTimestamp(LocalDateTime.now());
eventRepository.save(event);
}
@Transactional
public void compensatePayment(Long orderId) {
PaymentEvent event = new PaymentEvent();
event.setOrderId(orderId);
event.setStatus(PaymentStatus.COMPENSATED);
event.setEventType("PAYMENT_REFUNDED");
event.setTimestamp(LocalDateTime.now());
eventRepository.save(event); // Регистрирует компенсацию
// Фактически обработать возврат
processRefund(orderId);
}
}
3. Two-Phase Commit (2PC) — Избегать!
В распределённых системах 2PC работает плохо, но для критичных операций иногда используется.
// Координатор
@Service
public class TransactionCoordinator {
@Autowired
private PaymentServiceClient paymentService;
@Autowired
private InventoryServiceClient inventoryService;
public void executeTransaction(Long orderId, BigDecimal amount) {
// Phase 1: Prepare
boolean paymentOk = paymentService.preparePayment(orderId, amount);
boolean inventoryOk = inventoryService.reserveInventory(orderId);
if (paymentOk && inventoryOk) {
// Phase 2: Commit
paymentService.commitPayment(orderId);
inventoryService.commitReservation(orderId);
} else {
// Rollback
paymentService.abortPayment(orderId);
inventoryService.abortReservation(orderId);
}
}
}
Это не рекомендуется — может привести к deadlock-ам и блокировкам.
4. Идемпотентность + Retry
Сделать операции идемпотентными, чтобы можно было безопасно повторять.
@Service
public class PaymentService {
@Autowired
private PaymentRepository paymentRepository;
@Autowired
private BankAPI bankAPI;
@Transactional
public PaymentResponse processPayment(String idempotencyKey, BigDecimal amount) {
// Проверить если платёж уже обработан
Optional<Payment> existing = paymentRepository.findByIdempotencyKey(idempotencyKey);
if (existing.isPresent()) {
return existing.get().toResponse(); // Вернуть тот же результат
}
// Обработать платёж
PaymentResponse response = bankAPI.charge(amount);
// Сохранить результат с idempotencyKey
Payment payment = new Payment();
payment.setIdempotencyKey(idempotencyKey);
payment.setAmount(amount);
payment.setStatus(response.getStatus());
paymentRepository.save(payment);
return response;
}
}
// Клиент с retry
@Service
public class OrderClient {
@Autowired
private RestTemplate restTemplate;
public void processPaymentWithRetry(String idempotencyKey, BigDecimal amount) {
Retry retry = Retry.of("payment", RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofSeconds(1))
.build());
Retry.executeFunction(retry, () -> {
paymentService.processPayment(idempotencyKey, amount);
return null;
});
}
}
5. Компенсирующие операции
Для каждого действия определить компенсацию.
// Действие: Зарезервировать товар
public class ReserveInventoryCommand {
private Long orderId;
private List<OrderItem> items;
}
// Компенсация: Освободить резервацию
public class ReleaseInventoryCommand {
private Long orderId;
private List<OrderItem> items;
}
@Service
public class OrderSaga {
@Autowired
private CommandBus commandBus;
public void handleOrderCreated(OrderCreatedEvent event) {
try {
// Шаг 1
commandBus.send(new ReserveInventoryCommand(event.getOrderId(), event.getItems()));
// Шаг 2
commandBus.send(new ProcessPaymentCommand(event.getOrderId()));
} catch (Exception e) {
// Откатить в обратном порядке
commandBus.send(new ReleaseInventoryCommand(event.getOrderId(), event.getItems()));
commandBus.send(new RefundPaymentCommand(event.getOrderId()));
}
}
}
Лучшие практики
- Используй Saga паттерн для распределённых операций
- Предпочитай хореографию для простых сценариев
- Используй оркестрацию для сложных саг
- Сделай операции идемпотентными — ключ к надежности
- Используй Idempotency-Key для обнаружения дубликатов
- Логируй все события для отладки
- Избегай 2PC в продакшене
- Определи компенсирующие операции для каждого шага
- Тестируй сценарии отката в интеграционных тестах
- Мониторь саги на предмет зависаний