Spring Cloud Alibaba: demonstration of local and distributed transactions & used by Seata

The concepts related to transactions will not be introduced. This blog first demonstrates local transactions and distributed transactions, and then introduces the basic use of Seata. Later, it will introduce Seata's cluster, registry, configuration center and various transaction modes.

Construction works

A parent module and a child module. The parent module is used to manage dependent versions, and the child module is used to demonstrate local transactions and distributed transactions (demonstrated through a simple version of a user's purchase case). Finally, it is necessary to integrate the Seata distributed transaction solution.

POM of parent module xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.kaven</groupId>
    <artifactId>alibaba</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <description>Spring Cloud Alibaba</description>
    <modules>
        <module>seata</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <spring-cloud-version>Hoxton.SR9</spring-cloud-version>
        <spring-cloud-alibaba-version>2.2.6.RELEASE</spring-cloud-alibaba-version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
    </parent>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

seata module

seata module demonstrates local transactions and distributed transactions through a simple product purchase case. The structure of seata module is shown in the following figure:

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>com.kaven</groupId>
        <artifactId>alibaba</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>seata</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

application.yml:

server:
  port: 9000

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ITkaven@666
    url: jdbc:mysql://localhost:3306/seata?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
  jpa:
    show-sql: true

Entity

Order entity class:

package com.kaven.alibaba.entity;

import lombok.Data;

import javax.persistence.*;

@Entity
@Table(name = "`order`") // You cannot change 'order' to order (Mysql database keyword), otherwise an error will occur when interacting with the database
@Data
public class Order {
    // Order id
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Integer id;

    // User id 
    public Integer userId;
    
    // Commodity id
    public Integer productId;

    // Purchase quantity of goods
    public Integer count;

    // Order amount
    public Integer money;
}

Storage entity class:

package com.kaven.alibaba.entity;

import lombok.Data;

import javax.persistence.*;

@Entity
@Table(name = "storage")
@Data
public class Storage {
    
    // Commodity id
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Integer id;

    // stock
    public Integer count;
}

User entity class:

package com.kaven.alibaba.entity;

import lombok.Data;

import javax.persistence.*;

@Entity
@Table(name = "user")
@Data
public class User {

    // User id 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Integer id;

    // User balance
    public Integer money;
}

Repository

OrderRepository:

package com.kaven.alibaba.repository;

import com.kaven.alibaba.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderRepository extends JpaRepository<Order, Integer> {}

StorageRepository:

package com.kaven.alibaba.repository;

import com.kaven.alibaba.entity.Storage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface StorageRepository extends JpaRepository<Storage, Integer> {}

UserRepository:

package com.kaven.alibaba.repository;

import com.kaven.alibaba.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {}

Service

IOrderService:

package com.kaven.alibaba.service;

public interface IOrderService {
    void create(int userId, int productId, int count, int money);
}

OrderServiceImpl:

package com.kaven.alibaba.service.impl;

import com.kaven.alibaba.entity.Order;
import com.kaven.alibaba.repository.OrderRepository;
import com.kaven.alibaba.service.IOrderService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class OrderServiceImpl implements IOrderService {

    @Resource
    private OrderRepository orderRepository;

    @Override
    public void create(int userId, int productId, int count, int money) {
        // Generate order
        Order order = new Order();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setCount(count);
        order.setMoney(money);
        orderRepository.save(order);
    }
}

IStorageService:

package com.kaven.alibaba.service;

public interface IStorageService {
    void deduct(int productId, int count);
}

StorageServiceImpl:

package com.kaven.alibaba.service.impl;

import com.kaven.alibaba.entity.Storage;
import com.kaven.alibaba.repository.StorageRepository;
import com.kaven.alibaba.service.IStorageService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Optional;

@Service
public class StorageServiceImpl implements IStorageService {

    @Resource
    private StorageRepository storageRepository;

    @Override
    public void deduct(int productId, int count) {
        Optional<Storage> byId = storageRepository.findById(productId);
        if(byId.isPresent()) {
            Storage storage = byId.get();
            if(storage.getCount() >= count) {
                // Inventory reduction
                storage.setCount(storage.getCount() - count);
                storageRepository.save(storage);
            }
            else {
                throw new RuntimeException("This item is out of stock!");
            }
        }
        else {
            throw new RuntimeException("The product does not exist!");
        }
    }
}

IUserService:

package com.kaven.alibaba.service;

public interface IUserService {
    void debit(int userId, int money);
}

UserServiceImpl:

package com.kaven.alibaba.service.impl;

import com.kaven.alibaba.entity.User;
import com.kaven.alibaba.repository.UserRepository;
import com.kaven.alibaba.service.IUserService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Optional;

@Service
public class UserServiceImpl implements IUserService {

    @Resource
    private UserRepository userRepository;

    @Override
    public void debit(int userId, int money) {
        Optional<User> byId = userRepository.findById(userId);
        if(byId.isPresent()) {
            User user = byId.get();
            if(user.getMoney() >= money) {
                // Less balance
                user.setMoney(user.getMoney() - money);
                userRepository.save(user);
            }
            else {
                throw new RuntimeException("The user's balance is insufficient!");
            }
        }
        else {
            throw new RuntimeException("There is no such user!");
        }
    }
}

IBuyService:

package com.kaven.alibaba.service;

public interface IBuyService {
    void buy(int userId, int productId, int count);
}
package com.kaven.alibaba.service.impl;

import com.kaven.alibaba.service.IBuyService;
import com.kaven.alibaba.service.IOrderService;
import com.kaven.alibaba.service.IStorageService;
import com.kaven.alibaba.service.IUserService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class BuyServiceImpl implements IBuyService {

    @Resource
    private IOrderService orderService;

    @Resource
    private IStorageService storageService;

    @Resource
    private IUserService userService;

    @Override
    public void buy(int userId, int productId, int count) {
        int money = count;
        // Generate order
        orderService.create(userId, productId, count, money);
        // Inventory reduction
        storageService.deduct(productId, count);
        // Less balance
        userService.debit(userId, money);
    }
}

Controller

BuyController:

package com.kaven.alibaba.controller;

import com.kaven.alibaba.service.IBuyService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class BuyController {

    @Resource
    private IBuyService buyService;

    @PostMapping("/buy")
    public String buy(@RequestParam("userId") Integer userId,
                      @RequestParam("productId") Integer productId,
                      @RequestParam("count") Integer count) {
        buyService.buy(userId, productId, count);
        return "success";
    }
}

Startup class:

package com.kaven.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

database

Enter the database and execute the following sql (for simplicity, no relevant index is created).

CREATE DATABASE seata;

USE seata;

DROP TABLE IF EXISTS `storage`;
CREATE TABLE `storage` (
                           `id` int(11) NOT NULL AUTO_INCREMENT,
                           `count` int(11) DEFAULT 0,
                           PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


DROP TABLE IF EXISTS `order`;
CREATE TABLE `order` (
                         `id` int(11) NOT NULL AUTO_INCREMENT,
                         `user_id` int(11) DEFAULT NULL,
                         `product_id` int(11) DEFAULT NULL,
                         `count` int(11) DEFAULT 0,
                         `money` int(11) DEFAULT 0,
                         PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
                        `id` int(11) NOT NULL AUTO_INCREMENT,
                        `money` int(11) DEFAULT 0,
                        PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Insert several pieces of data, as shown in the following figure:

Start the application.

Local transaction

Send a request to the application using Postman.

Obviously, the user's amount is not enough.

Inventories of goods have also been deducted.

The order has been created.

The operation of reducing user balance has not been performed, so the user balance remains unchanged, which leads to the problem of inconsistent data.


This kind of transaction under only one service is easy to solve, and Spring's @ Transactional annotation can be used.

package com.kaven.alibaba.service.impl;

import com.kaven.alibaba.service.IBuyService;
import com.kaven.alibaba.service.IOrderService;
import com.kaven.alibaba.service.IStorageService;
import com.kaven.alibaba.service.IUserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
public class BuyServiceImpl implements IBuyService {

    @Resource
    private IOrderService orderService;

    @Resource
    private IStorageService storageService;

    @Resource
    private IUserService userService;

    @Override
    @Transactional
    public void buy(int userId, int productId, int count) {
        int money = count;
        // Generate order
        orderService.create(userId, productId, count, money);
        // Inventory reduction
        storageService.deduct(productId, count);
        // Less balance
        userService.debit(userId, money);
    }
}

Restart the application and manually roll back the data in the database. Then use Postman to send a request to the application.

The exception chain information is different from before. In fact, it is the function of @ Transactional annotation. The principle of this annotation will be introduced by bloggers in the Spring source code reading series in the future.


At this time, the data is consistent.


Distributed transaction

Now turn each operation into an interface to simulate a distributed environment.

Controller

OrderController:

package com.kaven.alibaba.controller;

import com.kaven.alibaba.service.IOrderService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Resource
    private IOrderService orderService;

    @PostMapping("/create")
    public void create(@RequestParam("userId") Integer userId,
                       @RequestParam("productId") Integer productId,
                       @RequestParam("count") Integer count,
                       @RequestParam("money") Integer money) {
        orderService.create(userId, productId, count, money);
    }
}

StorageController:

package com.kaven.alibaba.controller;

import com.kaven.alibaba.service.IStorageService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping("/storage")
public class StorageController {

    @Resource
    private IStorageService storageService;

    @PostMapping("/deduct")
    public void deduct(@RequestParam("productId") Integer productId,
                       @RequestParam("count") Integer count) {
        storageService.deduct(productId, count);
    }
}

UserController:

package com.kaven.alibaba.controller;

import com.kaven.alibaba.service.IUserService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private IUserService userService;

    @PostMapping("/debit")
    public void debit(@RequestParam("userId") Integer userId,
                       @RequestParam("money") Integer money) {
        userService.debit(userId, money);
    }
}

Modify BuyController:

package com.kaven.alibaba.controller;

import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@RestController
public class BuyController {

    @Resource
    private RestTemplate restTemplate;

    @PostMapping("/buy")
    @Transactional
    public String buy(@RequestParam("userId") Integer userId,
                      @RequestParam("productId") Integer productId,
                      @RequestParam("count") Integer count) {

        // Request parameters
        MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
        queryParams.add("userId", userId.toString());
        queryParams.add("productId", productId.toString());
        queryParams.add("count", count.toString());
        queryParams.add("money", count.toString());

        // Construction request
        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://localhost:9000/order/create").queryParams(queryParams);
        restTemplate.postForObject(builder.toUriString(), null, Void.class);

        // Construction request
        builder = UriComponentsBuilder.fromHttpUrl("http://localhost:9000/storage/deduct").queryParams(queryParams);
        restTemplate.postForObject(builder.toUriString(), null, Void.class);

        // Construction request
        builder = UriComponentsBuilder.fromHttpUrl("http://localhost:9000/user/debit").queryParams(queryParams);
        restTemplate.postForObject(builder.toUriString(), null, Void.class);
        return "success";
    }
}

Restart the application, and then use Postman to send a request to the application.


Inconsistent data.



The @ Transactional annotation will not work in the distributed environment (for simplicity, this is just a simulation of the case of users purchasing goods in the distributed environment). Next, you need to use Seata to solve this problem.

Seata

Seata is an open source distributed transaction solution, which is committed 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 (these introductions about Seata are from the official website).

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

Obviously, TM and RM are in business code, while TC is a separate application provided by Seata.

Bloggers download 1.3 Version 0 (to be compatible with Spring Cloud Alibaba version).


For simple use, there is no need to modify the configuration (the default form is based on the local file. For simplicity, the default form needs to be used. Other forms need to be used in the distributed environment, which will be introduced by bloggers later). Start Seata:

C:\Users\Dell>f:

F:\>cd F:\tools\seata-server-1.3.0\seata\bin

F:\tools\seata-server-1.3.0\seata\bin>dir
 Driver F The volume in is WorkSpace
 The serial number of the volume is D671-D29B

 F:\tools\seata-server-1.3.0\seata\bin Directory of

2021/12/29  10:50    <DIR>          .
2020/07/16  00:35    <DIR>          ..
2020/07/16  00:35             3,648 seata-server.bat
2020/07/16  00:35             4,175 seata-server.sh
2021/12/29  10:51    <DIR>          sessionStore
               2 Files          7,823 byte
               3 Directories 241,159,860,224 Available bytes

F:\tools\seata-server-1.3.0\seata\bin>seata-server.bat -p 8080


To modify a profile:

server:
  port: 9000

spring:
  application:
    name: seata
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ITkaven@666.com
    url: jdbc:mysql://47.112.7.219:3306/seata?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
  jpa:
    show-sql: true

seata:
  application-id: buy
  tx-service-group: kaven_seata_tx_group
  service:
    vgroup-mapping:
      kaven_seata_tx_group: default
    grouplist:
      - "127.0.0.1:8080"


file.conf (the service related configuration here should be consistent with that in the application.yml configuration file):

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = true
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #transaction service group mapping
  vgroupMapping.kaven_seata_tx_group = "default"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8080"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
  }
  undo {
    dataValidation = true
    logSerialization = "jackson"
    logTable = "undo_log"
  }
  log {
    exceptionRate = 100
  }
}

registry.conf:

registry {
  # file ,nacos ,eureka,redis,zk,consul,etcd3,sofa
  type = "file"
  file {
    name = "file.conf"
  }
}

config {
  # file,nacos ,apollo,zk,consul,etcd3
  type = "file"

  file {
    name = "file.conf"
  }
}

Annotate the buy interface with @ GlobalTransactional:

    @PostMapping("/buy")
    @GlobalTransactional
    public String buy(@RequestParam("userId") Integer userId,
                      @RequestParam("productId") Integer productId,
                      @RequestParam("count") Integer count) {

        // Request parameters
        MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
        queryParams.add("userId", userId.toString());
        queryParams.add("productId", productId.toString());
        queryParams.add("count", count.toString());
        queryParams.add("money", count.toString());

        // Construction request
        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://localhost:9000/order/create").queryParams(queryParams);
        restTemplate.postForObject(builder.toUriString(), null, Void.class);

        // Construction request
        builder = UriComponentsBuilder.fromHttpUrl("http://localhost:9000/storage/deduct").queryParams(queryParams);
        restTemplate.postForObject(builder.toUriString(), null, Void.class);

        // Construction request
        builder = UriComponentsBuilder.fromHttpUrl("http://localhost:9000/user/debit").queryParams(queryParams);
        restTemplate.postForObject(builder.toUriString(), null, Void.class);
        return "success";
    }

Create undo in database_ Log table.

USE seata;
DROP TABLE IF EXISTS `undo_log`;
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,
                            `ext` varchar(100) DEFAULT NULL,
                            PRIMARY KEY (`id`),
                            UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

Start the application.

Manually roll back the data in the database, and then use Postman for testing.

Client logs:


TC log:

The data in the database is also consistent.


It can also be seen from these log information that the application of Seata distributed transaction solution is successful. The demonstration of local transaction and distributed transaction and the use of Seata are introduced here. If the blogger has something wrong or you have different opinions, you are welcome to comment and supplement.

Keywords: Java Distribution Spring Cloud

Added by lynncrystal on Mon, 03 Jan 2022 19:08:49 +0200