- 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
542 lines
18 KiB
Markdown
542 lines
18 KiB
Markdown
# Коммуникация между сервисами
|
|
|
|
## Навигация
|
|
- [🏠 Главная страница курса](../../../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<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:**
|
|
|
|
```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<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
|
|
```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
|