# Коммуникация между сервисами ## Навигация - [🏠 Главная страница курса](../../../README.md) - [📚 Модуль 2](../README.md) --- ## Синхронная коммуникация ### HTTP/REST **Преимущества:** - Простота - Широкая поддержка - Стандартизация **Недостатки:** - Связанность - Блокирующие вызовы - Проблемы с производительностью **Технический пример - HTTP/REST коммуникация:** ```java // Order Service - клиент для Payment Service @RestController @RequestMapping("/api/orders") public class OrderController { @Autowired private PaymentServiceClient paymentServiceClient; @Autowired private OrderService orderService; @PostMapping public ResponseEntity 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 entity = new HttpEntity<>(request, headers); ResponseEntity 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 entity = new HttpEntity<>(refundRequest, headers); ResponseEntity 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:** ```java // 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 items; private BigDecimal totalAmount; private LocalDateTime createdAt; // конструкторы, геттеры, сеттеры } public class OrderCancelledEvent { private Long orderId; private Long customerId; private List items; private BigDecimal totalAmount; private LocalDateTime cancelledAt; // конструкторы, геттеры, сеттеры } public class InventoryReservedEvent { private Long orderId; private List 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 ```java @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 ```java @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