springboot+nacos+dubbo+seata 0-based build tutorial

Keywords: Spring Boot Distribution

Preface

Share a complete set-up tutorial of Ali's Distributed Transaction Framework Seata. I feel that this tutorial is rather detailed. Basically every necessary dependency and configuration item is clear and clear. In order to make it easier for everyone to get started, the registry and micro-service frameworks use Ali system, Nacos and Dubbo. Relevant framework services are built using the latest stable version. Okay, let's not talk much rubbish, let's get started.

text

Environmental preparation

1. Setup of Nacos Registry, Official Manual Address: Click Jump

  1. First download the nacos service pack as prompted by the document Official GitHub download address]

    2. After downloading, uncompress the package, enter the bin directory, and execute the startup script
    Linux/Unix/Mac
    Start command (standalone stands for single machine mode, non-cluster mode):
sh startup.sh -m standalone

If you are using a ubuntu system or running a script with an error message [[Symbol not found, try running as follows:

bash startup.sh -m standalone

Windows
Start command (standalone stands for single machine mode, non-cluster mode):

startup.cmd -m standalone

Access the nacos default background management address in the browser after startup: http://127.0.0.1:8848/nacos , the default login account and password are both nacos. A successful login indicates that the Nacos service has been set up, as shown in the figure

2. Seata Service Deployment, Official Manual Address: Click Jump

1. First download the Seata service pack as prompted by the document [ Official Github Download Address]

2. Modify the \conf\registry.conf configuration file after decompression, and change the registry to a nacos registry

Configuration information uses the default file and then modifies \conf\file.conf to use redis for transactional storage information

3. Enter the bin directory to execute the startup script

./seata-server.bat

After successful startup, you can see in the list of services in nacos that seata's services have been registered with the registry

At this point, the environmental preparation is complete, and the next step is to build the project

Project Construction

1. Initialize the SpringBoot project [2.4.2]

Using idea to directly new a SpringBoot project, simply introduce "Spring Web", which is named microservice-user

2. Integrated nacos Registry, [2.0.3] Official Integrated Documents: Click Jump

  1. Increase nacos-related dependencies:
    The Nacos dependency is increased in the pom file because the nacos-client version of alibaba's cloud integration dependency is lower, so it depends solely on the new version of nacos-client.
        <!-- Nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2021.1</version>
            <exclusions>
                <exclusion>
                    <groupId>com.alibaba.nacos</groupId>
                    <artifactId>nacos-client</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-client</artifactId>
            <version>2.0.3</version>
        </dependency>

PS: It is important to note that nacos has a version relationship, so the SpringBoot version of the project must be adjusted to 2.4.2 here, otherwise the project may not start properly

  1. Add nacos configuration (using yml):
    The project creates a default application configuration file, which is the property configuration file, deletes it, and creates a new application.yml with the following configuration
spring:
  application:
    name: microservice-user
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        #        namespace: public # The default is the public space, and for ease of use, the default is used for all service creation configurations
        username: nacos
        password: nacos
server:
  port: 8000
  1. Add the @EnableDiscoveryClient annotation on the startup class to enable client discovery.
  2. Then start the project and you will see the microservice in the list of services in nacos after successful startup. The service name is the spring.application.name configured in application.yml, as shown in the figure

3. Integrated dubbo, [3.0.3] Official Document Address: Click Jump

  1. Introducing dubbo-related dependencies in pom files
    	<properties>
       		<java.version>1.8</java.version>
        	<dubbo.version>3.0.3</dubbo.version>
    	</properties>
        <!-- Dubbo To configure -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>${dubbo.version}</version>
            <exclusions>
                <exclusion>
                    <artifactId>spring</artifactId>
                    <groupId>org.springframework</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>${dubbo.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-registry-nacos</artifactId>
            <version>${dubbo.version}</version>
        </dependency>
  1. Add dubbo-related configurations to the application.yml configuration file, and nacos configurations directly use the configuration parameters in step 2
dubbo:
  application:
    name: ${spring.application.name}
  scan:
    base-packages: com.cn.lucky.morning.user.service.api
  registry:
    address: nacos://${spring.cloud.nacos.discovery.server-addr}
    username: ${spring.cloud.nacos.discovery.username}
    password: ${spring.cloud.nacos.discovery.password}
  1. Add interface classes under the com.cn.lucky.morning.user.api package.
/**
 * @author lucky_morning
 */
public interface AccountService {

    /**
     * Loan from user account
     *
     * @param userId User ID
     * @param money  Amount of money
     */
    void debit(String userId, int money);
}

Add an interface implementation class under the com.cn.lucky.morning.user.api package, implement the interface class from the previous step, and label the class as a service provider using @DubboService on the class

@DubboService
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private ITblAccountService tblAccountService;
    
    @Override
    public void debit(String userId, int money) {
        TblAccount account = tblAccountService.getById(userId);
        if (account == null) {
            throw new RuntimeException("user does not exist");
        }
        account.setMoney(account.getMoney() - money);
        if (account.getMoney() < 0) {
            throw new RuntimeException("Insufficient user balance");
        }
        tblAccountService.updateById(account);
    }
}
  1. Start the microservice and see the registration information for the provider class in the nacos list of services
  2. How to use the service provider provided by dubbo, create a new microservice-order project, integrate nacos and Dubbo as described above, then introduce microservice-user and inject the provider class with the @DubboReference annotation, as shown in the following figure:
    (The code in the diagram decreases the user's account amount by invoking the user microservice in the service provider method that created the order)
  3. Add a controller to the microservice-order microservice to test the call
/**
 * @author lucky_morning
 */
@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired
    private OrderService orderService;

    /**
     * Create Order
     */
    @GetMapping("/create")
    public String create(String userId, String commodityCode, int orderCount) {
        try {
            return "Order creation results:" + orderService.create(userId, commodityCode, orderCount);
        }catch (Exception e){
            return "Order creation failed:" + e.getMessage();
        }
    }
}

Access address: http://localhost:8001/order/create?userId=1&commodityCode=phone&orderCount=1
Because account data is represented as empty tables and is not inserted into the data, microservice calls to users with reduced amounts throw exceptions that do not exist for users, proving that the methods provided by user microservice have been normally invoked in order microservice

4. Integrated seata, [1.4.2] Official Document Address: Click Jump

  1. Introducing dependencies in pom files
        <!-- Seata To configure -->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.4.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <version>2021.1</version>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
  1. seata-related configurations have been added to the application.yml configuration file, and nacos is used by both the Configuration Center and the Registry
seata:
  config:
    type: nacos
    nacos:
      server-addr: ${spring.cloud.nacos.discovery.server-addr}
      username: ${spring.cloud.nacos.discovery.username}
      password: ${spring.cloud.nacos.discovery.password}
      data-id: seataServer.properties
  registry:
    type: nacos
    nacos:
      server-addr: ${spring.cloud.nacos.discovery.server-addr}
      username: ${spring.cloud.nacos.discovery.username}
      password: ${spring.cloud.nacos.discovery.password}
  1. Add a new seataServer.properties configuration file to the nacos configuration center, where you add defaults for related microservices
  2. seata AT mode (default mode) requires UNDO_LOG table
-- Notice here 0.3.0+ Increase Unique Index ux_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;
  1. Start the project, normal startup console no error message means that the configuration is normal
  2. Increase the @GlobalTransactional(rollbackFor = Exception.class) annotation on methods that require distributed transactions
  3. Configure the other two microservices in the same way

5. Description of sample business microservices

microservice-user

  1. Data sheet:
DROP TABLE IF EXISTS `tbl_account`;
CREATE TABLE `tbl_account` (
  `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;
  1. Service Provider Interface:
package com.cn.lucky.morning.user.api;

/**
 * @author lucky_morning
 */
public interface AccountService {

    /**
     * Loan from user account
     *
     * @param userId User ID
     * @param money  Amount of money
     */
    void debit(String userId, int money);
}
  1. Service Provider Interface Implementation:
package com.cn.lucky.morning.user.service.api;

import com.cn.lucky.morning.user.api.AccountService;
import com.cn.lucky.morning.user.entity.TblAccount;
import com.cn.lucky.morning.user.service.ITblAccountService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@DubboService
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private ITblAccountService tblAccountService;

    @Override
    public void debit(String userId, int money) {
        TblAccount account = tblAccountService.getById(userId);
        if (account == null) {
            throw new RuntimeException("user does not exist");
        }
        account.setMoney(account.getMoney() - money);
        if (account.getMoney() < 0) {
            throw new RuntimeException("Insufficient user balance");
        }
        tblAccountService.updateById(account);
    }
}

microservice-order

  1. Data sheet
DROP TABLE IF EXISTS `tbl_order`;
CREATE TABLE `tbl_order` (
  `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;
  1. Service Provider Interface:
package com.cn.lucky.morning.order.api;

/**
 * @author lucky_morning
 */
public interface OrderService {
    /**
     * Create Order
     * @return
     */
    boolean create(String userId, String commodityCode, int orderCount);
}
  1. Service Provider Implementation Class:
package com.cn.lucky.morning.order.service.api;

import com.cn.lucky.morning.order.api.OrderService;
import com.cn.lucky.morning.order.entity.TblOrder;
import com.cn.lucky.morning.order.service.ITblOrderService;
import com.cn.lucky.morning.user.api.AccountService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author lucky_morning
 */
@Service
@DubboService
public class OrderServiceImpl implements OrderService {
    @Autowired
    private ITblOrderService tblOrderService;
    @DubboReference
    private AccountService accountService;

    @Override
    public boolean create(String userId, String commodityCode, int orderCount) {
        int orderMoney = calculate(commodityCode, orderCount);

        accountService.debit(userId, orderMoney);

        TblOrder order = new TblOrder();
        order.setId(Integer.valueOf(userId));
        order.setCommodityCode(commodityCode);
        order.setCount(orderCount);
        order.setMoney(orderMoney);
        return tblOrderService.save(order);
    }

    /**
     * Calculate price
     *
     * @param commodityCode Commodity Code
     * @param orderCount    Quantity of Goods
     * @return Purchase price
     */
    private int calculate(String commodityCode, int orderCount) {
        return 100 * orderCount;
    }
}

microservice-storage

  1. Data sheet:
DROP TABLE IF EXISTS `tbl_storage`;
CREATE TABLE `tbl_storage` (
  `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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  1. Inventory operation interface implementation class:
package com.cn.lucky.morning.storage.service.impl;

import com.cn.lucky.morning.storage.entity.TblStorage;
import com.cn.lucky.morning.storage.mapper.TblStorageMapper;
import com.cn.lucky.morning.storage.service.ITblStorageService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
 * <p>
 * Service Implementation Class
 * </p>
 *
 * @author lucky_morning
 * @since 2021-10-20
 */
@Service
public class TblStorageServiceImpl extends ServiceImpl<TblStorageMapper, TblStorage> implements ITblStorageService {

    @Override
    public void deduct(String commodityCode, int count) {
        TblStorage storage = this.lambdaQuery().eq(TblStorage::getCommodityCode, commodityCode).last("limit 1").one();
        if (storage == null) {
            throw new RuntimeException("Commodity does not exist");
        }
        if (storage.getCount() < count) {
            throw new RuntimeException("Insufficient inventory of goods");
        }
        storage.setCount(storage.getCount() - count);
        this.updateById(storage);
    }
}

  1. Purchase operation implementation class:
package com.cn.lucky.morning.storage.service.impl;

import com.cn.lucky.morning.order.api.OrderService;
import com.cn.lucky.morning.storage.service.BusinessService;
import com.cn.lucky.morning.storage.service.ITblStorageService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author lucky_morning
 */
@Service
public class BusinessServiceImpl implements BusinessService {
    @Autowired
    private ITblStorageService storageService;
    @DubboReference
    private OrderService orderService;
    @Override
    public void purchase(String userId, String commodityCode, int orderCount) {

        // Reduce Inventory
        storageService.deduct(commodityCode,orderCount);

        // New Orders
        orderService.create(userId, commodityCode, orderCount);
    }
}

  1. Purchasing interface: http://localhost:8002//business/purchase?userId=1&commodityCode=phone&orderCount=1
package com.cn.lucky.morning.storage.controller;


import com.cn.lucky.morning.storage.service.BusinessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 * Front End Controller
 * </p>
 *
 * @author lucky_morning
 * @since 2021-10-20
 */
@RestController
@RequestMapping("/business")
public class BusinessController {

    @Autowired
    private BusinessService businessService;

    /**
     * Purchase
     *
     * @param userId        Purchasing User
     * @param commodityCode Commodity Code
     * @param orderCount    Quantity of Goods
     * @return Purchase results
     */
    @GetMapping("/purchase")
    public String purchase(String userId, String commodityCode, int orderCount) {
        try {
            businessService.purchase(userId, commodityCode, orderCount);
            return "Operation Successful";
        } catch (Exception e) {
            return "Operation failed:" + e.getMessage();
        }
    }
}

Architecture Diagram (sample business project using seata official manual)

6. Actualizing various situations

Front: Default data between tables

1. Without the note on distributed transactions, if the amount and inventory are sufficient:


If the call succeeds, the changes in the three tables are tbl_ In the account table, the account amount with user ID 1 is reduced by 100; Tbl_ Add a new order record to the order table, tbl_storage table, the commodity Code is phone inventory reduction of 1, in this theoretical normal situation, the call between various micro-services is not wrong, Hello, Hello, everyone is good, but the reality is often not so good, there will be a variety of unknown circumstances leading to midway failure, such as the next few examples.

2. Without the note on distributed transactions, if the amount is insufficient and the inventory is sufficient:

Continue to use the data table above, we will modify the requested quantity parameter to 3, then the inventory is 4 can be picked up normally at this time, but the user amount is 200, the amount is not enough

Interface returned insufficient user balance, so let's take another look at the changes in the data table?

Since the user reduction was after the inventory was reduced, the inventory of goods remained reduced after the exception was thrown

3. Increase the Distributed Transaction Note when the amount is insufficient and the inventory is sufficient:

First we restore the data in the database back to the previous step, that is, modify the inventory data back to 4, add distributed transaction annotations on the purchasing method, and restart the storage micro-service

Call interface again after successful restart

Now when we go back to the database and look at the data, we see that there is no change in the data table, and we can also see information about distributed transactions printed by seata on the storage Micro Services console.

At this point, even if the integration of distributed transactions is completed, you can see that when a method calls multiple providers of microservices to operate on different tables, an error occurs one step in the middle. If you do not use distributed transactions, all operations on the database before the error will not be restored, resulting in inconsistent data. With distributed transactions, invocations of multiple microservices are as simple as previous database transactions, either succeeding or failing to restore

PS

This is the simplest example of project integration where the code for the sample project has been placed on GitHub and accessed: https://github.com/luckymorning/SpringBootNacosSeataDubboDemoProject
If there are any differences, you are welcome to refer to orthogonal flow

Posted by webmaster1 on Wed, 20 Oct 2021 09:59:09 -0700