”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > Saga 模式如何解决分布式事务问题:方法和实际示例

Saga 模式如何解决分布式事务问题:方法和实际示例

发布于2024-11-01
浏览:854

1. Understanding the Problem: The Complexity of Distributed Transactions

How the Saga Pattern Resolves Distributed Transaction Issues: Methods and Real-World Example

Distributed transactions involve multiple microservices, where each service performs a part of a transaction. For instance, an e-commerce platform might involve services like payment, inventory, and order management. These services need to work together to complete a transaction. However, what happens if one of these services fails?

1.1 A Real-World Scenario

Imagine an e-commerce application where the following steps occur during an order placement:

  • Step 1 : Deduct payment from the customer’s account.
  • Step 2 : Reduce the item count in the inventory.
  • Step 3 : Create an order in the order management system.

How the Saga Pattern Resolves Distributed Transaction Issues: Methods and Real-World Example

If the inventory service fails after the payment is deducted but before the order is created, the system ends up in an inconsistent state. The customer is charged, but no order is placed.

1.2 Traditional Solutions and Their Limitations

To handle such failures, one might consider using a distributed transaction with a two-phase commit protocol. However, this introduces several issues:

  • High Latency : Each service must lock resources during the transaction, leading to increased latency.
  • Reduced Availability : If any service fails, the entire transaction is rolled back, reducing the overall system availability.
  • Tight Coupling : Services become tightly coupled, making it harder to scale or modify individual services.

2. How the Saga Pattern Solves the Problem

In distributed systems, transactions often span multiple microservices. Ensuring that all services either complete successfully or none at all is challenging. The traditional way of handling this—using distributed transactions with two-phase commit—can be problematic due to issues like high latency, tight coupling, and reduced availability.

How the Saga Pattern Resolves Distributed Transaction Issues: Methods and Real-World Example

The Saga pattern offers a more flexible approach. Instead of attempting to execute a transaction as a single unit, the Saga pattern breaks down the transaction into smaller, isolated steps that can be performed independently. Each step is a local transaction that updates the database and then triggers the next step. If a step fails, the system performs compensating actions to undo the changes made by previous steps, ensuring that the system can return to a consistent state.

2.1 What is the Saga Pattern?

The Saga pattern is essentially a sequence of smaller transactions that are executed one after the other. Here’s how it works:

  • Local Transactions : Each service involved in the transaction performs its own local transaction. For instance, in an order processing system, one service might handle payment, another inventory, and yet another the order record.
  • Event or Message Publishing : After a service completes its local transaction, it publishes an event or sends a message indicating the successful completion of that step. For example, after the payment is processed, the payment service might publish a "PaymentCompleted" event.
  • Triggering the Next Step : The next service in the sequence listens for the event and, upon receiving it, proceeds with its local transaction. This continues until all steps in the transaction are completed.
  • Compensating Actions : If any step fails, compensating actions are invoked. These actions are designed to reverse the changes made by the previous steps. For instance, if the inventory reduction fails after payment, a compensating action would refund the payment.

2.2 Types of Sagas

There are two main ways to implement the Saga pattern: Choreography and Orchestration.

2.2.1 Choreography Saga

In a Choreography Saga, there is no central coordinator. Instead, each service involved in the Saga listens for events and decides when to act based on the outcome of previous steps. This approach is decentralized and allows services to operate independently. Here’s how it works:

  • Event-Based Coordination : Each service is responsible for handling the events that are relevant to it. For example, after the payment service processes a payment, it emits a "PaymentCompleted" event. The inventory service listens for this event and, when it receives it, deducts the item count.
  • Decentralized Control : Since there is no central coordinator, each service must know what to do next based on the events it receives. This gives the system more flexibility but requires careful planning to ensure that all services understand the correct sequence of operations.
  • Compensating Actions : If a service detects that something went wrong, it can emit a failure event, which other services listen for to trigger compensating actions. For example, if the inventory service cannot update the inventory, it might emit an "InventoryUpdateFailed" event, which the payment service listens for to trigger a refund.

Advantages of Choreography:

  • Loose Coupling : Services are loosely coupled, which makes it easier to scale and modify individual services.
  • Resilience : Since each service acts independently, the system can be more resilient to failures in individual services.

Challenges of Choreography:

  • Complexity : As the number of services grows, managing and understanding the flow of events can become complex.
  • Lack of Central Control : Without a central coordinator, it can be harder to monitor and debug the overall transaction flow.

2.2.2 Orchestration Saga

In an Orchestration Saga, a central orchestrator controls the flow of the transaction. The orchestrator determines the sequence of steps and handles the communication between services. Here’s how it works:

  • Centralized Control : The orchestrator sends commands to each service in sequence. For example, the orchestrator might first instruct the payment service to process a payment. Once that’s done, it tells the inventory service to update the inventory, and so on.
  • Sequential Execution : Each service performs its task only when instructed by the orchestrator, ensuring that the steps occur in the correct order.
  • Compensation Logic : The orchestrator is also responsible for initiating compensating actions if something goes wrong. For example, if the inventory update fails, the orchestrator can command the payment service to refund the payment.

Advantages of Orchestration:

  • Centralized Control : With a single orchestrator, it’s easier to monitor, manage, and debug the transaction flow.
  • Simpler Logic : Since the orchestrator handles the flow, individual services don’t need to be aware of the overall transaction sequence.

Challenges of Orchestration:

  • Single Point of Failure : The orchestrator can become a bottleneck or single point of failure if not designed for high availability.
  • Tight Coupling to the Orchestrator : Services are dependent on the orchestrator, which can make the system less flexible compared to choreography.

3. Implementing the Simple Orchestration Saga Pattern: A Step-by-Step Guide

Let's consider the e-commerce scenario and implement it using the Saga pattern.

In our coffee purchasing scenario, each service represents a local transaction. The Coffee Service acts as the orchestrator of this saga, coordinating the other services to complete the purchase.

Here's a breakdown of how the saga might work:

  • Customer places an order : The customer places an order through the Order Service.
  • Coffee Service initiates the saga : The Coffee Service receives the order and initiates the saga.
  • Order Service creates an order : The Order Service creates a new order and persists it.
  • Billing Service calculates the cost : The Billing Service calculates the total cost of the order and creates a billing record.
  • Payment Service processes the payment : The Payment Service processes the payment.
  • Coffee Service updates order status : Once the payment is successful, the Coffee Service updates the order status to "completed".

How the Saga Pattern Resolves Distributed Transaction Issues: Methods and Real-World Example

3.1 Transaction entity

How the Saga Pattern Resolves Distributed Transaction Issues: Methods and Real-World Example

In my implementation of the saga, each SagaItemBuilder represents a step in our distributed transaction flow. The ActionBuilder defines the actions to be performed, including the main action and the rollback action that will be executed if an error occurs. The ActionBuilder encapsulates three pieces of information:

component : The bean instance where the method to be invoked resides.

method : The name of the method to be called.

args : The arguments to be passed to the method.

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, 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, ActionBuilder> getBehaviour() {
        return onBehaviour;
    }
}

Scenarios

import java.util.ArrayList;
import java.util.List;

public class Scenarios {
    List 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 getScenario() {
        return scenarios;
    }
}

Bellow is how can I commit the distribute transaction.

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 = 0; i--) {
            SagaItemBuilder scenario = scenarios.getScenario().get(i);
            Map, ActionBuilder> behaviours = scenario.getBehaviour();
            Set> 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 , ActionBuilder> behaviours = scenario.getBehaviour();
            Set> 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  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.2 Using it

I have 3 services that call to external service: BillingService , OrderService , PaymentService.

OrderService

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");
    }
}

BillingService

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");
    }
}

PaymentService

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");
    }
}

And in Coffee Service, I implement it as follows, I create a scenario and then commit it.

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";
    }
}

3.3 Result

When i make a exception in create billing.

public String createBilling(String name, int number) {
    throw new NullPointerException();
}

Result

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

Check out my GitHub Repository

4. Conclusion

In summary, the Saga pattern provides a robust solution for managing distributed transactions by breaking them down into smaller, manageable steps. The choice between Choreography and Orchestration depends on the specific needs and architecture of your system. Choreography offers loose coupling and resilience, while Orchestration provides centralized control and easier monitoring. By carefully designing your system with the Saga pattern, you can achieve consistency, availability, and flexibility in your distributed microservices architecture.

Feel free to comment below if you have any questions or need further clarification on implementing the Saga pattern in your system!

Read posts more at : How the Saga Pattern Resolves Distributed Transaction Issues: Methods and Real-World Example

版本声明 本文转载于:https://dev.to/anh_trntun_4732cf3d299/how-the-saga-pattern-resolves-distributed-transaction-issues-methods-and-real-world-example-48ki?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • Python元类工作原理及类创建与定制
    Python元类工作原理及类创建与定制
    python中的metaclasses是什么? Metaclasses负责在Python中创建类对象。就像类创建实例一样,元类也创建类。他们提供了对类创建过程的控制层,允许自定义类行为和属性。在Python中理解类作为对象的概念,类是描述用于创建新实例或对象的蓝图的对象。这意味着类本身是使用类关...
    编程 发布于2025-04-26
  • 对象拟合:IE和Edge中的封面失败,如何修复?
    对象拟合:IE和Edge中的封面失败,如何修复?
    To resolve this issue, we employ a clever CSS solution that solves the problem:position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%)...
    编程 发布于2025-04-26
  • 如何在鼠标单击时编程选择DIV中的所有文本?
    如何在鼠标单击时编程选择DIV中的所有文本?
    在鼠标上选择div文本单击带有文本内容,用户如何使用单个鼠标单击单击div中的整个文本?这允许用户轻松拖放所选的文本或直接复制它。 在单个鼠标上单击的div元素中选择文本,您可以使用以下Javascript函数: function selecttext(canduterid){ if(do...
    编程 发布于2025-04-26
  • 在JavaScript中如何并发运行异步操作并正确处理错误?
    在JavaScript中如何并发运行异步操作并正确处理错误?
    同意操作execution 在执行asynchronous操作时,相关的代码段落会遇到一个问题,当执行asynchronous操作:此实现在启动下一个操作之前依次等待每个操作的完成。要启用并发执行,需要进行修改的方法。 第一个解决方案试图通过获得每个操作的承诺来解决此问题,然后单独等待它们: co...
    编程 发布于2025-04-26
  • 为什么使用Firefox后退按钮时JavaScript执行停止?
    为什么使用Firefox后退按钮时JavaScript执行停止?
    导航历史记录问题:JavaScript使用Firefox Back Back 此行为是由浏览器缓存JavaScript资源引起的。要解决此问题并确保在后续页面访问中执行脚本,Firefox用户应设置一个空功能。 警报'); }; alert('inline Alert')...
    编程 发布于2025-04-26
  • JavaScript计算两个日期之间天数的方法
    JavaScript计算两个日期之间天数的方法
    How to Calculate the Difference Between Dates in JavascriptAs you attempt to determine the difference between two dates in Javascript, consider this s...
    编程 发布于2025-04-26
  • 为什么不````''{margin:0; }`始终删除CSS中的最高边距?
    为什么不````''{margin:0; }`始终删除CSS中的最高边距?
    在CSS 问题:不正确的代码: 全球范围将所有余量重置为零,如提供的代码所建议的,可能会导致意外的副作用。解决特定的保证金问题是更建议的。 例如,在提供的示例中,将以下代码添加到CSS中,将解决余量问题: body H1 { 保证金顶:-40px; } 此方法更精确,避免了由全局保证金重置引...
    编程 发布于2025-04-26
  • 为什么不使用CSS`content'属性显示图像?
    为什么不使用CSS`content'属性显示图像?
    在Firefox extemers属性为某些图像很大,&& && && &&华倍华倍[华氏华倍华氏度]很少见,却是某些浏览属性很少,尤其是特定于Firefox的某些浏览器未能在使用内容属性引用时未能显示图像的情况。这可以在提供的CSS类中看到:。googlepic { 内容:url(&#...
    编程 发布于2025-04-26
  • 如何从PHP中的数组中提取随机元素?
    如何从PHP中的数组中提取随机元素?
    从阵列中的随机选择,可以轻松从数组中获取随机项目。考虑以下数组:; 从此数组中检索一个随机项目,利用array_rand( array_rand()函数从数组返回一个随机键。通过将$项目数组索引使用此键,我们可以从数组中访问一个随机元素。这种方法为选择随机项目提供了一种直接且可靠的方法。
    编程 发布于2025-04-26
  • 如何在无序集合中为元组实现通用哈希功能?
    如何在无序集合中为元组实现通用哈希功能?
    在未订购的集合中的元素要纠正此问题,一种方法是手动为特定元组类型定义哈希函数,例如: template template template 。 struct std :: hash { size_t operator()(std :: tuple const&tuple)const {...
    编程 发布于2025-04-26
  • 如何同步迭代并从PHP中的两个等级阵列打印值?
    如何同步迭代并从PHP中的两个等级阵列打印值?
    同步的迭代和打印值来自相同大小的两个数组使用两个数组相等大小的selectbox时,一个包含country代码的数组,另一个包含乡村代码,另一个包含其相应名称的数组,可能会因不当提供了exply for for for the uncore for the forsion for for ytry...
    编程 发布于2025-04-26
  • HTML格式标签
    HTML格式标签
    HTML 格式化元素 **HTML Formatting is a process of formatting text for better look and feel. HTML provides us ability to format text without us...
    编程 发布于2025-04-26
  • 为什么在我的Linux服务器上安装Archive_Zip后,我找不到“ class \” class \'ziparchive \'错误?
    为什么在我的Linux服务器上安装Archive_Zip后,我找不到“ class \” class \'ziparchive \'错误?
    class'ziparchive'在Linux Server上安装Archive_zip时找不到错误 commant in lin ins in cland ins in lin.11 on a lin.1 in a lin.11错误:致命错误:在... cass中找不到类z...
    编程 发布于2025-04-26
  • CSS强类型语言解析
    CSS强类型语言解析
    您可以通过其强度或弱输入的方式对编程语言进行分类的方式之一。在这里,“键入”意味着是否在编译时已知变量。一个例子是一个场景,将整数(1)添加到包含整数(“ 1”)的字符串: result = 1 "1";包含整数的字符串可能是由带有许多运动部件的复杂逻辑套件无意间生成的。它也可以是故意从单个真理...
    编程 发布于2025-04-26
  • 如何配置Pytesseract以使用数字输出的单位数字识别?
    如何配置Pytesseract以使用数字输出的单位数字识别?
    Pytesseract OCR具有单位数字识别和仅数字约束 在pytesseract的上下文中,在配置tesseract以识别单位数字和限制单个数字和限制输出对数字可能会提出质疑。 To address this issue, we delve into the specifics of Te...
    编程 发布于2025-04-26

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3