This is the 20th day of my participation in the First Challenge 2022

  • Microservices series: Introduction to Seata for Distributed Transactions Spring Cloud Alibaba

In the previous introductory article, we had an overview of Seata and set up the SeATa-Server server, the TC coordinator, as well as integrating Nacos.

In this article we are going to use Seata in the real world. After all, that’s what you learn to use Seata in the real world.

Seata is simple to use, mainly using the @GlobalTransactional annotation, but the scaffolding process is a little more complicated.

Without further ado, let’s begin today’s lesson.

One, foreword

  • This example usesSeataAts model
  • Due to limited space, only the core code and configuration are posted in this article. The full code address is cloud-seata: seata demo-gitee.com
  • This example uses Nacos as the registry and Nacos as the configuration center

1. Version description

  • MySQL 8.0.27
  • Nacos Server 2.0.3
  • Seata Server 1.4.2
  • Spring Boot 2.3.7. RELEASE
  • Spring Cloud Hoxton.SR9
  • Spring Cloud Alibaba 2.2.5. RELEASE

2. Case objective

This case will create three services, namely order service, inventory service and account service, and the call process among the services is as follows:

  • 1) When the user places an order, the order service is called to create an order, and then the inventory service is called remotely (OpenFeign) to deduct the inventory of the ordered item
  • 2) The order service then makes a remote call to the Account service (OpenFeign) to deduct the balance in the user account
  • 3) Finally change the order status to completed in the order service

The above operation spans three databases and has two remote calls. It is obvious that there are distributed transaction problems. The overall structure of the project is as follows:

├─ Cloud-Seata ├─ ├─ seata Product # ├─ seata ├─ ├─ seata ├─ seata Product # ├─ seata ├─ Seata │ ├─ Seata ├─ Seata │ ├─ Seata │ ├─ Seata │ ├─ Seata │ ├─ Seata │ ├─ Seata │ ├─ Seata │Copy the code

Second, the code

1. Account service establishment

Create the Seata-Account service module

1.1. Create a database

SeatA_AccountDROP DATABASE IF EXISTS seata_account;
CREATE DATABASE seata_account;
​
DROP TABLE IF EXISTS seata_account.account;
CREATE TABLE seata_account.account
(
    id               INT(11) NOT NULL AUTO_INCREMENT,
    balance          DOUBLE   DEFAULT NULL,
    last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP.PRIMARY KEY (id)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8mb4;
​
DROP TABLE IF EXISTS seata_account.undo_log;
CREATE TABLE seata_account.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 = utf8mb4;
INSERT INTO seata_account.account (id, balance)
VALUES (1.50);
Copy the code

The undo_log table in the library, which is required by Seata AT mode, is mainly used for rollback of branch transactions. Also, for testing convenience, we insert an account record with ID = 1.

1.2. Add dependencies

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
        </exclusion>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
    </exclusions>
</dependency><dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>1.4.2</version>
</dependency><dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.4.2</version>
</dependency>
Copy the code

Since the SpringCloud Alibaba dependency version used is 2.2.5.RELEASE, the built-in SEATA version is 1.3.0, but the version used by our SEATA server is 1.4.2, so we need to remove the original dependency and add the dependency of 1.4.2 again.

If the dependent versions are different, the following error is reported after startup

no available service ‘null’ found, please make sure registry config correct

Note: The seata client version must be the same as the server version.

1.3. Service Profile

server:
  port: 9201

# spring configuration
spring:
  application:
    name: seata-account
  datasource:
    druid:
      username: root
      password: root
      url: jdbc:mysql://localhost:3306/seata_account? useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
      driver-class-name: com.mysql.cj.jdbc.Driver
  cloud:
    nacos:
      discovery:
        server-addr: 127.0. 01.: 8848

# seata configuration
seata:
  # Whether to enable seATA. Default is true
  enabled: true
  ${spring.application. Name}
  application-id: ${spring.application.name}
  # Seata transaction group number, used for TC cluster name, must be the same as config.tx(nacOS) configured
  tx-service-group: ${spring.application.name}-group
  # Service configuration item
  service:
    # Mapping virtual groups and groups
    vgroup-mapping:
      ruoyi-system-group: default
    Group and Seata service mapping
    grouplist:
      default: 127.0. 01.: 8091
  config:
    type: nacos
    nacos:
      The configuration on the server side (registry. Config) needs to be consistent
      namespace: c18b9158-bcf3-4d5a-b78b-f02bc8a19353
      server-addr: localhost:8848
      group: SEATA_GROUP
      username: nacos
      password: nacos
  registry:
    type: nacos
    nacos:
      The name must be the same as the name of the seata server. The default is seata-server
      application: seata-server
      Need to be consistent with the configuration on the server side (registry.config)
      group: SEATA_GROUP
      namespace: c18b9158-bcf3-4d5a-b78b-f02bc8a19353
      server-addr: localhost:8848
      username: nacos
      password: nacos

# mybatis configuration
mybatis:
  Search for the specified package alias
  typeAliasesPackage: com.ezhang.account.mapper
  Configure mapper scan to find all mapper. XML mapping files
  mapperLocations: classpath:mapper/xml/*.xml

Copy the code

Note:

  • The nacOS configuration in the client seATA must be the same as that on the server, such as the address, namespace, and group…….
  • tx-service-groupNote that this attribute must be consistent with the configuration on the server. Otherwise, it does not take effect. For example, in the configuration above, add a new configuration to nacOSservice.vgroupMapping.seata-account-group=default, as shown below:

Value is the default

Note: Remember to add the configuration prefix service. VgroupMapping.

4. Core code

@Service
public class AccountServiceImpl implements AccountService
{
    private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class);
​
    @Resource
    private AccountMapper accountMapper;
​
    /** * Transaction propagation feature set to REQUIRES_NEW Start new transaction important !!!! Be sure to use REQUIRES_NEW */
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void reduceBalance(Long userId, Double price)
    {
        log.info("=============ACCOUNT START=================");
        log.info("XID: {}", RootContext.getXID());
​
        Account account = accountMapper.selectById(userId);
        Double balance = account.getBalance();
        log.info("Order user {} balance is {}, total commodity price is {}", userId, balance, price);
​
        if (balance < price)
        {
            log.warn("User {} balance is insufficient, current balance :{}", userId, balance);
            throw new RuntimeException("Insufficient balance");
        }
        log.info("Start deducting user {} balance", userId);
        double currentBalance = account.getBalance() - price;
        account.setBalance(currentBalance);
        accountMapper.updateById(account);
        log.info("Successful deduction of user {} balance, user account balance after deduction is {}", userId, currentBalance);
        log.info("=============ACCOUNT END================="); }}Copy the code

Note the @transactional (propagation = Propagation.REQUIRES_NEW) annotation

@RestController
@RequestMapping("/account")
public class AccountController {
    @Autowired
    private AccountService accountService;
    
    @PostMapping("/reduceBalance")
    public Map<String, Object> reduceBalance(Long userId, Double price){
        accountService.reduceBalance(userId, price);
        Map<String, Object> map = new HashMap<>();
        map.put("code"."success");
        returnmap; }}Copy the code

This is mainly used for remote calls to Feign, the rest of the code is not posted, the full code address will be at the end of the article.

2. Warehouse service construction

Create a seata-Product service module. Don’t bother with the name.

2.1. Create a database

SeatA_productDROP DATABASE IF EXISTS seata_product;
CREATE DATABASE seata_product;
​
DROP TABLE IF EXISTS seata_product.product;
CREATE TABLE seata_product.product
(
    id               INT(11) NOT NULL AUTO_INCREMENT,
    price            DOUBLE   DEFAULT NULL,
    stock            INT(11) DEFAULT NULL,
    last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP.PRIMARY KEY (id)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8mb4;
​
DROP TABLE IF EXISTS seata_product.undo_log;
CREATE TABLE seata_product.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 = utf8mb4;
​
INSERT INTO seata_product.product (id, price, stock)
VALUES (1.10.20);
Copy the code

There is also an undo_log table with a test data inserted

2.2. Add dependencies

This and account service is the same, will not stick

2.3. Service profile

The account service is basically the same as the account service, except for the port number, spring.application.name, database connection, etc

server:
  port: 9221

# spring configuration
spring:
  application:
    name: seata-product
Copy the code

Note tx-service-group again

  # Seata transaction group number used for TC cluster name
  tx-service-group: ${spring.application.name}-group
Copy the code

Add a service in Nacos console. VgroupMapping. Seata – product – group

2.4. Core code

@Service
public class ProductServiceImpl implements ProductService
{
    private static final Logger log = LoggerFactory.getLogger(ProductServiceImpl.class);
​
    @Resource
    private ProductMapper productMapper;
​
    /** * Transaction propagation feature set to REQUIRES_NEW Start new transaction important !!!! Be sure to use REQUIRES_NEW */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public Double reduceStock(Long productId, Integer amount)
    {
        log.info("=============PRODUCT START=================");
        log.info("XID: {}", RootContext.getXID());
​
        // Check inventory
        Product product = productMapper.selectById(productId);
        Integer stock = product.getStock();
        log.info("The inventory with item number {} is {} and the quantity of the order is {}", productId, stock, amount);
​
        if (stock < amount)
        {
            log.warn("Item number {} out of stock, current stock :{}", productId, stock);
            throw new RuntimeException("Out of stock");
        }
        log.info("Start deducting inventory with item number {} and unit price {}", productId, product.getPrice());
        // Deduct inventory
        int currentStock = stock - amount;
        product.setStock(currentStock);
        productMapper.updateById(product);
        double totalPrice = product.getPrice() * amount;
        log.info("Inventory {} is deducted successfully, inventory {} is deducted, and the total price of {} items is {}", productId, currentStock, amount, totalPrice);
        log.info("=============PRODUCT END=================");
        returntotalPrice; }}Copy the code
@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private ProductService productService;
​
    @PostMapping("/reduceStock")
    public Map<String, Object> reduceStock(Long productId, Integer amount){
        Double totalPrice = productService.reduceStock(productId, amount);
        Map<String, Object> map = new HashMap<>();
        map.put("code"."success");
        map.put("totalPrice", totalPrice);
        returnmap; }}Copy the code

3. Order service establishment

3.1. Create a database

Order database information seatA_ORDERDROP DATABASE IF EXISTS seata_order;
CREATE DATABASE seata_order;
​
DROP TABLE IF EXISTS seata_order.p_order;
CREATE TABLE seata_order.p_order
(
    id               INT(11) NOT NULL AUTO_INCREMENT,
    user_id          INT(11) DEFAULT NULL,
    product_id       INT(11) DEFAULT NULL,
    amount           INT(11) DEFAULT NULL,
    total_price      DOUBLE       DEFAULT NULL,
    status           VARCHAR(100) DEFAULT NULL,
    add_time         DATETIME     DEFAULT CURRENT_TIMESTAMP,
    last_update_time DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP.PRIMARY KEY (id)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8mb4;
​
DROP TABLE IF EXISTS seata_order.undo_log;
CREATE TABLE seata_order.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 = utf8mb4;
Copy the code

3.2. Add dependencies

In addition to the same dependencies as the two services above, the order service also requires an OpenFeign dependency

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Copy the code

3.3. Service configuration files

server:
  port: 9211

# spring configuration
spring:
  application:
    name: seata-order
  datasource:
    druid:
      username: root
      password: root
      url: jdbc:mysql://localhost:3306/seata_order? useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
      driver-class-name: com.mysql.cj.jdbc.Driver
  cloud:
    nacos:
      discovery:
        server-addr: 127.0. 01.: 8848

feign:
  hystrix:
    enabled: true

# seata configuration
seata:
  enabled: true
  ${spring.application. Name}
  application-id: ${spring.application.name}
  # Seata transaction group number used for TC cluster name
  tx-service-group: ${spring.application.name}-group
  Disable automatic proxy
  enable-auto-data-source-proxy: false
  # Service configuration item
  service:
    # Mapping virtual groups and groups
    vgroup-mapping:
      ruoyi-system-group: default
    Group and Seata service mapping
    grouplist:
      default: 127.0. 01.: 8091
  config:
    type: nacos
    nacos:
      namespace: c18b9158-bcf3-4d5a-b78b-f02bc8a19353
      server-addr: localhost:8848
      group: SEATA_GROUP
      username: nacos
      password: nacos
  registry:
    type: nacos
    nacos:
      application: seata-server
      group: SEATA_GROUP
      namespace: c18b9158-bcf3-4d5a-b78b-f02bc8a19353
      server-addr: localhost:8848
      username: nacos
      password: nacos

# mybatis configuration
mybatis:
  Search for the specified package alias
  typeAliasesPackage: com.ezhang.order.mapper
  Configure mapper scan to find all mapper. XML mapping files
  mapperLocations: classpath:mapper/**/*.xml
Copy the code

3.4. Core code

The test interface

@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired
    private OrderService orderService;
​
    @PostMapping("/placeOrder")
    public String placeOrder(@Validated @RequestBody PlaceOrderRequest request) {
        orderService.placeOrder(request);
        return "Order successful"; }}Copy the code

The specific implementation is through Feign to remotely call the other two services to deduct inventory, deduct the balance of the interface

@Service
public class OrderServiceImpl implements OrderService
{
    private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);
​
    @Resource
    private OrderMapper orderMapper;
​
    @Autowired
    private RemoteAccountService accountService;
​
    @Autowired
    private RemoteProductService productService;
​
    @Override
    @Transactional
    @GlobalTransactional The seATA global transaction annotation needs to be added if the transaction is started first
    public void placeOrder(PlaceOrderRequest request)
    {
        log.info("=============ORDER START=================");
        Long userId = request.getUserId();
        Long productId = request.getProductId();
        Integer amount = request.getAmount();
        log.info("Order request received, User :{}, item :{}, Quantity :{}", userId, productId, amount);
​
        log.info("XID: {}", RootContext.getXID());
​
        Order order = new Order(userId, productId, 0, amount);
​
        orderMapper.insert(order);
        log.info("The order is generated in the first stage, waiting for the payment of the inventory.");
        // Deduct the inventory and calculate the total price
        Map<String, Object> reduceStockMap = productService.reduceStock(productId, amount);
        Double totalPrice = Double.valueOf(reduceStockMap.get("totalPrice").toString());
        // Deduct the balance
        accountService.reduceBalance(userId, totalPrice);
​
        order.setStatus(1);
        order.setTotalPrice(totalPrice);
        orderMapper.updateById(order);
        log.info("Order has been placed successfully.");
        log.info("=============ORDER END================="); }}Copy the code

Note: There is a @GlobalTransactional annotation, which is provided by Seata to start global transactions.

RemoteAccountService and RemoteProductService

@FeignClient(contextId = "remoteAccountService", value = "seata-account")
public interface RemoteAccountService {
​
    @PostMapping(value = "/account/reduceBalance")
    Map<String, Object> reduceBalance(@RequestParam("userId") Long userId, @RequestParam("price") Double price);
​
}
​
@FeignClient(contextId = "remoteProductService", value = "seata-product")
public interface RemoteProductService {
​
    @PostMapping(value = "/product/reduceStock")
    Map<String, Object> reduceStock(@RequestParam("productId") Long productId, @RequestParam("amount") Integer amount);
​
}
Copy the code

Since we are only testing Seata, the degraded fallbackFactory is not added.

Don’t forget the @enableFeignClients annotation on the startup class.

Now that our code is basically written, let’s test it out.

Three, test,

The premise of the test is that the Nacos, Seata and MySQL used in the test are successfully started, and then the three services built above are successfully started

The price of the item with ID 1 in the Product table in the SEATA_Product repository is 10 and the inventory is 20

The balance of user 1 in the account table of the seatA_account account library is 50

1. Order normally

To simulate the normal order, buy a commodity at http://localhost:9211/order/placeOrder

Parameters:

{
    "userId": 1,
    "productId": 1,
    "amount": 1
}
Copy the code

View console logs:

2. Inventory is insufficient

Simulation of inventory shortage, transaction rollback http://localhost:9211/order/placeOrder

Content-Type/application/json

{
    "userId": 1,
    "productId": 1,
    "amount": 21
}
Copy the code

Request exception, console log:

Order records added to p_order table are rolled back

3. The user balance is insufficient

Lack of simulated user balance, transaction rollback http://localhost:9211/order/placeOrder

Content-Type/application/json

{
    "userId": 1,
    "productId": 1,
    "amount": 6
}
Copy the code

Request exception, console log:

P_order, product, account all records are rolled back.

At this point, the test is complete and this article is closed.

Cloud-seata: seata Demo-gitee.com

Give it a thumbs up, Daniel!