天天看点

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

随着业务的逐渐复杂和不断增加,以及并发量的提高,以往的单体应用慢慢会无法胜任后端的职责,使用nginx做横向扩展也只是临时解决方案,无法从根源上解问题。微服务架构应运而生,微服务架构将原来的单体应用业务做分解,一类微服务负责一类业务,每类微服务各自管理自己的数据,微服务之间使用rest或者rpc进行交换数据,这样就可以很好的解决并发和负载的问题;当某一时期业务量增大,并发需求加大(如春运中的车票系统、春晚活动、双十一促销等)时,就可以通过临时租用服务器来解决,这是一种比较经济且灵活、单体应用无法比拟的新型后端架构。

微服务虽好,但是技术复杂度和难度比单体应用大得多,以往单体应用不是问题的问题在微服务上却异常突出,如:分布式系统的世界性难题——分布式事务。目前分布式事务存在两大理论依据:CAP定律 BASE理论。 CAP定律指的是在一个分布式系统中、Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼;BASE理论指的是Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结, 是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

如果分布式系统做不到数据一致性,那么会产生很多的脏数据,会比单体应用还要糟糕。所以为了解决这种后遗症,今天介绍的是开源分布式事务框架——TX-LCN(5.0.2.RELEASE),详细文档请参考http://www.txlcn.org/zh-cn/index.html,这里只做demo的整合,基于本教程,你会很快搭建并熟悉分布式事务框架。

框架分为两部分,TX-Manager和TC微服务模块。我们先看一下项目结构:

1、TX-Manager:

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

2、TC微服务模块:

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

TX-Manager为单独运行的服务,修改配置后导出jar包运行,源代码可以从GitHub中获取:

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

TC为业务服务依赖,整合到具体微服务中。

整合开始:

首先新建数据库tx-manager和tx-lcn-service,前者作为事务管理者TX-Manager的数据库,后者为业务数据库,建表语句在项目的每个模块的resource下。

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网
CREATE DATABASE IF NOT EXISTS  `tx-manager` DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
USE `tx-manager`;

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_tx_exception
-- ----------------------------
DROP TABLE IF EXISTS `t_tx_exception`;
CREATE TABLE `t_tx_exception`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `group_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `unit_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `mod_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `transaction_state` tinyint(4) NULL DEFAULT NULL,
  `registrar` tinyint(4) NULL DEFAULT NULL,
  `ex_state` tinyint(4) NULL DEFAULT NULL COMMENT '0 待处理 1已处理',
  `remark` varchar(10240) NULL DEFAULT NULL COMMENT '备注',
  `create_time` datetime(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 967 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;
           
SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `name` varchar(255) NOT NULL COMMENT '用户姓名',
  `balance` int(10) DEFAULT NULL COMMENT '余额(分)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', '张三', '10000');
           
SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for goods
-- ----------------------------
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods` (
  `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '商品id',
  `name` varchar(255) DEFAULT NULL COMMENT '商品名称',
  `price` int(10) DEFAULT NULL COMMENT '单价(分)',
  `inventory` int(10) DEFAULT NULL COMMENT '商品库存',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of goods
-- ----------------------------
INSERT INTO `goods` VALUES ('1', '可口可乐', '300', '100');
           
SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for user_order
-- ----------------------------
DROP TABLE IF EXISTS `user_order`;
CREATE TABLE `user_order` (
  `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `user_id` int(10) NOT NULL COMMENT '用户id',
  `goods_id` int(10) NOT NULL COMMENT '商品id',
  `user_name` varchar(255) NOT NULL COMMENT '用户姓名',
  `goods_name` varchar(255) NOT NULL COMMENT '商品名称',
  `goods_count` int(1) NOT NULL COMMENT '商品数量',
  `price` int(10) NOT NULL COMMENT '单价(分)',
  `total_price` int(10) NOT NULL COMMENT '总价(分)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user_order
-- ----------------------------
           

然后修改txlcn-tm的项目配置为自己项目的实际地址和端口,这里我们修改分布式事务执行总时间为100秒,这样方便进入断点后有足够时间去观察数据库数据的变化情况。

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

打包txlcn-tm为jar运行:

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

浏览器打开:http://localhost:8069/admin/index.html#/login进入到TX-Manager后台,即可查看连接到分布式事务管理器的微服务。密码默认为:codingapi,项目中被我修改成:123456。

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

然后启动nacos和redis:

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网
SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

微服务相关知识这里不做描述,如果不熟悉请参考博主[程序员DD]的教程:http://blog.didispace.com/spring-cloud-learning/

TC-Clients:

接下来是业务微服务,这里模拟的是商城应用中的用户下单业务,分为三个模块:用户服务、商品服务和订单服务,order-service调用user-service和goods-service进行算价、扣款、扣库存、生成订单。

tx-lcn.pom

<?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.xujianjie</groupId>
    <artifactId>txlcn</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <name>tx-lcn</name>

    <modules>
        <module>order-service</module>
        <module>user-service</module>
        <module>goods-service</module>
    </modules>

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

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <mysql.version>5.1.25</mysql.version>
        <mybatis.version>2.0.0</mybatis.version>
        <tx.lcn.version>5.0.2.RELEASE</tx.lcn.version>
        <org.json.version>20180813</org.json.version>
        <fastjson.version>1.2.54</fastjson.version>
    </properties>

    <dependencyManagement>
        <dependencies>

            <!--spring cloud alibaba-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>0.2.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--mysql-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>

            <!--mybatis-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.version}</version>
            </dependency>

            <!--tx-lcn分布式事务-->
            <dependency>
                <groupId>com.codingapi.txlcn</groupId>
                <artifactId>txlcn-tc</artifactId>
                <version>${tx.lcn.version}</version>
            </dependency>

            <dependency>
                <groupId>com.codingapi.txlcn</groupId>
                <artifactId>txlcn-txmsg-netty</artifactId>
                <version>${tx.lcn.version}</version>
            </dependency>

            <!--org-json-->
            <dependency>
                <groupId>org.json</groupId>
                <artifactId>json</artifactId>
                <version>${org.json.version}</version>
            </dependency>

            <!--fastJson-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>${fastjson.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

</project>
           

user-service:

pom

<?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>
    <parent>
        <artifactId>txlcn</artifactId>
        <groupId>com.xujianjie</groupId>
        <version>1.0.0</version>
    </parent>

    <artifactId>account-service</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>user-service</name>

    <dependencies>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.codingapi.txlcn</groupId>
            <artifactId>txlcn-tc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.codingapi.txlcn</groupId>
            <artifactId>txlcn-txmsg-netty</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
           

application.properties

spring.application.name=user-service
server.port=8003

spring.cloud.nacos.discovery.server-addr=localhost:8848

#lcn负载均衡策略
tx-lcn.ribbon.loadbalancer.dtx.enabled=true

#tm监听地址
tx-lcn.client.manager-address=localhost:8070

#预计调用链平均长度
tx-lcn.client.chain-level=3

#mysql
spring.datasource.url=jdbc:mysql://localhost:3306/tx-lcn-service?useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
           
UserController.java
           
package com.xujianjie.userservice.controller;

import com.xujianjie.userservice.model.ResponseData;
import com.xujianjie.userservice.model.User;
import com.xujianjie.userservice.service.UserService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/user")
public class UserController
{
    @Autowired
    private UserService userService;

    @RequestMapping(value = "/info", method = RequestMethod.GET)
    public ResponseData info(Integer userId)
    {
        User user = userService.findById(userId);
        if (user != null)
        {
            return new ResponseData(ResponseData.STATUS_OK, user, "获取成功!");
        }
        else
        {
            return new ResponseData(ResponseData.STATUS_FAILED, null, "获取失败!");
        }
    }

    @RequestMapping(value = "/balance/deduct", method = RequestMethod.GET)
    public ResponseData deductBalance(Integer userId, Integer money)
    {
        if (userService.deductBalance(userId, money))
        {
            return new ResponseData(ResponseData.STATUS_OK, null, "扣减成功!");
        }
        else
        {
            return new ResponseData(ResponseData.STATUS_FAILED, null, "扣减失败!");
        }
    }
}
           
UserServiceImpl.java
           
package com.xujianjie.userservice.service.impl;

import com.codingapi.txlcn.tc.annotation.LcnTransaction;
import com.xujianjie.userservice.mapper.UserMapper;
import com.xujianjie.userservice.model.User;
import com.xujianjie.userservice.service.UserService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@Service
public class UserServiceImpl implements UserService
{
    @Autowired
    private UserMapper userMapper;

    @Override
    public User findById(int id)
    {
        return userMapper.findById(id);
    }

    @LcnTransaction
    @Override
    public boolean deductBalance(int userId, int money)
    {
        User user = findById(userId);
        if (user != null && user.getBalance() >= money)
        {
            user.setBalance(user.getBalance() - money);
            userMapper.update(user);
            return true;
        }

        return false;
    }
}
           
UserMapper.java
           
package com.xujianjie.userservice.mapper;

import com.xujianjie.userservice.model.User;

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

@Mapper
public interface UserMapper
{
    @Select("select * from user where id = #{id}")
    User findById(@Param("id") int id);

    @Update("update user set " +
            "name = #{user.name}, " +
            "balance = #{user.balance} " +
            "where id = #{user.id}")
    void update(@Param("user") User user);
}
           

goods-service:

pom

spring.application.name=goods-service
server.port=8002

spring.cloud.nacos.discovery.server-addr=localhost:8848

#lcn负载均衡策略
tx-lcn.ribbon.loadbalancer.dtx.enabled=true

#tm监听地址
tx-lcn.client.manager-address=localhost:8070

#预计调用链平均长度
tx-lcn.client.chain-level=3

#mysql
spring.datasource.url=jdbc:mysql://localhost:3306/tx-lcn-service?useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
           
GoodsController.java
           
package com.xujianjie.goodsservice.controller;

import com.xujianjie.goodsservice.model.Goods;
import com.xujianjie.goodsservice.model.ResponseData;
import com.xujianjie.goodsservice.service.GoodsService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/goods")
public class GoodsController
{
    @Autowired
    private GoodsService goodsService;

    @RequestMapping(value = "/info", method = RequestMethod.GET)
    public ResponseData info(Integer goodsId)
    {
        Goods goods = goodsService.findById(goodsId);
        if (goods != null)
        {
            return new ResponseData(ResponseData.STATUS_OK, goods, "获取成功!");
        }
        else
        {
            return new ResponseData(ResponseData.STATUS_FAILED, null, "获取失败!");
        }
    }

    @RequestMapping(value = "/inventory/deduct", method = RequestMethod.GET)
    public ResponseData deductBalance(Integer goodsId, Integer count)
    {
        if (goodsService.deductInventory(goodsId, count))
        {
            return new ResponseData(ResponseData.STATUS_OK, null, "扣减成功!");
        }
        else
        {
            return new ResponseData(ResponseData.STATUS_FAILED, null, "扣减失败!");
        }
    }
}
           
GoodsServiceImpl.java
           
package com.xujianjie.goodsservice.service.impl;

import com.codingapi.txlcn.tc.annotation.LcnTransaction;
import com.xujianjie.goodsservice.mapper.GoodsMapper;
import com.xujianjie.goodsservice.model.Goods;
import com.xujianjie.goodsservice.service.GoodsService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@Service
public class GoodsServiceImpl implements GoodsService
{
    @Autowired
    private GoodsMapper goodsMapper;

    @Override
    public Goods findById(int id)
    {
        return goodsMapper.findById(id);
    }

    @LcnTransaction
    @Override
    public boolean deductInventory(int goodsId, int count)
    {
        Goods goods = findById(goodsId);
        if (goods != null && goods.getInventory() >= count)
        {
            goods.setInventory(goods.getInventory() - count);
            goodsMapper.update(goods);
            return true;
        }

        return false;
    }
}
           
GoodsMapper.java
           
package com.xujianjie.goodsservice.mapper;

import com.xujianjie.goodsservice.model.Goods;

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

@Mapper
public interface GoodsMapper
{
    @Select("select * from goods where id = #{id}")
    Goods findById(@Param("id") int id);

    @Update("update goods set " +
            "name = #{goods.name}, " +
            "price = #{goods.price}, " +
            "inventory = #{goods.inventory} " +
            "where id = #{goods.id}")
    void update(@Param("goods") Goods goods);
}
           

order-service:

pom

<?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>
    <parent>
        <artifactId>txlcn</artifactId>
        <groupId>com.xujianjie</groupId>
        <version>1.0.0</version>
    </parent>

    <artifactId>order-service</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>order-service</name>

    <dependencies>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.codingapi.txlcn</groupId>
            <artifactId>txlcn-tc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.codingapi.txlcn</groupId>
            <artifactId>txlcn-txmsg-netty</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.json</groupId>
            <artifactId>json</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
           
OrderController.java
           
package com.xujianjie.orderservice.controller;

import com.xujianjie.orderservice.model.Order;
import com.xujianjie.orderservice.model.ResponseData;
import com.xujianjie.orderservice.service.OrderService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/order")
public class OrderController
{
    @Autowired
    private OrderService orderService;

    @RequestMapping(value = "/create", method = RequestMethod.GET)
    public ResponseData createOrder(Integer userId, Integer goodsId, Integer count)
    {
        orderService.createOrder(userId, goodsId, count);
        return new ResponseData(ResponseData.STATUS_FAILED, null, "下单成功!");
    }

    @RequestMapping(value = "/info", method = RequestMethod.GET)
    public ResponseData info(Integer goodsId)
    {
        Order order = orderService.findById(goodsId);
        if (order != null)
        {
            return new ResponseData(ResponseData.STATUS_OK, order, "获取成功!");
        }
        else
        {
            return new ResponseData(ResponseData.STATUS_FAILED, null, "获取失败!");
        }
    }
}
           
OrderServiceImpl.java
           
核心注解:@LcnTransaction
           
package com.xujianjie.orderservice.service.impl;

import com.alibaba.fastjson.JSON;
import com.codingapi.txlcn.tc.annotation.LcnTransaction;
import com.xujianjie.orderservice.exception.MyException;
import com.xujianjie.orderservice.mapper.OrderMapper;
import com.xujianjie.orderservice.model.Order;
import com.xujianjie.orderservice.model.ResponseData;
import com.xujianjie.orderservice.service.OrderService;

import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

@Transactional
@Service
public class OrderServiceImpl implements OrderService
{
    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    @LcnTransaction
    @Override
    public void createOrder(int userId, int goodsId, int count)
    {
        String userName = "";
        String goodsName = "";
        int userBalance = 0;
        int price = 0;
        int totalPrice = 0;
        int goodsInventory = 0;

        //用户信息
        ResponseData response1 = restTemplate.getForObject("http://user-service/api/user/info?userId=" + userId, ResponseData.class);
        if (response1 != null)
        {
            if (response1.getStatusCode() == ResponseData.STATUS_OK)
            {
                JSONObject json = new JSONObject(JSON.toJSONString(response1.getData()));
                userName = json.optString("name");
                userBalance = json.optInt("balance");
            }
            else
            {
                throw new MyException(ResponseData.STATUS_FAILED, "获取用户信息失败!");
            }
        }

        //商品信息
        ResponseData response2 = restTemplate.getForObject("http://goods-service/api/goods/info?goodsId=" + goodsId, ResponseData.class);
        if (response2 != null)
        {
            if (response2.getStatusCode() == ResponseData.STATUS_OK)
            {
                JSONObject json = new JSONObject(JSON.toJSONString(response2.getData()));
                goodsName = json.optString("name");
                price = json.optInt("price");
                goodsInventory = json.optInt("inventory");
            }
            else
            {
                throw new MyException(ResponseData.STATUS_FAILED, "获取商品信息失败!");
            }
        }

        //生成订单
        if (goodsInventory >= count)
        {
            totalPrice = count * price;

            if (userBalance >= totalPrice)
            {
                System.out.println("=======================订单信息=======================");
                System.out.println("用户名:" + userName);
                System.out.println("商品名:" + goodsName);
                System.out.println("购买数量:" + count);
                System.out.println("单价:" + price);
                System.out.println("总价:" + totalPrice);

                //扣减用户余额
                System.out.println("开始扣减用户余额");
                boolean isDeductBalanceSuccess = false;
                ResponseData response3 = restTemplate.getForObject("http://user-service/api/user/balance/deduct?userId=" + userId + "&money=" + totalPrice, ResponseData.class);
                if (response3 != null)
                {
                    System.out.println("扣减用户余额=" + JSON.toJSONString(response3));
                    isDeductBalanceSuccess = response3.getStatusCode() == ResponseData.STATUS_OK;
                }

                //扣减商品库存
                System.out.println("开始扣减商品库存");
                boolean isDeductInventorySuccess = false;
                ResponseData response4 = restTemplate.getForObject("http://goods-service/api/goods/inventory/deduct?goodsId=" + goodsId + "&count=" + count, ResponseData.class);
                if (response4 != null)
                {
                    System.out.println("扣减商品库存=" + JSON.toJSONString(response3));
                    isDeductInventorySuccess = response4.getStatusCode() == ResponseData.STATUS_OK;
                }

                if (!isDeductBalanceSuccess || !isDeductInventorySuccess)
                {
                    throw new MyException(ResponseData.STATUS_FAILED, "扣减用户余额或商品库存失败!");
                }

                Order order = new Order();
                order.setUserId(userId);
                order.setGoodsId(goodsId);
                order.setUserName(userName);
                order.setGoodsName(goodsName);
                order.setGoodsCount(count);
                order.setPrice(price);
                order.setTotalPrice(totalPrice);
                orderMapper.insert(order);

                if (count == 10)
                {
                    throw new MyException(ResponseData.STATUS_FAILED, "模拟异常!");
                }
                else
                {
                    System.out.println("订单信息创建成功");
                }
            }
            else
            {
                throw new MyException(ResponseData.STATUS_FAILED, "用户余额不足!");
            }
        }
        else
        {
            throw new MyException(ResponseData.STATUS_FAILED, "商品库存不足!");
        }
    }

    @Override
    public Order findById(int orderId)
    {
        return orderMapper.findById(orderId);
    }
}
           
OrderMapper.java
           
package com.xujianjie.orderservice.mapper;

import com.xujianjie.orderservice.model.Order;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface OrderMapper
{
    @Insert("insert into user_order(user_id, goods_id, user_name, goods_name, goods_count, price, total_price) values(" +
            "#{order.userId}, " +
            "#{order.goodsId}, " +
            "#{order.userName}, " +
            "#{order.goodsName}, " +
            "#{order.goodsCount}, " +
            "#{order.price}, " +
            "#{order.totalPrice}" +
            ")")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void insert(@Param("order") Order order);

    @Select("select * from order where id = #{id}")
    Order findById(@Param("id") int id);
}
           

所有微服务模块的Application入口均配置@EnableDistributedTransaction注解。

package com.xujianjie.orderservice;

import com.codingapi.txlcn.tc.config.EnableDistributedTransaction;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDistributedTransaction
@EnableDiscoveryClient
@SpringBootApplication
public class OrderServiceApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}
           

ok项目结构大致如此。

接下来,我们启动三个微服务(其中订单服务使用调试模式启动),可以在TX-Manager后台查询到微服务已连接,并在nacos上查询到他们。

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网
SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

接下来我们模拟下单业务,当用户下单时调用order-service,order-service调用user-service和goods-service查询用户账户余额和商品价格库存等信息进行计算总价,并扣减用户余额和商品库存,生成订单记录存库。

正常调用:调用下单接口传入用户id,商品id,商品数量:http://localhost:8001/api/order/create?userId=1&goodsId=1&count=1

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

数据库:

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网
SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网
SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

可见,所有数据正常。

接下来我们以事先预定的条件(商品数量为10)来触发异常,看数据是不是一致的:http://localhost:8001/api/order/create?userId=1&goodsId=1&count=10

进入断点后,我们可以看到订单已经执行入库,同时余额扣减和库存扣减也成功返回,此时还未抛出异常:

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

我们再看数据库:

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网
SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网
SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

我们可以看到数据库数据还未发生改变,然后我们跳过断点,抛出异常:

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

可以看到日志记录已经抛出异常并将错误信息返回给前端:

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

再看数据库:

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网
SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网
SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

所有业务数据保持了一致性!

在发生异常前,就已经调用成功了扣减用户余额和商品库存,最后在订单信息入库代码执行后才触发的异常,我们可以看到:order-service的入库指令实际上是由本地事务起作用,因为order-service抛出了异常,所以订单信息入库失败。那user-service和goods-service的扣减操作接口都请求成功了,为什么用户余额和商品库存没变呢?因为tx-lcn框架不生产事务,而是在协调本地数据库事务,在TX-Manager中,一次业务调用中的调用方和被调用方会组成事务组,只有所有组内事务都正常提交,才不会发起整体回滚事件。此次调用中,order-service抛出了异常,导致所有组内成员的事务回滚,所以扣减操作并没有执行成功。借用官方的原理图:

SpringCloud微服务使用TX-LCN实现分布式事务整合开始:本例源代码TX-LCN官网

本例源代码

TX-LCN官网

继续阅读