Spring Boot integrates with Seata to solve distributed transaction problems

Posted by levi_501_dehaan on Thu, 16 Jan 2020 06:42:38 +0100

seata introduction

Seata is Alibaba's open-source distributed transaction solution in 2019, which is committed to providing high-performance and easy-to-use distributed transaction services under the microservice architecture. Before the open source of Seata, the corresponding internal version of Seata played a role of distributed consistency Middleware in Ali, helping Ali to survive the double 11 of the past years and providing strong support for various businesses. After years of precipitation and accumulation, 2019.1 Seata officially announced open source. Currently, Seata 1.0 has GA.

Distributed transaction in microservices

Let's imagine a traditional monolithic application whose business consists of three modules that use a single local data source. Naturally, local transactions will ensure data consistency.

The microservice architecture has changed. The three modules mentioned above are designed as three services. Local transactions naturally guarantee data consistency in each service. But what about the whole business logic?

What about Seata?

We say that a distributed transaction is a global transaction composed of a batch of branch transactions, which are usually only local transactions.

Seata has three basic components:

  • Transaction Coordinator (TC): maintains the state of global and branch transactions, driving global commit or rollback.
  • Transaction manager TM: define the scope of global transaction: start global transaction, commit or roll back global transaction.
  • Resource Manager (RM): manage the resources of the branch transaction being processed, talk with TC to register the branch transaction and report the status of the branch transaction, and drive the commit or rollback of the branch transaction.

Typical lifecycle of distributed transactions managed by Seata:

  1. TM requires TC to start a new global transaction. TC generates xids that represent global transactions.
  2. XID is propagated through the call chain of microservices.
  3. RM registers the local transaction as a branch of the corresponding global transaction from XID to TC.
  4. TM requires TC to commit or fallback the corresponding XID global transaction.
  5. TC drives all branch transactions under the corresponding global transaction of XID to complete branch commit or rollback.

Quick start

Use case

Business logic for users to purchase goods. The whole business logic is supported by three microservices:

  • Warehousing service: deduct the warehousing quantity from the given goods.
  • Order service: create an order according to the purchase demand.
  • Account service: deduct balance from user account.

Environmental preparation

Step 1: establish database

# db_seata
DROP SCHEMA IF EXISTS db_seata;
CREATE SCHEMA db_seata;
USE db_seata;

# Account
CREATE TABLE `account_tbl` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `user_id` VARCHAR(255) DEFAULT NULL,
  `money` INT(11) DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

INSERT INTO account_tbl (id, user_id, money)
VALUES (1, '1001', 10000);
INSERT INTO account_tbl (id, user_id, money)
VALUES (2, '1002', 10000);

# Order
CREATE TABLE `order_tbl`
(
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `user_id` VARCHAR(255) DEFAULT NULL,
  `commodity_code` VARCHAR(255) DEFAULT NULL,
  `count` INT(11) DEFAULT '0',
  `money` INT(11) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

# Storage
CREATE TABLE `storage_tbl` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` VARCHAR(255) DEFAULT NULL,
  `count` INT(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;


INSERT INTO storage_tbl (id, commodity_code, count)
VALUES (1, '2001', 1000);

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

In the sea at mode, the undo log table is required, and the other three are business tables.

Step 2: start the Seata Server

The Server-side storage mode (store.mode) has two types: file and db (raft will be introduced later). The file mode does not need to be changed and can be started directly. The db mode needs to import three tables for storing global transaction call back information.

*Note: the file mode is stand-alone mode, and the global transaction session information is read and written in memory and the local file root.data is persisted, with high performance;
db mode is high availability mode, and global transaction session information is shared through db, so the corresponding performance is poor*

You can start Seata Server directly through bash script or through Docker image. However, Docker only supports file mode at present, and does not support registering Seata Server in Eureka or Nacos registration center.

Start by script

stay https://github.com/seata/seata/releases Download the corresponding version of Seata Server, decompress it and execute the following command to start. Here, use file configuration

Start with Docker
docker run --name seata-server -p 8091:8091 seataio/seata-server:latest

Project introduction

Project name address Explain
sbm-account-service 127.0.0.1:8081 Account service
sbm-order-service 127.0.0.1:8082 Order service
sbm-storage-service 127.0.0.1:8083 Warehousing service
sbm-business-service 127.0.0.1:8084 Main business
seata-server 172.16.2.101:8091 seata-server

Core code

In order not to make the space too long, only part of the code is given here, and the source address will be given at the end of the detailed code

maven introduces sea TA's dependency eata spring boot starter

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

Warehousing service

application.properties
spring.application.name=account-service
server.port=8081
spring.datasource.url=jdbc:mysql://172.16.2.101:3306/db_seata?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
seata.tx-service-group=my_test_tx_group
mybatis.mapper-locations=classpath*:mapper/*Mapper.xml
seata.service.grouplist=172.16.2.101:8091
logging.level.io.seata=info
logging.level.io.seata.samples.account.persistence.AccountMapper=debug
StorageService
public interface StorageService {

    /**
     * Deduct storage quantity
     */
    void deduct(String commodityCode, int count);
}

Order service

public interface OrderService {

    /**
     * Create order
     */
    Order create(String userId, String commodityCode, int orderCount);
}

Account service

public interface AccountService {

    /**
     * Loan from user account
     */
    void debit(String userId, int money);
}

Main business logic

Only one @ GlobalTransactional annotation is needed on the business method.

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

XID transfer

Cross service delivery of global transaction ID needs to be implemented by ourselves. Here we use interceptors. Each service needs to add the following two classes.

SeataFilter
@Component
public class SeataFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @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();
            }
        }
    }

    @Override
    public void destroy() {
    }
}
SeataRestTemplateAutoConfiguration
@Configuration
public class SeataRestTemplateAutoConfiguration {
    @Autowired(
            required = false
    )
    private Collection<RestTemplate> restTemplates;
    @Autowired
    private SeataRestTemplateInterceptor seataRestTemplateInterceptor;

    public SeataRestTemplateAutoConfiguration() {
    }

    @Bean
    public SeataRestTemplateInterceptor seataRestTemplateInterceptor() {
        return new SeataRestTemplateInterceptor();
    }

    @PostConstruct
    public void init() {
        if (this.restTemplates != null) {
            Iterator var1 = this.restTemplates.iterator();

            while (var1.hasNext()) {
                RestTemplate restTemplate = (RestTemplate) var1.next();
                List<ClientHttpRequestInterceptor> interceptors = new ArrayList(restTemplate.getInterceptors());
                interceptors.add(this.seataRestTemplateInterceptor);
                restTemplate.setInterceptors(interceptors);
            }
        }

    }
}

test

Test success scenario:

curl -X POST http://127.0.0.1:8084/api/business/purchase/commit

The returned result is: true

Test failure scenario:

When a user with UserId 1002 places an order, SBM account service will throw an exception and the transaction will be rolled back

http://127.0.0.1:8084/api/business/purchase/rollback

The returned result is: false

View the log or primary key of undo log, and you can see that there are saved data during execution.
For example, if you view the value of the auto increment of the primary key, the value will change before and after execution, which is 1 before execution and 7 after execution.

Source address

https://github.com/gf-huanchupk/SpringBootLearning/tree/master/springboot-seata

Reference resources

http://seata.io/zh-cn/docs/overview/what-is-seata.html

Topics: Java Spring Docker Session github