[spring cloud] spring cloud integrates the client business process of Seata distributed transactions

preface

Article on Spring cloud integrates the basic environment of Seata distributed transaction (Part I) In this paper, we mainly complete the function implementation of three micro service order services: cloudalibaba Seata order service2001, inventory service: cloudalibaba Seata storage service2002 and balance service: cloudalibaba Seata account service2003.

The package structure of the whole order service is as follows. The package structures of the three services are basically the same, so they will not be posted one by one.

Of course, in order to avoid space, entity classes use lombok plug-ins to simplify getter s, setter s and other methods. After adding maven dependencies, don't forget to download the plug-ins in idea.

After creating the order module, we first create a POJO (also called domain, entity, etc.), that is, an entity class mapped to the database table.

Order entity class:

package com.dl.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order
{
    private Long id; //Primary key id

    private Long userId; //User id

    private Long productId; //Product id

    private Integer count; //quantity

    private BigDecimal money; //amount of money

    private Integer status; //Order status: 0: being created; 1: Closed
}

Next, write the mapper interface and the corresponding xml file from the dao layer. In order to quickly realize the jump between xml and mapper interface, use a Free Mybatis plugin.

After the installation, after the mapper and xml are written, you can use the small arrow to jump quickly.

Order dao interface:

package com.dl.dao;

import com.dl.pojo.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper //Native collocation annotation using mybatis
public interface OrderDao {
    //1 new order
    int create(Order order);

    //2. Modify the order status from zero to 1
    int update(@Param("userId") Long userId,@Param("status") Integer status);
}

After the dao is created, we create the corresponding XML file OrderMapper.xml in the mapper folder under the resources directory xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.dl.dao.OrderDao">

    <resultMap id="BaseResultMap" type="com.dl.pojo.Order">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="count" property="count" jdbcType="INTEGER"/>
        <result column="money" property="money" jdbcType="DECIMAL"/>
        <result column="status" property="status" jdbcType="INTEGER"/>
    </resultMap>

    <insert id="create">
        insert into t_order (id,user_id,product_id,count,money,status)
        values (null,#{userId},#{productId},#{count},#{money},0);
    </insert>


    <update id="update">
        update t_order set status = 1
        where user_id=#{userId} and status = #{status};
    </update>

</mapper>

Then write the service business logic layer, which mainly includes three interfaces, one is the order creation interface, and the other two are the interfaces used for remote calling of inventory service and balance service.
Order interface:

package com.dl.service;

import com.dl.pojo.Order;

public interface OrderService {
    int create(Order order);
}

In the remote call, we use openfeign to make the remote call. The bottom layer actually encapsulates the ribbon. The following is the use of httpclient. After the order is created, we need to make inventory deduction according to the product id and product quantity. The remote call inventory service interface is as follows:

package com.dl.service;

import com.dl.response.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "seata-storage-service") //value is the application of inventory service Name service name (configured in yml)
public interface StorageService {
    //Deduct inventory and call inventory service
    @PostMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

Finally, the account balance needs to be deducted. Therefore, the account balance service is called remotely. The interface for remote call to deduct account balance is as follows:

package com.dl.service;

import com.dl.response.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

@FeignClient(value = "seata-account-service") //Remote call account balance service
public interface AccountService {
    //Deduction account
    @PostMapping(value = "/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

Then we continue to improve the implementation class of order creation, which mainly includes three logics: order creation - inventory deduction - account balance deduction. If there is a problem in each link, the overall transaction should be rolled back. Therefore, the annotation @ globaltransaction provided by Seata is used to ensure transaction rollback, such as inventory deduction failure and account balance insufficient deduction failure, Remote call timeout, etc. is equivalent to starting global transactions here. We still guarantee each local transaction through @ Transactional.

package com.dl.service.impl;

import com.dl.dao.OrderDao;
import com.dl.pojo.Order;
import com.dl.service.AccountService;
import com.dl.service.OrderService;
import com.dl.service.StorageService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderDao orderDao;

    @Autowired
    private AccountService accountService;
    @Autowired
    private StorageService storageService;

    /**
     * Create order - > call inventory service to deduct inventory - > call account service to deduct account balance - > Modify order status
     * Place order - > deduct inventory - > deduct balance - > change status in a distributed transaction
     * @param order
     */
    @GlobalTransactional(rollbackFor = Exception.class)
    @Override
    public int create(Order order) {
        log.info("------------Start creating order!------------");
        int result = orderDao.create(order);

        log.info("------------Call inventory service to start deducting inventory!------------");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("------------End of inventory deduction!------------");

        log.info("------------Call inventory service to start account deduction!------------");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("------------End of account balance deduction!------------");

        log.info("------------Start modifying order status!------------");
        result=orderDao.update(order.getUserId(),0); //0: creating, 1: creating completed
        log.info("------------End of order status modification!------------");

        log.info("------------End of order transaction!------------");
        return result;
    }
}

Meanwhile, since Seata needs to deal with the database, we need to cancel the automatic configuration of the data source and give the proxy of the data source to Seata. First, add the config configuration class as follows:

package com.dl.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;

/**
 * We know that for transaction processing, the most important thing is to get the data source, because through the data source, we can control when the transaction is rolled back or committed,
 * Therefore, we need seata to proxy the data source and exclude the automatically loaded data source @ SpringBootApplication from our startup annotation
 * (exclude = {DataSourceAutoConfiguration.class})And configure the seata data source agent
 */
@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }
    /**
     * Create DataSourceProxy
     */
    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    /**
     * Replace the original DataSource object with DataSourceProxy
     */
    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }
}

Then remove the automatic configuration of the data source on the main startup class as follows (the other two services also need to be configured):

package com.dl;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient 
@EnableFeignClients
//Cancel the automatic creation of the data source, but use the self-defined data source. Seata needs to get the proxy of the data source
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@MapperScan({"com.dl.dao"})
public class SeataOrder2001Application {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrder2001Application.class,args);
    }
}

Finally, the controller provides the user with a single interface. At the same time, in order to unify the response, first define a simple response result class:

package com.dl.response;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class CommonResult<T>{
    private Integer code;
    private String message;
    private T data;

    public CommonResult(Integer code, String message, T data){
        this.code=code;
        this.message=message;
        this.data=data;
    }

    public static <T> CommonResult<T>createBySuccessMsg(String message,T data){
        return new CommonResult<T>(200,message,data);
    }
    public static <T> CommonResult<T>createByErrorMsg(String message,T data){
        return new CommonResult<T>(404,message,data);
    }
}

Code of order service controller:

package com.dl.controller;

import com.dl.pojo.Order;
import com.dl.response.CommonResult;
import com.dl.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;

    @RequestMapping(value="/order/create")
    public CommonResult<String> create(Order order){
        int result = orderService.create(order);
        if(result>0){
            return CommonResult.createBySuccessMsg("Order transaction succeeded!",null);
        }else{
            return CommonResult.createByErrorMsg("Order transaction failed!",null);
        }
    }
}

In the inventory service, the corresponding entity classes are:

package com.dl.pojo;

import lombok.Data;

@Data
public class Storage {

    private Long id;

    /**
     * Product id
     */
    private Long productId;

    /**
     * Total inventory
     */
    private Integer total;

    /**
     * Used inventory
     */
    private Integer used;

    /**
     * Remaining inventory
     */
    private Integer residue;
}

Inventory service deducting inventory dao:

package com.dl.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface StorageDao {
    //Deduct inventory
    int decrease(@Param("productId") Long productId, @Param("count") Integer count);
}

The corresponding mapper file storagemapper xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.dl.dao.StorageDao">

    <resultMap id="BaseResultMap" type="com.dl.pojo.Storage">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="total" property="total" jdbcType="INTEGER"/>
        <result column="used" property="used" jdbcType="INTEGER"/>
        <result column="residue" property="residue" jdbcType="INTEGER"/>
    </resultMap>

    <update id="decrease">
        UPDATE
            t_storage
        SET
            used = used + #{count},residue = residue - #{count}
        WHERE
            product_id = #{productId} AND residue>#{count}
    </update>

</mapper>

Inventory deduction interface of business logic layer:

package com.dl.service;

import org.springframework.web.bind.annotation.RequestParam;

public interface StorageService {
    int decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

Corresponding implementation class:

package com.dl.service.impl;

import com.dl.dao.StorageDao;
import com.dl.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Slf4j
public class StorageServiceImpl implements StorageService {

    @Autowired
    private StorageDao storageDao;

    @Transactional(rollbackFor = Exception.class) //Ensure local transaction rollback
    @Override
    public int decrease(Long productId, Integer count) {
        log.info("---------------Inventory service begins to deduct inventory---------------");
        return storageDao.decrease(productId,count); //If it is returned directly here, the log of deduction end is not written
    }
}

Provide an interface in the controller for external service calls:

package com.dl.controller;

import com.dl.response.CommonResult;
import com.dl.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class StorageController {
    @Autowired
    private StorageService storageService;

    /**
     * Deduct inventory
     */
    @RequestMapping("/storage/decrease")
    public CommonResult<String> decrease(Long productId, Integer count) {
        int result = storageService.decrease(productId, count);
        if(result>0){
            return CommonResult.createBySuccessMsg("Inventory deduction succeeded!",null);
        }else{
            return CommonResult.createByErrorMsg("Deduction library failed!",null);
        }
    }
}

The service name is configured through the configuration file, and we all use nacos as the service discovery, so the caller can obtain the service directly through nacos.

As for the configuration of config, response and main startup class, they are consistent in the three services.

Account balance service

Account entity class:

package com.dl.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {

    private Long id;

    /**
     * User id
     */
    private Long userId;

    /**
     * Total amount
     */
    private BigDecimal total;

    /**
     * Used limit
     */
    private BigDecimal used;

    /**
     * Remaining limit
     */
    private BigDecimal residue;
}

dao interface:

package com.dl.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.math.BigDecimal;
@Mapper
public interface AccountDao {
    int decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}

Corresponding accountmapper XML file:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.dl.dao.AccountDao">

    <resultMap id="BaseResultMap" type="com.dl.pojo.Account">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="total" property="total" jdbcType="DECIMAL"/>
        <result column="used" property="used" jdbcType="DECIMAL"/>
        <result column="residue" property="residue" jdbcType="DECIMAL"/>
    </resultMap>

    <update id="decrease">
        UPDATE t_account
        SET
          residue = residue - #{money},used = used + #{money}
        WHERE
          user_id = #{userId} AND residue>#{money}
    </update>

</mapper>

service business logic layer

service interface:

package com.dl.service;

import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;


public interface AccountService {
    /**
     * Deduction of account balance
     * @param userId User id
     * @param money amount of money
     */
    int decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

Corresponding implementation class:

package com.dl.service.impl;

import com.dl.dao.AccountDao;
import com.dl.service.AccountService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public int decrease(Long userId, BigDecimal money) {
        log.info("---------------The account service starts to deduct the account amount--------------");
        return accountDao.decrease(userId,money);
    }
}

Finally, the controller:

package com.dl.controller;

import com.dl.response.CommonResult;
import com.dl.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
public class AccountController {
    @Autowired
    private AccountService accountService;

    @RequestMapping("/account/decrease")
    public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
        int result = accountService.decrease(userId,money);
        if(result>0){
            return CommonResult.createBySuccessMsg("Account deduction succeeded",null);
        }else{
            return CommonResult.createByErrorMsg("Account deduction failed!",null);
        }
    }
}

So far, three services have been completed, and the basic implementation has been completed. Then we use the interface to test:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
Before testing, ensure that Nacos and Seata services are started, and then start the 2002 and 2003 inventory and account balance services and 2001 order services. You can use the above interfaces for testing. If you find that the business function is normal, you can also carry out exception testing. For example, we use remote calling, the calling interface has a certain response time, and if it times out, it will fail, You can manually turn off the inventory or account service. After an exception occurs, it is found that other services have been submitted locally and can be rolled back normally. So far, the Cloud integration Seata distributed transaction test has been completed.

Keywords: Java Spring Distribution Spring Cloud

Added by DaZZleD on Sat, 25 Dec 2021 08:25:59 +0200