随着业务的逐渐复杂和不断增加,以及并发量的提高,以往的单体应用慢慢会无法胜任后端的职责,使用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:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIwczX0xiRGZkRGZ0Xy9GbvNGL2EzXlpXazxSPNRlTxsGROlXQE50dFpWYxh2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL2AzNxQTNyQTM3EDNwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
2、TC微服务模块:
TX-Manager为单独运行的服务,修改配置后导出jar包运行,源代码可以从GitHub中获取:
TC为业务服务依赖,整合到具体微服务中。
整合开始:
首先新建数据库tx-manager和tx-lcn-service,前者作为事务管理者TX-Manager的数据库,后者为业务数据库,建表语句在项目的每个模块的resource下。
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秒,这样方便进入断点后有足够时间去观察数据库数据的变化情况。
打包txlcn-tm为jar运行:
浏览器打开:http://localhost:8069/admin/index.html#/login进入到TX-Manager后台,即可查看连接到分布式事务管理器的微服务。密码默认为:codingapi,项目中被我修改成:123456。
然后启动nacos和redis:
微服务相关知识这里不做描述,如果不熟悉请参考博主[程序员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上查询到他们。
接下来我们模拟下单业务,当用户下单时调用order-service,order-service调用user-service和goods-service查询用户账户余额和商品价格库存等信息进行计算总价,并扣减用户余额和商品库存,生成订单记录存库。
正常调用:调用下单接口传入用户id,商品id,商品数量:http://localhost:8001/api/order/create?userId=1&goodsId=1&count=1
数据库:
可见,所有数据正常。
接下来我们以事先预定的条件(商品数量为10)来触发异常,看数据是不是一致的:http://localhost:8001/api/order/create?userId=1&goodsId=1&count=10
进入断点后,我们可以看到订单已经执行入库,同时余额扣减和库存扣减也成功返回,此时还未抛出异常:
我们再看数据库:
我们可以看到数据库数据还未发生改变,然后我们跳过断点,抛出异常:
可以看到日志记录已经抛出异常并将错误信息返回给前端:
再看数据库:
所有业务数据保持了一致性!
在发生异常前,就已经调用成功了扣减用户余额和商品库存,最后在订单信息入库代码执行后才触发的异常,我们可以看到:order-service的入库指令实际上是由本地事务起作用,因为order-service抛出了异常,所以订单信息入库失败。那user-service和goods-service的扣减操作接口都请求成功了,为什么用户余额和商品库存没变呢?因为tx-lcn框架不生产事务,而是在协调本地数据库事务,在TX-Manager中,一次业务调用中的调用方和被调用方会组成事务组,只有所有组内事务都正常提交,才不会发起整体回滚事件。此次调用中,order-service抛出了异常,导致所有组内成员的事务回滚,所以扣减操作并没有执行成功。借用官方的原理图: