Сталкивался ли с распределенными транзакциями микросервисной архитектуры
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Сталкивался ли с распределёнными транзакциями микросервисной архитектуры?
Это отличный вопрос для выяснения практического опыта. Да, я сталкивался с этим вызовом, и это одна из самых сложных проблем в микросервисной архитектуре.
Реальная ситуация: Система заказов с оплатой
Контекст: Приложение электронной коммерции состояло из 3 сервисов:
- OrderService — создание заказов
- PaymentService — обработка платежей
- InventoryService — управление складом
При создании заказа нужно:
- Создать заказ в БД OrderService
- Зарезервировать товар в InventoryService
- Провести платёж в 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;
}
}
}
Проблемы с этим подходом:
- Deadlock: InventoryService заблокирован, ждёт подтверждения
- Network partition: если PaymentService не отвечает, InventoryService ждёт бесконечно
- Performance: всё блокирующее, синхронное
- Масштабируемость: плохо масштабируется с ростом числа сервисов
- Теорема 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()));
}
}
Проблемы:
- Компенсирующие транзакции: что если отмена платежа тоже не удалась?
- Eventually consistent: данные не согласованы в момент времени
- Сложная отладка: много событий, трудно понять последовательность
Попытка 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 и готовность обрабатывать отказы