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

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

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

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]删除
最新教程 更多>
  • 如何在Java字符串中有效替换多个子字符串?
    如何在Java字符串中有效替换多个子字符串?
    在java 中有效地替换多个substring,需要在需要替换一个字符串中的多个substring的情况下,很容易求助于重复应用字符串的刺激力量。 However, this can be inefficient for large strings or when working with nu...
    编程 发布于2025-07-16
  • Go语言如何动态发现导出包类型?
    Go语言如何动态发现导出包类型?
    与反射软件包中的有限类型的发现能力相反,本文探讨了在运行时发现所有包装类型(尤其是struntime go import( “ FMT” “去/进口商” ) func main(){ pkg,err:= incorter.default()。导入(“ time”) ...
    编程 发布于2025-07-16
  • 如何在JavaScript对象中动态设置键?
    如何在JavaScript对象中动态设置键?
    在尝试为JavaScript对象创建动态键时,如何使用此Syntax jsObj['key' i] = 'example' 1;不工作。正确的方法采用方括号: jsobj ['key''i] ='example'1; 在JavaScript中,数组是一...
    编程 发布于2025-07-16
  • 使用jQuery如何有效修改":after"伪元素的CSS属性?
    使用jQuery如何有效修改":after"伪元素的CSS属性?
    在jquery中了解伪元素的限制:访问“ selector 尝试修改“:”选择器的CSS属性时,您可能会遇到困难。 This is because pseudo-elements are not part of the DOM (Document Object Model) and are th...
    编程 发布于2025-07-16
  • CSS可以根据任何属性值来定位HTML元素吗?
    CSS可以根据任何属性值来定位HTML元素吗?
    靶向html元素,在CSS 中使用任何属性值,在CSS中,可以基于特定属性(如下所示)基于特定属性的基于特定属性的emants目标元素: 字体家庭:康斯拉斯(Consolas); } 但是,出现一个常见的问题:元素可以根据任何属性值而定位吗?本文探讨了此主题。的目标元素有任何任何属性值,属...
    编程 发布于2025-07-16
  • 为什么使用Firefox后退按钮时JavaScript执行停止?
    为什么使用Firefox后退按钮时JavaScript执行停止?
    导航历史记录问题:JavaScript使用Firefox Back Back 此行为是由浏览器缓存JavaScript资源引起的。要解决此问题并确保在后续页面访问中执行脚本,Firefox用户应设置一个空功能。 警报'); }; alert('inline Alert')...
    编程 发布于2025-07-16
  • 如何使用FormData()处理多个文件上传?
    如何使用FormData()处理多个文件上传?
    )处理多个文件输入时,通常需要处理多个文件上传时,通常是必要的。 The fd.append("fileToUpload[]", files[x]); method can be used for this purpose, allowing you to send multi...
    编程 发布于2025-07-16
  • C++成员函数指针正确传递方法
    C++成员函数指针正确传递方法
    如何将成员函数置于c [&& && && && && && && && && && &&&&&&&&&&&&&&&&&&&&&&&华仪的函数时,在接受成员函数指针的函数时,要在函数上既要提供指针又可以提供指针和指针到函数的函数。需要具有一定签名的功能指针。要通过成员函数,您需要同时提供对象指针(此...
    编程 发布于2025-07-16
  • 如何同步迭代并从PHP中的两个等级阵列打印值?
    如何同步迭代并从PHP中的两个等级阵列打印值?
    同步的迭代和打印值来自相同大小的两个数组使用两个数组相等大小的selectbox时,一个包含country代码的数组,另一个包含乡村代码,另一个包含其相应名称的数组,可能会因不当提供了exply for for for the uncore for the forsion for for ytry...
    编程 发布于2025-07-16
  • 在Pandas中如何将年份和季度列合并为一个周期列?
    在Pandas中如何将年份和季度列合并为一个周期列?
    pandas data frame thing commans date lay neal and pree pree'和pree pree pree”,季度 2000 q2 这个目标是通过组合“年度”和“季度”列来创建一个新列,以获取以下结果: [python中的concate...
    编程 发布于2025-07-16
  • 如何将来自三个MySQL表的数据组合到新表中?
    如何将来自三个MySQL表的数据组合到新表中?
    mysql:从三个表和列的新表创建新表 答案:为了实现这一目标,您可以利用一个3-way Join。 选择p。*,d.content作为年龄 来自人为p的人 加入d.person_id = p.id上的d的详细信息 加入T.Id = d.detail_id的分类法 其中t.taxonomy =...
    编程 发布于2025-07-16
  • 如何干净地删除匿名JavaScript事件处理程序?
    如何干净地删除匿名JavaScript事件处理程序?
    删除匿名事件侦听器将匿名事件侦听器添加到元素中会提供灵活性和简单性,但是当要删除它们时,可以构成挑战,而无需替换元素本身就可以替换一个问题。 element? element.addeventlistener(event,function(){/在这里工作/},false); 要解决此问题,请考虑...
    编程 发布于2025-07-16
  • 如何有效地选择熊猫数据框中的列?
    如何有效地选择熊猫数据框中的列?
    在处理数据操作任务时,在Pandas DataFrames 中选择列时,选择特定列的必要条件是必要的。在Pandas中,选择列的各种选项。选项1:使用列名 如果已知列索引,请使用ILOC函数选择它们。请注意,python索引基于零。 df1 = df.iloc [:,0:2]#使用索引0和1 c...
    编程 发布于2025-07-16
  • 为什么不````''{margin:0; }`始终删除CSS中的最高边距?
    为什么不````''{margin:0; }`始终删除CSS中的最高边距?
    在CSS 问题:不正确的代码: 全球范围将所有余量重置为零,如提供的代码所建议的,可能会导致意外的副作用。解决特定的保证金问题是更建议的。 例如,在提供的示例中,将以下代码添加到CSS中,将解决余量问题: body H1 { 保证金顶:-40px; } 此方法更精确,避免了由全局保证金重置引...
    编程 发布于2025-07-16
  • PHP阵列键值异常:了解07和08的好奇情况
    PHP阵列键值异常:了解07和08的好奇情况
    PHP数组键值问题,使用07&08 在给定数月的数组中,键值07和08呈现令人困惑的行为时,就会出现一个不寻常的问题。运行print_r($月份)返回意外结果:键“ 07”丢失,而键“ 08”分配给了9月的值。此问题源于PHP对领先零的解释。当一个数字带有0(例如07或08)的前缀时,PHP将...
    编程 发布于2025-07-16

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

Copyright© 2022 湘ICP备2022001581号-3