Challenges facing the micro-service model

Nowadays, micro services are very popular, many enterprises have adopted this architecture mode to provide services. Adopting the microservices model offers many benefits, such as high scalability, shorter development cycles, easier deployment, a more open technology stack, and more. But it also brings up a lot of problems, such as the topic of today: Distributed Transaction Processing (DTP).

The system of our company adopts the microservice architecture, which separates a huge single system into multiple independent microservices. Since there are some system functions that require cross-services, the invocation between services is essential. The problem is that transaction management is relatively simple in a singleton system. In SpringBoot applications it can be done with a @Transactional annotation. In microservices, when the downstream services have problems, the upstream services may have committed transactions, so how to manage distributed transactions has become a stumbling block that microservices have to face.

response

There are several mature solutions in the industry, and the following three are briefly introduced:

  • 2PC/XA pattern: Two Phase Commit and 2PC-based XA specification
  • SAGA mode: Novel mode
  • TCC mode: Try, Confirm, or Cancel

2PC/XA

In a distributed system, although each node can know the success or failure of its own operation, it cannot know the success or failure of other nodes’ operation. 2 PC believes that when a transaction across multiple nodes, in order to keep the ACID characteristic of transaction, need to introduce a unity as coordinator component to control all the nodes (referred to as participants) operating results and eventually indicating whether the node should submit the operating results are real (for example, the updated data to disk, etc.). Therefore, the idea of two-stage submission can be summarized as follows: participants will inform the coordinator of the success or failure of the operation, and then the coordinator will decide whether to submit the operation or stop the operation according to the feedback information of all participants.

The XA specification is the OpenGroup specification for distributed transaction processing (DTP). The specification introduces a global transaction manager (TM) and a local resource manager (RM), similar to the coordinator and actor in 2PC. The XA specification mainly defines the interface between the two, which uses a two-phase commit to ensure that all resources commit or roll back a particular transaction at the same time. At present, mainstream databases support XA protocol, such as Oracle, MySQL and DB2. Distributed transaction processing can be easily implemented in Java using MysqlXAConnection.

SAGA

The SAGA pattern first appeared in a 1987 Princeton University paper to deal with the problem of Long Lived Transactions (LLTs) in computer systems. How long a transaction is LLT? According to the statement in the paper, long transactions are transactions calculated by hours and days.

The name Saga comes from the word itself, the novel. The core is to decompose a long transaction into a collection of many sub-transactions. When a long transaction fails, it does not roll back, but uses compensation actions. The compensation action semantically undoes the behavior of the transaction Ti, but does not necessarily return the database to the state in which Ti was executed. (For example, if a transaction triggers a missile launch, you may not be able to undo this action)

  • Each Saga consists of a series of sub-Transaction Ti
  • Each Ti has a corresponding compensation action Ci, which is used to undo the result caused by Ti

Saga defines two recovery strategies:

  • Backward recovery, which compensates all completed transactions if any of the sub-transactions fail. The order of execution isT1, T2, ... , Tj, Cj,... , C2, C1Where j is the sub-transaction in which the error occurred. The effect of this approach is to cancel all the previous successful sub-transation and make the execution result of the whole Saga be cancelled.
  • Forward recovery, which retries failed transactions, assuming that each subtransaction will eventually succeed. For scenarios that must succeed, the order of execution is something like this:T1, T2, ... , Tj(failed), Tj(retry),... , TnWhere j is the sub-transaction where the error occurred. No Ci is required in this case.

Saga hasThere are two implementations:

  • Orchestration- Based Saga Central coordination

An EC order created using central coordinated SAGA contains the following steps:

  1. The Order Service receives POST/Orders requests and creates the Create Order Saga Orchestrator
  2. The Saga Orchestrator creates an order that is in a PENDING state
  3. It then sends a Reserve Credit command to customer service
  4. Customer service tries to keep Credit
  5. It then sends back a reply indicating the result
  6. The Saga coordinator approves or rejects the order
  • Choreography- Based Saga Local Autonomy Type

An EC order created using local autonomy SAGA contains the following steps:

  1. OrderService receives the POST/Orders request and creates an order in the PENDING state
  2. It then issues an Order Created event
  3. The event handler for the customer service tries to preserve the Credit
  4. It then emits an event indicating the result
  5. The OrderService’s event handler approves or rejects the order

Saga does not provide ACID guarantees because atomicity and isolation cannot be satisfied. The original paper is described as follows:

full atomicity is not provided. That is, sagas may view the partial results of other sagas

TCC

TCC is an acronym for three words: Try, Confirm, or Cancel. The TCC mode requires a orchestrator to control a series of processes and to control the invocation of multiple resources (services). The service to be invoked must implement the Try/Confirm/Cancel apis.

Note that TCC mode can take twice as long. This is because the TCC mode communicates twice for each service and does not begin validation until a Try response from all services is received.

The TCC mode is characterized by the fact that the service passes through a pending state before entering the final state, and the cancellation process is easy. For example, an E-mail service sends a request to mark the E-mail as ready to send, acknowledging the request to send the E-mail. The corresponding cancellation request will only be flagged. In Saga mode, if an email is sent, the corresponding compensation action sends another email explaining the cancellation.

And a comparison of SAGA

The disadvantage of Saga compared with TCC is the lack of reserved actions, which makes the implementation of compensation actions more difficult: Ti is commit. For example, if a business is to send an email, in TCC mode, save the draft (Try) and then send (Confirm), and delete the draft (Cancel) directly. Saga directly sends an email (Ti), and if it wants to cancel, it has to send another email explaining the cancellation (Ci), which is difficult to realize.

If the email example above were replaced with: Service A immediately sends the Event to the ESB (Enterprise Service Bus, which can be considered as A messaging-oriented middleware) after completing Ti. The downstream service listens to the Event to do some work of its own and then sends the Event to the ESB. If service A performs the compensation action Ci, then the whole compensation action level is very deep.

But the absence of reservation can also be considered an advantage:

  • Some businesses are so simple that applying TCC requires modifying the original business logic, whereas Saga only needs to add a compensation action.
  • The minimum number of TCC communications is 2N, while Saga is n (n= number of sub-transactions).
  • Some third-party services don’t have a Try interface and the TCC pattern is tricky to implement, whereas Saga is simple.
  • No reservation means you don’t have to worry about resource release, and exception handling is easier (compare Saga’s recovery strategy to TCC’s exception handling).

SEATA scheme

Seata is an open source distributed transaction solution dedicated to providing high performance and easy to use distributed transaction services. Seata will provide users with AT, TCC, SAGA and XA transaction modes to create a one-stop distributed solution for users. Before the open source of Seata, the internal version of Seata has been playing the role of distributed consistency middleware in Ali economy, helping the economy to smoothly go through the Double 11 of each year and providing strong support for BU businesses. After years of precipitation and accumulation, commercial products have been sold on Ali Cloud and Financial cloud. In order to create a more complete technology ecosystem and inclusive technology achievements, Seata officially announced open source. In the future, Seata will help its technology in the form of community construction more reliable and complete.

AT mode

Based on SEATA’s user survey, I found that the AT model is still the most used in large enterprises. I guess the AT stands for AlibabaTransaction. AT is implemented based on 2PC mode and JDBC. Please refer to the official documentation for detailed implementation principles.

Use the premise

  • Based on a relational database that supports local ACID transactions.
  • Java applications, access the database through JDBC.

The whole mechanism

Evolution of the two-phase commit protocol:

  • Phase one: Business data and rollback log records are committed in the same local transaction, freeing local locks and connection resources.

  • Stage 2:

    • Commits are asynchronous and done very quickly.
    • Rollback is compensated in reverse by the rollback log of one phase.

Let’s look AT an example using the AT pattern.

The service call

Taking the classic EC order scenario as an example, after the Business service accepts the user request, it invokes the inventory service to deduct the inventory, then invokes the order service to create the order, and the order service invokes the account service to deduct the account balance. The four services in the example belong to the Seata Client (where the Business service is TM and the other services belong to RM). There is also a Seata Server(TC), whose interaction with the TC is entirely the responsibility of the Seata library, without the business developer being aware of it.

sequenceDiagram User->>BusinessService: GET /purchase/commit BusinessService->>StorageService: /deduct StorageService-->>BusinessService: success BusinessService->>OrderService: /debit OrderService->>AccountService: /? money=5 AccountService-->>OrderService: success OrderService-->>BusinessService: success BusinessService-->>User: true

If an exception occurs somewhere in the call chain, you need to roll back the steps that you have already performed.

sequenceDiagram User->>BusinessService: GET /purchase/rollback BusinessService->>StorageService: /deduct StorageService-->>BusinessService: success BusinessService->>OrderService: /debit OrderService->>AccountService: /? Money =5 note over AccountService: ❌ exception AccountService-->>OrderService: Failure OrderService-->>BusinessService: failure par note over AccountService: rollback note over OrderService: rollback note over StorageService: rollback end BusinessService-->>User: false

Code implementation

First let’s look at BusinessController, which implements two apis, one for normal commit with user ID 1001 and one for rolling back global transactions with user ID 1002.

/** ** purchase order, simulate global transaction commit **@return* /
@RequestMapping("/purchase/commit")
public Boolean purchaseCommit(HttpServletRequest request) {
    businessService.purchase("1001"."2001".1);
    return true;
}

/** * buy the order, simulate the global transaction rollback **@return* /
@RequestMapping("/purchase/rollback")
public Boolean purchaseRollback(a) {
    try {
        businessService.purchase("1002"."2001".1);
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }

    return true;
}
Copy the code

Next comes the implementation of BusinessService, isn’t it too easy? A single @GlobalTransactional implements distributed transaction processing! The RestTemplate is used in xxxClient for remote API access.

@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
    LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
    storageClient.deduct(commodityCode, orderCount);
    orderClient.create(userId, commodityCode, orderCount);
}
Copy the code

Then there is the implementation of StorageService, which simply operates on the database and deducts inventory. Note that the @GlobalTransactional annotation is not used in the Provider implementation.

public void deduct(String commodityCode, int count) {
    //select + for update
    Storage storage = storageMapper.findByCommodityCode(commodityCode);
    storage.setCount(storage.getCount() - count);
    storageMapper.updateById(storage);
}
Copy the code

Next comes the implementation of The OrderService, which invokes the account service after saving the created order to the database.

public void create(String userId, String commodityCode, Integer count) {
    BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
    Order order = new Order();
    order.setUserId(userId);
    order.setCommodityCode(commodityCode);
    order.setCount(count);
    order.setMoney(orderMoney);

    orderMapper.insert(order);

    accountClient.debit(userId, orderMoney);

}
Copy the code

Finally, there is the implementation of AccountService, which deducts the account balance, determines the user ID, and throws an exception if ERROR_USER_ID (1002) to simulate a rollback scenario.

public void debit(String userId, BigDecimal num) {
    Account account = accountMapper.selectByUserId(userId);
    account.setMoney(account.getMoney().subtract(num));
    accountMapper.updateById(account);

    if (ERROR_USER_ID.equals(userId)) {
        throw new RuntimeException("account branch exception"); }}Copy the code

Note that in the Common module, a RestTemplate interceptor and a filter are implemented. Interceptors are used to add the transaction ID to the request Header when invoking across services. The interceptor, on receiving the request, takes the transaction ID from the Header and binds it locally.

public class SeataRestTemplateInterceptor implements ClientHttpRequestInterceptor {

    public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
        HttpRequestWrapper requestWrapper = new HttpRequestWrapper(httpRequest);
        String xid = RootContext.getXID();
        if (StringUtils.isNotEmpty(xid)) {
            requestWrapper.getHeaders().add(RootContext.KEY_XID, xid);
        }

        returnclientHttpRequestExecution.execute(requestWrapper, bytes); }}Copy the code
@Component
public class SeataFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        String xid = req.getHeader(RootContext.KEY_XID.toLowerCase());
        boolean isBind = false;
        if (StringUtils.isNotBlank(xid)) {
            RootContext.bind(xid);
            isBind = true;
        }
        try {
            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            if(isBind) { RootContext.unbind(); }}}}Copy the code

The database is defined in local-seata-env/initdb.d/all_in_one.sql. The instance code has been uploaded to Github.

What are the considerations for using AT mode?

  1. Proxy data sources must be used, and there are three forms to proxy data sources:
  • When relying on seta-spring-boot-starter, the data source is automatically proxy without additional processing.
  • Rely on seata -all, use the @ EnableAutoDataSourceProxy (since 1.1.0) annotations, annotation parameters can choose the JDK or additional agents.
  • You can also use DatasourceProxy to wrap a DataSource manually if you rely on Seata-all.
  1. GlobalTransactionScanner needs to be configured manually when seata-all is used, but no additional operations are required when Seata-spring-boot-starter is used.
  2. A service table must contain a single column of primary keys. If there are compound primary keys, only mysql is supported for now. You are advised to create a column of primary keys with an increased ID for other types of databases and change the original compound primary key to a unique key.
  3. Each service library must contain the undo_log table. If it is used with the database and table component, the database and table are not separated.
  4. Transactions across microservice links require support for the corresponding RPC framework, which is currently supported in SEata-ALL: Apache Dubbo, Alibaba Dubbo, SOFA -RPC, Motan, gRpc, httpClient, for Spring Cloud support, please refer to spring-Cloud-Alibaba-seata. Other self-developed frameworks, asynchronous models and message consumption transaction models should be supported by API.
  5. AT supports MySQL, Oracle, PostgreSQL, and TiDB.
  6. When a distributed transaction is enabled with annotations, if the default service provider joins the consumer transaction, the provider does not annotate the transaction. However, providers also require corresponding dependencies and configurations, and only annotations can be omitted.
  7. When a distributed transaction is started using annotations, if the transaction is required to be rolled back, an exception must be thrown to the originator of the transaction, which is aware of it by the @GlobalTransactional annotation. Provide directly throw an exception or define an error code for the consumer to judge and then throw an exception.

What do I need to notice when using the AT pattern with the Spring @Transactional annotation?

A laparoscope, usually connected to a @ Transactional with DataSourceTransactionManager and JTATransactionManager respectively local transactions and XA distributed transactions, we commonly used is combined with local affairs. When combined with local transactions, @Transactional and @GlobalTransaction are used together, @Transactional can only be at the same method level as the annotation in @GlobalTransaction or within the annotation method in @GlobalTransaction. The concept of a distributed transaction is more important than that of a local transaction. The @Transactional annotation on the outer layer will result in a distributed transaction being committed empty. When a connection corresponding to @Transactional is committed, the global transaction will be reported as being committed or its XID does not exist.

Other Solutions

Before adopting SEATA, our company also investigated SAGA solutions based on Uber Cadence and Kafka. Cadence is a workflow platform that promises to let you focus on business logic, leaving the complexities of distributed systems to Cadence. Cadence is powerful, but the cost of learning and customization is relatively high, unlike SEATA, which is out of the box. Those of you who are interested can take a look.

In addition, SAGA mode of SEATA is also a good choice, and I will have the opportunity to share it with students.