Патерн проектування Saga використовується в мікросервісах для управління розподіленими транзакціями.
Два типи патернів Saga:
- Saga на основі хореографії (Event-Driven)
- Saga на основі оркестрації (Centralized Control)
Приклад: Замовлення продукту в системі електронної комерції
Уявіть систему онлайн-шопінгу з трьома мікросервісами:
- Сервіс замовлень → Створює замовлення
- Платіжний сервіс → Знімає кошти
- Інвентарний сервіс → Резервує товар
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
. - Якщо потрібен відкат, відбувається повернення оплати.
Підсумок: Коли використовувати що?
Обидва підходи Choreography та Orchestration в патерні Сага допомагають управляти транзакціями у мікросервісах. Choreography є подієво-орієнтованим і децентралізованим, але може стати складним, тоді як Orchestration надає кращий контроль з центральним координатором.
Вибір підходу залежить від складності вашої системи, потреб у масштабованості та стратегії обробки помилок. Оберіть мудро, щоб побудувати надійні мікросервіси! 🚀
Який підхід ви обрали б? Давайте обговоримо!
Перекладено з: Microservices Design Pattern: Explain Saga Pattern with Example(Part 2)