분산 트랜잭션에는 각 서비스가 트랜잭션의 일부를 수행하는 여러 마이크로서비스가 포함됩니다. 예를 들어 전자상거래 플랫폼에는 결제, 재고, 주문 관리와 같은 서비스가 포함될 수 있습니다. 거래를 완료하려면 이러한 서비스가 함께 작동해야 합니다. 그러나 이러한 서비스 중 하나가 실패하면 어떻게 됩니까?
주문 중에 다음 단계가 발생하는 전자상거래 애플리케이션을 상상해 보세요.
결제 금액이 차감된 후 주문이 생성되기 전에 재고 서비스가 실패하면 시스템이 일관성 없는 상태가 됩니다. 고객에게 요금이 청구되었으나 주문이 이루어지지 않았습니다.
이러한 실패를 처리하려면 2단계 커밋 프로토콜을 사용하는 분산 트랜잭션을 사용하는 것이 좋습니다. 그러나 이로 인해 몇 가지 문제가 발생합니다.
분산 시스템에서는 트랜잭션이 여러 마이크로서비스에 걸쳐 이루어지는 경우가 많습니다. 모든 서비스가 성공적으로 완료되거나 전혀 완료되지 않도록 하는 것은 어려운 일입니다. 2단계 커밋이 포함된 분산 트랜잭션을 사용하여 이를 처리하는 기존 방법은 높은 대기 시간, 긴밀한 결합 및 가용성 감소와 같은 문제로 인해 문제가 될 수 있습니다.
Saga 패턴은 보다 유연한 접근 방식을 제공합니다. Saga 패턴은 트랜잭션을 단일 단위로 실행하려고 시도하는 대신 독립적으로 수행할 수 있는 더 작고 격리된 단계로 트랜잭션을 나눕니다. 각 단계는 데이터베이스를 업데이트하고 다음 단계를 트리거하는 로컬 트랜잭션입니다. 단계가 실패하면 시스템은 보상 작업을 수행하여 이전 단계에서 변경한 내용을 취소하여 시스템이 일관된 상태로 돌아갈 수 있도록 합니다.
Saga 패턴은 기본적으로 차례로 실행되는 작은 트랜잭션의 시퀀스입니다. 작동 방식은 다음과 같습니다.
사가 패턴을 구현하는 방법은 크게 안무와 오케스트레이션 두 가지가 있습니다.
안무사가에는 중앙 코디네이터가 없습니다. 대신, Saga와 관련된 각 서비스는 이벤트를 수신하고 이전 단계의 결과에 따라 조치를 취할 시기를 결정합니다. 이 접근 방식은 분산되어 있어 서비스가 독립적으로 작동할 수 있습니다. 작동 방식은 다음과 같습니다.
안무의 장점:
안무의 어려움:
오케스트레이션 사가에서는 중앙 오케스트레이터가 트랜잭션 흐름을 제어합니다. 오케스트레이터는 단계의 순서를 결정하고 서비스 간의 통신을 처리합니다. 작동 방식은 다음과 같습니다.
오케스트레이션의 장점:
오케스트레이션의 과제:
전자상거래 시나리오를 고려하고 Saga 패턴을 사용하여 구현해 보겠습니다.
커피 구매 시나리오에서 각 서비스는 현지 거래를 나타냅니다. Coffee Service는 구매를 완료하기 위해 다른 서비스를 조정하는 이 시리즈의 조정자 역할을 합니다.
이 이야기의 진행 방식은 다음과 같습니다.
제가 구현한 Saga에서 각 SagaItemBuilder는 분산 트랜잭션 흐름의 한 단계를 나타냅니다. ActionBuilder는 오류가 발생할 경우 실행할 기본 작업과 롤백 작업을 포함하여 수행할 작업을 정의합니다. ActionBuilder는 세 가지 정보를 요약합니다.
comComponent : 호출될 메소드가 상주하는 Bean 인스턴스입니다.
method : 호출할 메소드의 이름입니다.
args : 메소드에 전달될 인수입니다.
ActionBuilder
public class ActionBuilder { private Object component; private String method; private Object[] args; public static ActionBuilder builder() { return new ActionBuilder(); } public ActionBuilder component(Object component) { this.component = component; return this; } public ActionBuilder method(String method) { this.method = method; return this; } public ActionBuilder args(Object... args) { this.args = args; return this; } public Object getComponent() { return component; } public String getMethod() { return method; } public Object[] getArgs() { return args; } }
SagaItemBuilder
import java.util.HashMap; import java.util.Map; import java.util.Objects; public class SagaItemBuilder { private ActionBuilder action; private Map<Class<? extends Exception>, ActionBuilder> onBehaviour; public static SagaItemBuilder builder() { return new SagaItemBuilder(); } public SagaItemBuilder action(ActionBuilder action) { this.action = action; return this; } public SagaItemBuilder onBehaviour(Class<? extends Exception> exception, ActionBuilder action) { if (Objects.isNull(onBehaviour)) onBehaviour = new HashMap<>(); onBehaviour.put(exception, action); return this; } public ActionBuilder getAction() { return action; } public Map<Class<? extends Exception>, ActionBuilder> getBehaviour() { return onBehaviour; } }
시나리오
import java.util.ArrayList; import java.util.List; public class Scenarios { List<SagaItemBuilder> scenarios; public static Scenarios builder() { return new Scenarios(); } public Scenarios scenario(SagaItemBuilder sagaItemBuilder) { if (scenarios == null) scenarios = new ArrayList<>(); scenarios.add(sagaItemBuilder); return this; } public List<SagaItemBuilder> getScenario() { return scenarios; } }
다음은 배포 트랜잭션을 커밋하는 방법입니다.
package com.example.demo.saga; import com.example.demo.saga.exception.CanNotRollbackException; import com.example.demo.saga.exception.RollBackException; import com.example.demo.saga.pojo.ActionBuilder; import com.example.demo.saga.pojo.SagaItemBuilder; import com.example.demo.saga.pojo.Scenarios; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.Map; import java.util.Set; @Component public class DTC { public boolean commit(Scenarios scenarios) throws Exception { validate(scenarios); for (int i = 0; i < scenarios.getScenario().size(); i++) { SagaItemBuilder scenario = scenarios.getScenario().get(i); ActionBuilder action = scenario.getAction(); Object bean = action.getComponent(); String method = action.getMethod(); Object[] args = action.getArgs(); try { invoke(bean, method, args); } catch (Exception e) { rollback(scenarios, i, e); return false; } } return true; } private void rollback(Scenarios scenarios, Integer failStep, Exception currentStepFailException) { for (int i = failStep; i >= 0; i--) { SagaItemBuilder scenario = scenarios.getScenario().get(i); Map<Class<? extends Exception>, ActionBuilder> behaviours = scenario.getBehaviour(); Set<Class<? extends Exception>> exceptions = behaviours.keySet(); ActionBuilder actionWhenException = null; if (failStep == i) { for(Class<? extends Exception> exception: exceptions) { if (exception.isInstance(currentStepFailException)) { actionWhenException = behaviours.get(exception); } } if (actionWhenException == null) actionWhenException = behaviours.get(RollBackException.class); } else { actionWhenException = behaviours.get(RollBackException.class); } Object bean = actionWhenException.getComponent(); String method = actionWhenException.getMethod(); Object[] args = actionWhenException.getArgs(); try { invoke(bean, method, args); } catch (Exception e) { throw new CanNotRollbackException("Error in %s belong to %s. Can not rollback transaction".formatted(method, bean.getClass())); } } } private void validate(Scenarios scenarios) throws Exception { for (int i = 0; i < scenarios.getScenario().size(); i++) { SagaItemBuilder scenario = scenarios.getScenario().get(i); ActionBuilder action = scenario.getAction(); if (action.getComponent() == null) throw new Exception("Missing bean in scenario"); if (action.getMethod() == null) throw new Exception("Missing method in scenario"); Map<Class<? extends Exception>, ActionBuilder> behaviours = scenario.getBehaviour(); Set<Class<? extends Exception>> exceptions = behaviours.keySet(); if (exceptions.contains(null)) throw new Exception("Exception can not be null in scenario has method %s, bean %s " .formatted(action.getMethod(), action.getComponent().getClass())); if (!exceptions.contains(RollBackException.class)) throw new Exception("Missing default RollBackException in scenario has method %s, bean %s " .formatted(action.getMethod(), action.getComponent().getClass())); } } public String invoke(Object bean, String methodName, Object... args) throws Exception { try { Class<?>[] paramTypes = new Class[args.length]; for (int i = 0; i < args.length; i++) { paramTypes[i] = parameterType(args[i]); } Method method = bean.getClass().getDeclaredMethod(methodName, paramTypes); Object result = method.invoke(bean, args); return result != null ? result.toString() : null; } catch (Exception e) { throw e; } } private static Class<?> parameterType (Object o) { if (o instanceof Integer) { return int.class; } else if (o instanceof Boolean) { return boolean.class; } else if (o instanceof Double) { return double.class; } else if (o instanceof Float) { return float.class; } else if (o instanceof Long) { return long.class; } else if (o instanceof Short) { return short.class; } else if (o instanceof Byte) { return byte.class; } else if (o instanceof Character) { return char.class; } else { return o.getClass(); } } }
외부 서비스를 호출하는 서비스가 3개 있습니다: BillingService , OrderService , PaymentService.
주문서비스
package com.example.demo.service; import org.springframework.stereotype.Service; @Service public class OrderService { public String prepareOrder(String name, int number) { System.out.println("Prepare order for %s with order id %d ".formatted(name, number)); return "Prepare order for %s with order id %d ".formatted(name, number); } public void Rollback_prepareOrder_NullPointException() { System.out.println("Rollback prepareOrder because NullPointException"); } public void Rollback_prepareOrder_RollBackException() { System.out.println("Rollback prepareOrder because RollBackException"); } }
결제서비스
package com.example.demo.service; import org.springframework.stereotype.Service; @Service public class BillingService { public String prepareBilling(String name, int number) { System.out.println("Prepare billing for %s with order id %d ".formatted(name, number)); return "Prepare billing for %s with order id %d ".formatted(name, number); } public String createBilling(String name, int number) { System.out.println("Create billing for %s with order id %d ".formatted(name, number)); return "Create billing for %s with order id %d ".formatted(name, number); } public void Rollback_prepareBilling_NullPointException() { System.out.println("Rollback prepareBilling because NullPointException"); } public void Rollback_prepareBilling_ArrayIndexOutOfBoundsException() { System.out.println("Rollback prepareBilling because ArrayIndexOutOfBoundsException"); } public void Rollback_prepareBilling_RollBackException() { System.out.println("Rollback prepareBilling because RollBackException"); } public void Rollback_createBilling_NullPointException() { System.out.println("Rollback createBilling because NullPointException"); } public void Rollback_createBilling_ArrayIndexOutOfBoundsException() { System.out.println("Rollback createBilling because ArrayIndexOutOfBoundsException"); } public void Rollback_createBilling_RollBackException() { System.out.println("Rollback createBilling because RollBackException"); } }
결제서비스
package com.example.demo.service; import org.springframework.stereotype.Service; @Service public class PaymentService { public String createPayment() { System.out.println("Create payment"); return "Create payment"; } public void Rollback_createPayment_NullPointException() { System.out.println("Rollback createPayment because NullPointException"); } public void Rollback_createPayment_RollBackException() { System.out.println("Rollback createPayment because RollBackException"); } }
그리고 커피서비스에서는 아래와 같이 구현하는데, 시나리오를 작성한 후 커밋합니다.
package com.example.demo.service; import com.example.demo.saga.DTC; import com.example.demo.saga.exception.RollBackException; import com.example.demo.saga.pojo.ActionBuilder; import com.example.demo.saga.pojo.SagaItemBuilder; import com.example.demo.saga.pojo.Scenarios; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class CoffeeService { @Autowired private OrderService orderService; @Autowired private BillingService billingService; @Autowired private PaymentService paymentService; @Autowired private DTC dtc; public String test() throws Exception { Scenarios scenarios = Scenarios.builder() .scenario( SagaItemBuilder.builder() .action(ActionBuilder.builder().component(orderService).method("prepareOrder").args("tuanh.net", 123)) .onBehaviour(NullPointerException.class, ActionBuilder.builder().component(orderService).method("Rollback_prepareOrder_NullPointException").args()) .onBehaviour(RollBackException.class, ActionBuilder.builder().component(orderService).method("Rollback_prepareOrder_RollBackException").args()) ).scenario( SagaItemBuilder.builder() .action(ActionBuilder.builder().component(billingService).method("prepareBilling").args("tuanh.net", 123)) .onBehaviour(NullPointerException.class, ActionBuilder.builder().component(billingService).method("Rollback_prepareBilling_NullPointException").args()) .onBehaviour(RollBackException.class, ActionBuilder.builder().component(billingService).method("Rollback_prepareBilling_RollBackException").args()) ).scenario( SagaItemBuilder.builder() .action(ActionBuilder.builder().component(billingService).method("createBilling").args("tuanh.net", 123)) .onBehaviour(NullPointerException.class, ActionBuilder.builder().component(billingService).method("Rollback_createBilling_ArrayIndexOutOfBoundsException").args()) .onBehaviour(RollBackException.class, ActionBuilder.builder().component(billingService).method("Rollback_createBilling_RollBackException").args()) ).scenario( SagaItemBuilder.builder() .action(ActionBuilder.builder().component(paymentService).method("createPayment").args()) .onBehaviour(NullPointerException.class, ActionBuilder.builder().component(paymentService).method("Rollback_createPayment_NullPointException").args()) .onBehaviour(RollBackException.class, ActionBuilder.builder().component(paymentService).method("Rollback_createPayment_RollBackException").args()) ); dtc.commit(scenarios); return "ok"; } }
빌링 생성 시 예외를 적용한 경우
public String createBilling(String name, int number) { throw new NullPointerException(); }
결과
2024-08-24T14:21:45.445+07:00 INFO 19736 --- [demo] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/' 2024-08-24T14:21:45.450+07:00 INFO 19736 --- [demo] [main] com.example.demo.DemoApplication : Started DemoApplication in 1.052 seconds (process running for 1.498) 2024-08-24T14:21:47.756+07:00 INFO 19736 --- [demo] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2024-08-24T14:21:47.756+07:00 INFO 19736 --- [demo] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2024-08-24T14:21:47.757+07:00 INFO 19736 --- [demo] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms Prepare order for tuanh.net with order id 123 Prepare billing for tuanh.net with order id 123 Rollback createBilling because RollBackException Rollback prepareBilling because RollBackException Rollback prepareOrder because RollBackException
내 GitHub 저장소를 확인하세요
요약하자면, Saga 패턴은 분산 트랜잭션을 더 작고 관리 가능한 단계로 나누어 관리하기 위한 강력한 솔루션을 제공합니다. 안무와 오케스트레이션 사이의 선택은 시스템의 특정 요구 사항과 아키텍처에 따라 달라집니다. 안무는 느슨한 결합과 탄력성을 제공하는 반면, 오케스트레이션은 중앙 집중식 제어와 보다 쉬운 모니터링을 제공합니다. Saga 패턴으로 시스템을 신중하게 설계하면 분산 마이크로서비스 아키텍처에서 일관성, 가용성 및 유연성을 얻을 수 있습니다.
질문이 있거나 시스템에 Saga 패턴을 구현하는 데 대한 추가 설명이 필요한 경우 아래에 자유롭게 의견을 남겨주세요!
에서 더 많은 게시물 읽기: 사가 패턴이 분산 트랜잭션 문제를 해결하는 방법: 방법 및 실제 사례
위 내용은 Saga 패턴이 분산 트랜잭션 문제를 해결하는 방법: 방법 및 실제 사례의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!