Патерн проектування мікросервісів: Пояснення патерну Сага з прикладом (Частина 2)

Патерн проектування Saga використовується в мікросервісах для управління розподіленими транзакціями.

Два типи патернів Saga:

  1. Saga на основі хореографії (Event-Driven)
  2. Saga на основі оркестрації (Centralized Control)

Приклад: Замовлення продукту в системі електронної комерції

Уявіть систему онлайн-шопінгу з трьома мікросервісами:

  1. Сервіс замовлень → Створює замовлення
  2. Платіжний сервіс → Знімає кошти
  3. Інвентарний сервіс → Резервує товар

1.

Choreography-Based Saga (Event-Driven)

У цьому підході:

  • Кожен сервіс слухає події і реагує незалежно.
  • Сервіси комунікують асинхронно, використовуючи Kafka (Event Broker).
  • Відкат відбувається непрямо на основі сценаріїв з помилками.

Крок 1: Сервіс замовлення (Виділяє подію ORDER_CREATED)

@RestController  
@RequestMapping("/orders")  
public class OrderController {  
 @Autowired private KafkaTemplate kafkaTemplate;  
 @PostMapping  
 public ResponseEntity createOrder(@RequestBody OrderRequest request) {  
 // Логіка збереження замовлення (опущено)  
 kafkaTemplate.send("order-topic", "ORDER_CREATED:" + request.getOrderId());  
 return ResponseEntity.ok("Замовлення успішно розміщено!");  
 }  
}

Пояснення:

  • REST endpoint /orders дозволяє користувачам зробити замовлення.
  • Він публікує подію "ORDER_CREATED:" до Kafka order-topic.
  • Інші мікросервіси слухають цю подію, щоб продовжити процес.

Крок 2: Платіжний сервіс (Слухає подію ORDERCREATED і видає PAYMENTSUCCESS/FAILED)

@Component  
@KafkaListener(topics = "order-topic", groupId = "payment-group")  
public class PaymentListener {  
 @Autowired private KafkaTemplate kafkaTemplate;  
 public void processOrder(String message) {  
 if (message.startsWith("ORDER_CREATED")) {  
 String orderId = message.split(":")[1];  
 // Імітація обробки платежу  
 if (Math.random() > 0.2) { // 80% ймовірність успіху  
 kafkaTemplate.send("payment-topic", "PAYMENT_SUCCESS:" + orderId);  
 } else {  
 kafkaTemplate.send("payment-topic", "PAYMENT_FAILED:" + orderId);  
 }  
 }  
 }  
}

Пояснення:

  • Цей сервіс слухає події "ORDER_CREATED" з Kafka.
  • Він отримує orderId з події.
  • Імітує обробку платежу (80% ймовірність успіху).
  • Якщо платіж успішний, він видає "PAYMENT_SUCCESS:" в Kafka.
  • Якщо не вдалося, він видає "PAYMENT_FAILED:", і замовлення буде скасовано.

Крок 3: Інвентарний сервіс (Слухає подію PAYMENTSUCCESS і видає STOCKRESERVED/STOCKNOTAVAILABLE)

@Component  
@KafkaListener(topics = "payment-topic", groupId = "inventory-group")  
public class InventoryListener {  
 @Autowired private KafkaTemplate kafkaTemplate;  
 public void processPayment(String message) {  
 if (message.startsWith("PAYMENT_SUCCESS")) {  
 String orderId = message.split(":")[1];  
 // Імітація перевірки інвентарю  
 if (Math.random() > 0.1) { // 90% ймовірність наявності  
 kafkaTemplate.send("inventory-topic", "STOCK_RESERVED:" + orderId);  
 } else {  
 kafkaTemplate.send("inventory-topic", "STOCK_NOT_AVAILABLE:" + orderId);  
 }  
 } else if (message.startsWith("PAYMENT_FAILED")) {  
 System.out.println("Платіж не пройшов, замовлення скасовано.");  
 }  
 }  
}

🔍 Пояснення:

  • Цей сервіс слухає події "PAYMENT_SUCCESS".
  • Імітує перевірку інвентарю (90% ймовірність наявності товару).
  • Якщо товар є в наявності, він видає "STOCK_RESERVED:".
  • Якщо товар відсутній, він видає "STOCK_NOT_AVAILABLE:", що запускає відшкодування.

Крок 4: Сервіс замовлення (Слухає події STOCKRESERVED або STOCKNOT_AVAILABLE)

@Component  
@KafkaListener(topics = "inventory-topic", groupId = "order-group")  
public class OrderListener {  
 public void processInventory(String message) {  
 if (message.startsWith("STOCK_RESERVED")) {  
 System.out.println("Замовлення успішно завершено!");  
 } else if (message.startsWith("STOCK_NOT_AVAILABLE")) {  
 System.out.println("Товар недоступний, здійснюється повернення коштів.");  
 }  
 }  
}

Пояснення:

  • Слухає подію "STOCK_RESERVED" (замовлення успішне 🎉).
  • Слухає подію "STOCK_NOT_AVAILABLE" (замовлення скасовано, і кошти повертаються).

Як відбувається відкат у хореографії?

  • Якщо платіж не вдався, подія "PAYMENT_FAILED" скасовує замовлення.
  • Якщо товар відсутній, подія "STOCK_NOT_AVAILABLE" ініціює відшкодування.
  • Немає централізованого відкату, кожен сервіс реагує на події незалежно.

2.

Оркестраційна Сага (Централізоване Управління)

У цьому підході Saga Orchestrator сервіс контролює процес:

  • Викликає кожен сервіс послідовно за допомогою REST API.
  • Якщо будь-який етап не вдається, оркестратор ініціює відкат.

Крок 1: Saga Orchestrator (Керує Робочим Процесом)

@Service  
public class OrderSagaOrchestrator {  
 @Autowired private RestTemplate restTemplate;  
 public String startSaga(String orderId) {  
 // Крок 1: Створити замовлення  
 ResponseEntity orderResponse = restTemplate.postForEntity(  
 "http://localhost:8081/orders", orderId, String.class);  

 if (!orderResponse.getStatusCode().is2xxSuccessful()) {  
 return "Створення замовлення не вдалося!";  
 }  
 // Крок 2: Обробка оплати  
 ResponseEntity paymentResponse = restTemplate.postForEntity(  
 "http://localhost:8082/payments", orderId, String.class);  
 if (!paymentResponse.getStatusCode().is2xxSuccessful()) {  
 // Відкат замовлення  
 restTemplate.delete("http://localhost:8081/orders/" + orderId);  
 return "Оплата не вдалася, замовлення скасовано!";  
 }  
 // Крок 3: Резервування товару  
 ResponseEntity stockResponse = restTemplate.postForEntity(  
 "http://localhost:8083/inventory", orderId, String.class);  
 if (!stockResponse.getStatusCode().is2xxSuccessful()) {  
 // Відкат оплати  
 restTemplate.delete("http://localhost:8082/payments/" + orderId);  
 restTemplate.delete("http://localhost:8081/orders/" + orderId);  
 return "Товару немає в наявності, замовлення та оплата скасовані!";  
 }  
 return "Замовлення успішно завершено!";  
 }  
}

Пояснення:

  • Крок 1: Викликає Order Service для створення замовлення.
  • Крок 2: Викликає Payment Service для обробки оплати.
  • Крок 3: Викликає Inventory Service для резервування товару.
  • Логіка відкату:
  • Якщо оплата не вдалася, скасовується замовлення.
  • Якщо товару немає в наявності, повертається оплата і скасовується замовлення.

Крок 2: Order Service

@RestController  
@RequestMapping("/orders")  
public class OrderController {  
 private final Map orders = new HashMap<>();  
 @PostMapping  
 public ResponseEntity createOrder(@RequestBody String orderId) {  
 orders.put(orderId, "CREATED");  
 return ResponseEntity.ok("Замовлення створено");  
 }  
 @DeleteMapping("/{orderId}")  
 public ResponseEntity cancelOrder(@PathVariable String orderId) {  
 orders.remove(orderId);  
 return ResponseEntity.ok("Замовлення скасовано");  
 }  
}

Пояснення:

  • Створює замовлення та зберігає його в HashMap.
  • Якщо оркестратор викликає API DELETE, він видаляє замовлення.

Крок 3: Payment Service

@RestController  
@RequestMapping("/payments")  
public class PaymentController {  
 private final Map payments = new HashMap<>();  
 @PostMapping  
 public ResponseEntity processPayment(@RequestBody String orderId) {  
 payments.put(orderId, "PAID");  
 return ResponseEntity.ok("Оплата оброблена");  
 }  
 @DeleteMapping("/{orderId}")  
 public ResponseEntity refundPayment(@PathVariable String orderId) {  
 payments.remove(orderId);  
 return ResponseEntity.ok("Оплата повернена");  
 }  
}

Пояснення:

  • Якщо оплата успішна, вона зберігається з статусом PAID.
  • Якщо потрібен відкат, відбувається повернення оплати.

Підсумок: Коли використовувати що?

pic

Обидва підходи Choreography та Orchestration в патерні Сага допомагають управляти транзакціями у мікросервісах. Choreography є подієво-орієнтованим і децентралізованим, але може стати складним, тоді як Orchestration надає кращий контроль з центральним координатором.

Вибір підходу залежить від складності вашої системи, потреб у масштабованості та стратегії обробки помилок. Оберіть мудро, щоб побудувати надійні мікросервіси! 🚀

Який підхід ви обрали б? Давайте обговоримо!

Перекладено з: Microservices Design Pattern: Explain Saga Pattern with Example(Part 2)