@[TOC] Seata provides four modes of processing distributed transactions:

  • AT
  • TCC
  • XA
  • Saga

We have introduced the first three kinds of Songko, today we will look at the Saga model. If you’re not familiar with the first three, take a look at the previous article, Portal:

  • Experience distributed transactions in five minutes! So easy!
  • Read so many blogs, still do not understand TCC, might as well take a look at this case!
  • XA business water is very deep, young man I am afraid you can not grasp!

Okay, let’s get started with the body of the day.

1. What is Saga transaction mode

Saga mode is a long transaction solution provided by Seata. In Saga mode, each participant in the business process submits a local transaction, and when a participant fails, the previous successful participant is compensated. One-stage forward service and two-stage compensation service are implemented by business development.

It is important to note that the Saga mode rollback, like AT and TCC rollback, is a reverse compensation operation (as opposed to XA mode).

The following flow chart is provided by the authorities. Let’s take a look:

It can be seen that T1, T2, T3 and Tn respectively represent branch transactions in distributed transactions, and this line is the normal state of the transaction. If an exception is thrown during the execution, the transaction will be rolled back at C3, C2 and C1, where rollback is actually reverse compensation operation.

Generally speaking, Saga mode is suitable for distributed transactions with long and multiple business processes, just like the flow chart above. However, when the business process is long, how to define the state of each transaction becomes a problem.

This involves the state machine of Saga distributed transactions.

2. Saga status chart

If you’ve used the Activiti process engine, you probably know what a state diagram is, and Saga’s state diagram is similar to that.

The state diagram in Saga looks like this:

  1. First we need to define a state flow diagram, like the following:

This flowchart is officially provided with a drawing tool, the address is as follows:

https://seata.io/saga_designer/index.html
Copy the code

The video is probably recorded by the person who recorded it for the first time. He has no experience and can’t watch the video with various problems, so I won’t put the link. If you need to draw a state map at work, you can refer to this document:

https://help.aliyun.com/document_detail/172550.html
Copy the code

The flowchart records the state of each branch transaction and the associated compensation operations. After the process is drawn, the JSON state language definition file is automatically generated and copied into the project in the future.

  1. Each node in the state diagram can invoke a service, and each node can configure its compensation node. When the node is abnormal, the state engine reverse-executes the corresponding compensation node of the successful node to roll back the transaction (rollback or not can be decided by the user).
  2. State graph can realize service choreography requirements, support single choice, concurrency, sub-process, parameter conversion, parameter mapping, service execution state judgment, exception capture and other functions.

3. Saga mode cases

Let’s take a look at an example of Saga mode, after which you will understand what Saga mode is.

3.1 Preparations

Let’s use the official case. However, as Songko said before, the official case is easy to import failure, and there are some problems in it, so friends can directly reply to seata-demo download this case in the background of the public account.

For Saga we use this:

If you use the official case directly, you need to make the following modifications:

  1. Modified Dubbo version to 2.7.3. The original 3.0.1 version had runtime problems.
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring</artifactId>
        </exclusion>
    </exclusions>
    <version>2.7.3</version>
</dependency>
Copy the code
  1. The official default database script is missing a field, I don’t know why, so obvious BUG. This requires us to be theresrc/main/resources/sql/h2_init.sqlAdd one to the seatA_state_inst tablegmt_updated timestamp(3) not nullField.

The preparation is complete.

3.2 Test Run

Now let’s test run.

First of all, let’s perform the SRC/main/Java/IO/seata/samples/saga/starter/DubboSagaProviderStarter. The main method in Java, start the server.

Then open the SRC/main/Java/IO/seata/samples/saga/starter/DubboSagaTransactionStarter. Java classes, this class needs to be modified it to run.

public static void main(String[] args) {
    AbstractApplicationContext applicationContext = new ClassPathXmlApplicationContext(new String[] {"spring/seata-saga.xml"."spring/seata-dubbo-reference.xml"});
    StateMachineEngine stateMachineEngine = (StateMachineEngine) applicationContext.getBean("stateMachineEngine");
    transactionCommittedDemo(stateMachineEngine);
    transactionCompensatedDemo(stateMachineEngine);
    new ApplicationKeeper(applicationContext).keep();
}
Copy the code

As you can see, there are two test methods in the main method:

  • transactionCommittedDemo
  • transactionCompensatedDemo

The first method is a two-phase commit test, and the second method is a two-phase compensation test. We comment out one of them and only execute one of them each time we execute it.

In addition, the transactionCommittedDemo method provides two methods of state retrieval: synchronous and asynchronous. We need to comment out one of them and test it, as follows:

private static void transactionCommittedDemo(StateMachineEngine stateMachineEngine) {
    Map<String, Object> startParams = new HashMap<>(3);
    String businessKey = String.valueOf(System.currentTimeMillis());
    startParams.put("businessKey", businessKey);
    startParams.put("count".10);
    startParams.put("amount".new BigDecimal("100"));
    //sync test
    //StateMachineInstance inst = stateMachineEngine.startWithBusinessKey("reduceInventoryAndBalance", null, businessKey, startParams);
    //Assert.isTrue(ExecutionStatus.SU.equals(inst.getStatus()), "saga transaction execute failed. XID: " + inst.getId());
    //System.out.println("saga transaction commit succeed. XID: " + inst.getId());
    //async test
    businessKey = String.valueOf(System.currentTimeMillis());
    StateMachineInstance inst = stateMachineEngine.startWithBusinessKeyAsync("reduceInventoryAndBalance".null, businessKey, startParams, CALL_BACK);
    waittingForFinish(inst);
    Assert.isTrue(ExecutionStatus.SU.equals(inst.getStatus()), "saga transaction execute failed. XID: " + inst.getId());
    System.out.println("saga transaction commit succeed. XID: " + inst.getId());
}
Copy the code

Comment out synchronous code blocks or asynchronous code blocks. After commenting out, execute the main method to test.

If you test the transactionCommittedDemo method, the console prints the following log:

Saga transaction commit succeed. XID: 192.168.1.105:8091-2612256553007833092Copy the code

If the test transactionCompensatedDemo method, the console print log is as follows:

Saga transaction compensate succeed. XID: 192.168.1.105:8091-2612256553007833094Copy the code

If you can see the above two logs, the case is running fine.

Next, let’s analyze what this case tells us!

3.3 Case Analysis

3.3.1 Analyzing the JSON Status Description

This case does not have a clear business, it is simply a case.

We first define two actions:

  • InventoryAction
  • BalanceAction

There are two methods defined in each Action:

  • reduce
  • compensateReduce

As you can see from the method name, the Reduce method is the normal execution logic, and the compensateReduce method is the code compensation logic, that is, the code that needs to be executed during the rollback.

In terms of the implementation of these two methods, there is nothing, they are both printed logs, so in this project we just need to look at the printed logs to know whether the transaction was committed or rolled back.

In the SRC/main/resources/statelang/reduce_inventory_and_balance json file defines the state of individual transactions, we can probably see, because complete json file is long, I will block it.

{
    "Name": "reduceInventoryAndBalance"."Comment": "reduce inventory then reduce balance in a transaction"."StartState": "ReduceInventory"."Version": "0.0.1". .Copy the code

This section defines the name of the state machine for reduceInventoryAndBalance, in a project, we can at the same time there are multiple such a JSON file, each has a name attribute, This allows the Java code to specify which process to call with a specific name. StartState defines the whole process to start with the ReduceInventory, which is the node defined later.

"ReduceInventory": {
    "Type": "ServiceTask"."ServiceName": "inventoryAction"."ServiceMethod": "reduce"."CompensateState": "CompensateReduceInventory"."Next": "ChoiceState"."Input": [
        "$.[businessKey]"."$.[count]"]."Output": {
        "reduceInventoryResult": "$.#root"
    },
    "Status": {
        "#root == true": "SU"."#root == false": "FA"."$Exception{java.lang.Throwable}": "UN"}}Copy the code

This is the first step in the flow chart, so LET me just pick out a few key points.

  • ServiceName: This is the name of the service, which object will handle the request here, inventoryAction is the object we got from Dubbo.
  • ServiceMethod: This specifies the method to execute, which is the normal method to execute.
  • CompensateState: Specifies the node responsible for the compensation service. The value of this node is the other one defined in the JSON file.
  • Next: This is the Next node to go to after the current node has finished.
  • Input/Output/Status: indicates Input parameters, Output parameters, and Status values respectively.

After executing the above node, it enters the following node:

"ChoiceState": {"Type": "Choice"."Choices":[
        {
            "Expression":"[reduceInventoryResult] == true"."Next":"ReduceBalance"}]."Default":"Fail"
},
Copy the code

If the return value of the previous node is not true, the execution fails and compensation is required. If the result of the previous node is true, enter the ReduceBalance of the next node.

The definition of the back node is also similar, I will not list it, small friends public number background reply seata-demo download article case, you can view.

This is the state diagram.

3.3.2 Code analysis

Let’s take a quick look at the code.

Two test methods are provided, one for phase 2 commit and one for phase 2 rollback.

Let’s start with this:

private static void transactionCommittedDemo(StateMachineEngine stateMachineEngine) {
    Map<String, Object> startParams = new HashMap<>(3);
    String businessKey = String.valueOf(System.currentTimeMillis());
    startParams.put("businessKey", businessKey);
    startParams.put("count".10);
    startParams.put("amount".new BigDecimal("100"));
    businessKey = String.valueOf(System.currentTimeMillis());
    StateMachineInstance inst = stateMachineEngine.startWithBusinessKeyAsync("reduceInventoryAndBalance".null, businessKey, startParams, CALL_BACK);
    waittingForFinish(inst);
    Assert.isTrue(ExecutionStatus.SU.equals(inst.getStatus()), "saga transaction execute failed. XID: " + inst.getId());
    System.out.println("saga transaction commit succeed. XID: " + inst.getId());
}
Copy the code

There are synchronous and asynchronous cases in this method, so I’m going to delete the synchronous lines and look at the asynchronous ones.

StartParams is the parameter of the project. In the JSON analysis above, each method (reduce, compensateReduce) has parameters, which are here.

Then called startWithBusinessKeyAsync method of state machine start execution of each process, the method of the first parameter is the name of the process, which is ahead of us the name of the JSON, through this name can determine which one is the execution process, StartParams is also passed in here.

WaittingForFinish is a custom blocking method that allows the process to complete in order to obtain the result of a transaction. This is the basic thread knowledge, which I will not stress here, you can download the source code to view.

Finally, the execution status of the transaction is determined by assertions (inst.getStatus()) and the relevant log is printed.

Let’s look at the method of two-phase rollback:

private static void transactionCompensatedDemo(StateMachineEngine stateMachineEngine) {
    Map<String, Object> startParams = new HashMap<>(4);
    String businessKey = String.valueOf(System.currentTimeMillis());
    startParams.put("businessKey", businessKey);
    startParams.put("count".10);
    startParams.put("amount".new BigDecimal("100"));
    startParams.put("mockReduceBalanceFail"."true");
    //sync test
    StateMachineInstance inst = stateMachineEngine.startWithBusinessKey("reduceInventoryAndBalance".null, businessKey, startParams);
    Assert.isTrue(ExecutionStatus.SU.equals(inst.getCompensationStatus()), "saga transaction compensate failed. XID: " + inst.getId());
    System.out.println("saga transaction compensate succeed. XID: " + inst.getId());
}
Copy the code

The mockReduceBalanceFail parameter is used to define the input parameter in the JSON file. See JSON below:

"ReduceBalance": {
    "Type": "ServiceTask"."ServiceName": "balanceAction"."ServiceMethod": "reduce"."CompensateState": "CompensateReduceBalance"."Input": [
        "$.[businessKey]"."$.[amount]",
        {
            "throwException" : "$.[mockReduceBalanceFail]"}]."Output": {
        "compensateReduceBalanceResult": "$.#root"
    },
Copy the code

You can see the mockReduceBalanceFail in this input parameter, but instead of taking it directly as an input parameter, it turns it into a Map whose key is throwException, So in the BalanceActionImpl# Reduce method there is the following code:

public boolean reduce(String businessKey, BigDecimal amount, Map<String, Object> params) {
    if(params ! =null && "true".equals(params.get("throwException"))) {throw new RuntimeException("reduce balance failed");
    }
    LOGGER.info("reduce balance succeed, amount: " + amount + ", businessKey:" + businessKey);
    return true;
}
Copy the code

If throwException is true, an exception is thrown, which triggers the rollback of the transaction.

Back to second stage rollback method, finally through inst. GetCompensationStatus () method to transaction compensation operation state, if this method returns true, said transaction compensation operation implementation success.

There are some Dubbo points in this case that I won’t go over here, but that’s not the point of this article.

Well, after the above analysis, you should have a general idea of how the Saga works.

4. Design experience

4.1 Empty compensation is allowed

Null compensation is the original service is not executed, the result of the compensation service is executed, when the original service timeout, packet loss and other situations, or before receiving the original service request received compensation request, may appear null compensation.

Therefore, we need to allow null compensation in service design, that is, if the business primary key to be compensated is not found, compensation success is returned and the original business primary key is recorded. This is also the reason why both the original service and compensation service have businessKey parameter in the case.

4.2 Anti-suspension control

Suspension is the compensation service compared to the original service to perform first, the reason and the front say similar things, so we need to perform the original service, need to check the current business record have a primary key in the empty compensation, if has already been recorded, explain compensation has been carried out first, at this time we can stop the execution of the original service.

4.3 Idempotent control

Both the original service and compensation service need to ensure idempotency. Since the network may time out, we may set a retry policy. When retries occur, idempotency control should be adopted to avoid repeated updates of business data. How to ensure idempotence, Songko before the public article and we talked, here will not repeat.

4.4 Lack of isolation response

Because Saga transactions do not guarantee isolation, in extreme cases the rollback operation may not be completed due to dirty writes.

An extreme example is A distributed transaction in which user A is charged and the balance is deducted from user B. If user A is charged successfully and the balance is consumed by user A before the transaction is committed, there is no way to compensate if the transaction is rolled back. This is a classic problem caused by lack of isolation.

To solve this problem, we can try to solve it in the following ways:

The design of business process follows the principle of “rather long money, not short money”. Long money means that if the customer loses money and the organization gains more money, the customer can be refunded by the reputation of the organization. Otherwise, it is short money, and the less money may not be recovered. So the business process design must be deducted first.

Some business scenarios can be allowed to business success finally, in the case of a rollback the can continue to try again to complete the back of the process, so the state machine engine also need to provide in addition to providing “rollback” ability “forward” to restore the context’s ability to continue, let business execution success finally, achieve the goal of eventual consistency.

4.5 Performance Optimization

Configuration parameters of the client client. Rm. Report. Success. Enable = false, can be in when the branch transaction execution success state branch not reported to the server, thereby improving performance.

When the status of the previous branch transaction has not been reported, the next branch transaction is registered and the previous branch transaction is considered to have actually succeeded

5. Summary

This is the Saga pattern in Seata distributed transactions. At this point, Songo is finished with the four distributed transaction patterns in Seata, which I will compare throughout this article.

Three more portals:

  • Experience distributed transactions in five minutes! So easy!
  • Read so many blogs, still do not understand TCC, might as well take a look at this case!
  • XA business water is very deep, young man I am afraid you can not grasp!

Public number background reply seata-demo can download this case.

References:

  • www.sofastack.tech/blog/sofa-c…
  • Seata. IO/useful – cn/docs /…