Files
sa-lessons/modules/module2/sections/03-service-communication.md
Alexander Abakumov 15e744da49 Initial commit: Complete Software Architecture course with 11 modules
- Added comprehensive course structure with 11 modules
- Included theoretical content, practical examples, and tasks
- Multi-language code examples (Java, Rust, Node.js, Python)
- Detailed analysis of architectural approaches and trade-offs
- Complete caching strategies documentation
- Architecture examples from real-world systems (Twitter, Netflix, Uber)
- Structured navigation and learning path
- Ready for educational use
2025-09-09 22:08:07 +03:00

18 KiB

Коммуникация между сервисами

Навигация


Синхронная коммуникация

HTTP/REST

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

  • Простота
  • Широкая поддержка
  • Стандартизация

Недостатки:

  • Связанность
  • Блокирующие вызовы
  • Проблемы с производительностью

Технический пример - HTTP/REST коммуникация:

// Order Service - клиент для Payment Service
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    @Autowired
    private PaymentServiceClient paymentServiceClient;
    @Autowired
    private OrderService orderService;
    
    @PostMapping
    public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
        try {
            // Синхронный вызов Payment Service
            PaymentRequest paymentRequest = new PaymentRequest(
                request.getAmount(),
                request.getCurrency(),
                request.getPaymentMethod(),
                request.getPaymentDetails()
            );
            
            PaymentResult paymentResult = paymentServiceClient.processPayment(paymentRequest);
            
            if (paymentResult.isSuccess()) {
                // Создаем заказ только после успешной оплаты
                Order order = orderService.createOrder(request, paymentResult.getTransactionId());
                return ResponseEntity.ok(order);
            } else {
                return ResponseEntity.badRequest()
                    .body(new ErrorResponse("Payment failed: " + paymentResult.getErrorMessage()));
            }
            
        } catch (PaymentServiceException e) {
            // Обработка ошибок коммуникации
            return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
                .body(new ErrorResponse("Payment service unavailable"));
        }
    }
}

// HTTP клиент для Payment Service
@Component
public class PaymentServiceClient {
    @Autowired
    private RestTemplate restTemplate;
    
    private static final String PAYMENT_SERVICE_URL = "http://payment-service:8080";
    
    public PaymentResult processPayment(PaymentRequest request) {
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            
            HttpEntity<PaymentRequest> entity = new HttpEntity<>(request, headers);
            
            ResponseEntity<PaymentResult> response = restTemplate.postForEntity(
                PAYMENT_SERVICE_URL + "/api/payments/process",
                entity,
                PaymentResult.class
            );
            
            if (response.getStatusCode() == HttpStatus.OK) {
                return response.getBody();
            } else {
                throw new PaymentServiceException("Payment service returned error: " + response.getStatusCode());
            }
            
        } catch (RestClientException e) {
            throw new PaymentServiceException("Failed to communicate with payment service", e);
        }
    }
    
    public PaymentResult refundPayment(String transactionId, BigDecimal amount) {
        try {
            RefundRequest refundRequest = new RefundRequest(transactionId, amount);
            
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            
            HttpEntity<RefundRequest> entity = new HttpEntity<>(refundRequest, headers);
            
            ResponseEntity<RefundResult> response = restTemplate.postForEntity(
                PAYMENT_SERVICE_URL + "/api/payments/refund",
                entity,
                RefundResult.class
            );
            
            if (response.getStatusCode() == HttpStatus.OK) {
                RefundResult refundResult = response.getBody();
                return new PaymentResult(
                    null,
                    refundResult.isSuccess() ? PaymentStatus.REFUNDED : PaymentStatus.FAILED,
                    transactionId,
                    refundResult.isSuccess() ? null : "Refund failed"
                );
            } else {
                throw new PaymentServiceException("Refund service returned error: " + response.getStatusCode());
            }
            
        } catch (RestClientException e) {
            throw new PaymentServiceException("Failed to communicate with refund service", e);
        }
    }
}

// Конфигурация RestTemplate с таймаутами и retry
@Configuration
public class RestTemplateConfig {
    
    @Bean
    public RestTemplate restTemplate() {
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setConnectTimeout(5000); // 5 секунд на подключение
        factory.setReadTimeout(10000);    // 10 секунд на чтение
        
        RestTemplate restTemplate = new RestTemplate(factory);
        
        // Добавляем retry логику
        restTemplate.setInterceptors(Arrays.asList(new RetryInterceptor()));
        
        return restTemplate;
    }
}

// Interceptor для retry логики
public class RetryInterceptor implements ClientHttpRequestInterceptor {
    private static final int MAX_RETRIES = 3;
    
    @Override
    public ClientHttpResponse intercept(
            HttpRequest request, 
            byte[] body, 
            ClientHttpRequestExecution execution) throws IOException {
        
        int retryCount = 0;
        IOException lastException = null;
        
        while (retryCount < MAX_RETRIES) {
            try {
                return execution.execute(request, body);
            } catch (IOException e) {
                lastException = e;
                retryCount++;
                
                if (retryCount < MAX_RETRIES) {
                    try {
                        Thread.sleep(1000 * retryCount); // Exponential backoff
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        throw new IOException("Retry interrupted", ie);
                    }
                }
            }
        }
        
        throw lastException;
    }
}

GraphQL

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

  • Гибкость запросов
  • Типизация
  • Единая точка входа

Недостатки:

  • Сложность кэширования
  • Потенциальные проблемы с производительностью

Асинхронная коммуникация

Очереди сообщений

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

  • Слабая связанность
  • Отказоустойчивость
  • Масштабируемость

Недостатки:

  • Сложность отладки
  • Потенциальные проблемы с порядком

Технический пример - асинхронная коммуникация с RabbitMQ:

// Order Service - публикует события
@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public Order createOrder(CreateOrderRequest request) {
        // Создаем заказ
        Order order = new Order();
        order.setCustomerId(request.getCustomerId());
        order.setItems(request.getItems());
        order.setStatus(OrderStatus.CREATED);
        order.setCreatedAt(LocalDateTime.now());
        
        order = orderRepository.save(order);
        
        // Публикуем событие о создании заказа
        OrderCreatedEvent event = new OrderCreatedEvent(
            order.getId(),
            order.getCustomerId(),
            order.getItems(),
            order.getTotalAmount(),
            order.getCreatedAt()
        );
        
        rabbitTemplate.convertAndSend("order.exchange", "order.created", event);
        
        return order;
    }
    
    public void cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException("Order not found"));
        
        if (order.getStatus() == OrderStatus.CREATED) {
            order.setStatus(OrderStatus.CANCELLED);
            orderRepository.save(order);
            
            // Публикуем событие об отмене заказа
            OrderCancelledEvent event = new OrderCancelledEvent(
                order.getId(),
                order.getCustomerId(),
                order.getItems(),
                order.getTotalAmount(),
                LocalDateTime.now()
            );
            
            rabbitTemplate.convertAndSend("order.exchange", "order.cancelled", event);
        }
    }
}

// Notification Service - подписывается на события
@Component
public class OrderEventHandler {
    @Autowired
    private NotificationService notificationService;
    
    @RabbitListener(queues = "order.created.queue")
    public void handleOrderCreated(OrderCreatedEvent event) {
        try {
            // Отправляем подтверждение заказа
            notificationService.sendOrderConfirmation(
                event.getCustomerId(),
                event.getOrderId(),
                event.getTotalAmount()
            );
            
            // Отправляем уведомление о статусе заказа
            notificationService.sendOrderStatusUpdate(
                event.getCustomerId(),
                event.getOrderId(),
                "Your order has been created and is being processed"
            );
            
        } catch (Exception e) {
            // Логируем ошибку, но не прерываем обработку
            log.error("Failed to send order confirmation for order: " + event.getOrderId(), e);
        }
    }
    
    @RabbitListener(queues = "order.cancelled.queue")
    public void handleOrderCancelled(OrderCancelledEvent event) {
        try {
            // Отправляем уведомление об отмене
            notificationService.sendOrderCancellation(
                event.getCustomerId(),
                event.getOrderId(),
                event.getTotalAmount()
            );
            
        } catch (Exception e) {
            log.error("Failed to send order cancellation for order: " + event.getOrderId(), e);
        }
    }
}

// Inventory Service - подписывается на события
@Component
public class InventoryEventHandler {
    @Autowired
    private InventoryService inventoryService;
    
    @RabbitListener(queues = "order.created.queue")
    public void handleOrderCreated(OrderCreatedEvent event) {
        try {
            // Резервируем товары
            for (OrderItem item : event.getItems()) {
                inventoryService.reserveItem(item.getProductId(), item.getQuantity());
            }
            
            // Публикуем событие о резервировании
            InventoryReservedEvent inventoryEvent = new InventoryReservedEvent(
                event.getOrderId(),
                event.getItems(),
                LocalDateTime.now()
            );
            
            rabbitTemplate.convertAndSend("inventory.exchange", "inventory.reserved", inventoryEvent);
            
        } catch (InsufficientStockException e) {
            // Если товаров недостаточно, отменяем заказ
            OrderCancelledEvent cancelEvent = new OrderCancelledEvent(
                event.getOrderId(),
                event.getCustomerId(),
                event.getItems(),
                event.getTotalAmount(),
                LocalDateTime.now()
            );
            
            rabbitTemplate.convertAndSend("order.exchange", "order.cancelled", cancelEvent);
            
        } catch (Exception e) {
            log.error("Failed to reserve inventory for order: " + event.getOrderId(), e);
        }
    }
    
    @RabbitListener(queues = "order.cancelled.queue")
    public void handleOrderCancelled(OrderCancelledEvent event) {
        try {
            // Освобождаем зарезервированные товары
            for (OrderItem item : event.getItems()) {
                inventoryService.releaseReservation(item.getProductId(), item.getQuantity());
            }
            
        } catch (Exception e) {
            log.error("Failed to release inventory for order: " + event.getOrderId(), e);
        }
    }
}

// Конфигурация RabbitMQ
@Configuration
@EnableRabbit
public class RabbitMQConfig {
    
    // Exchange для заказов
    @Bean
    public TopicExchange orderExchange() {
        return new TopicExchange("order.exchange");
    }
    
    // Очереди для событий заказов
    @Bean
    public Queue orderCreatedQueue() {
        return new Queue("order.created.queue", true);
    }
    
    @Bean
    public Queue orderCancelledQueue() {
        return new Queue("order.cancelled.queue", true);
    }
    
    // Привязки очередей к exchange
    @Bean
    public Binding orderCreatedBinding() {
        return BindingBuilder
            .bind(orderCreatedQueue())
            .to(orderExchange())
            .with("order.created");
    }
    
    @Bean
    public Binding orderCancelledBinding() {
        return BindingBuilder
            .bind(orderCancelledQueue())
            .to(orderExchange())
            .with("order.cancelled");
    }
    
    // Exchange для инвентаря
    @Bean
    public TopicExchange inventoryExchange() {
        return new TopicExchange("inventory.exchange");
    }
    
    @Bean
    public Queue inventoryReservedQueue() {
        return new Queue("inventory.reserved.queue", true);
    }
    
    @Bean
    public Binding inventoryReservedBinding() {
        return BindingBuilder
            .bind(inventoryReservedQueue())
            .to(inventoryExchange())
            .with("inventory.reserved");
    }
    
    // Конфигурация RabbitTemplate
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        template.setMessageConverter(new Jackson2JsonMessageConverter());
        return template;
    }
    
    // Конфигурация Jackson для сериализации
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

// События (DTO)
public class OrderCreatedEvent {
    private Long orderId;
    private Long customerId;
    private List<OrderItem> items;
    private BigDecimal totalAmount;
    private LocalDateTime createdAt;
    
    // конструкторы, геттеры, сеттеры
}

public class OrderCancelledEvent {
    private Long orderId;
    private Long customerId;
    private List<OrderItem> items;
    private BigDecimal totalAmount;
    private LocalDateTime cancelledAt;
    
    // конструкторы, геттеры, сеттеры
}

public class InventoryReservedEvent {
    private Long orderId;
    private List<OrderItem> items;
    private LocalDateTime reservedAt;
    
    // конструкторы, геттеры, сеттеры
}

Потоковая обработка событий

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

  • Высокая пропускная способность
  • Исторические данные
  • Real-time обработка

Технологии:

  • Apache Kafka
  • Apache Pulsar
  • AWS Kinesis

Паттерны коммуникации

1. Request-Response

Client → Service A → Service B
Client ← Service A ← Service B

2. Publish-Subscribe

Publisher → Event Bus → Subscriber 1
                      → Subscriber 2
                      → Subscriber 3

3. Request-Reply

Client → Message Queue → Service
Client ← Message Queue ← Service

4. Event Sourcing

Command → Aggregate → Event Store
Query  → Read Model ← Event Store

Обнаружение сервисов

Проблема: Как сервисы находят друг друга в динамической среде?

Решения:

1. Client-Side Discovery

@Service
public class PaymentServiceClient {
    @Autowired
    private ServiceDiscovery serviceDiscovery;
    
    public PaymentResult processPayment(PaymentRequest request) {
        // клиент сам находит сервис
        ServiceInstance instance = serviceDiscovery.getInstance("payment-service");
        String url = "http://" + instance.getHost() + ":" + instance.getPort();
        
        return restTemplate.postForObject(url + "/payments", request, PaymentResult.class);
    }
}

2. Server-Side Discovery

@Service
public class PaymentServiceClient {
    public PaymentResult processPayment(PaymentRequest request) {
        // сервис находится через load balancer
        String url = "http://payment-service/payments";
        return restTemplate.postForObject(url, request, PaymentResult.class);
    }
}

Инструменты:

  • Consul — service discovery и configuration
  • Eureka — Netflix service discovery
  • Kubernetes — встроенный service discovery