业务需求
用户下订单服务、扣库存服务和扣账号余额服务,三个服务要保证原子性,要么全部成功,要么全部失败,利用Seata的分布式事务可以解决全局原子性的问题,由于订单、库存和账户属于强绑定业务,属于强一致性,所以必然选择Seata中的XA模式来解决当前问题,但是为了了解AT的模式,我们也将利用AT模式来演示当前业务。
SpringCloudAlibaba之Seata-AT和XA模式
SpringCloudAlibaba之Seata-TCC和Saga

数据库表
表结构:库存表、订单表、账户表
库存表: 商品编码(commodity_code)、库存(count)
订单表:用户id(user_id)、商品编码(commodity_code)、购买数量(count)、价格(money)
账户表:用户编码(user_id)、账户余额(money)
以MySQL数据库为例,建表语句如下所示:
DROP TABLE IF EXISTS `stock_tbl`;CREATE TABLE `stock_tbl`(`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;DROP TABLE IF EXISTS `order_tbl`;CREATE TABLE `order_tbl`(`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;DROP TABLE IF EXISTS `account_tbl`;CREATE TABLE `account_tbl`(`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;
将三张表分别在不同的数据库上执行,以达到模拟不同数据源的效果。

微服务构建
业务服务:当前模块的作用是调用库存和订单接口,在seata中充当了RM的角色,用于开启全局事务、提交事务和回滚事务,也是提供接口给到用户调用的入口,接口地址为
//count为用户购买商品的数量,固定买一个商品,商品编码在代码内部封装了http://localhost:8072/purchase?count=1

seata-biz微服务端口号为8072,服务注册到nacos注册中心,将seata注册到nacos请查看文章:SpringCloud Alibaba之Seata简介和安装配置
核心的购买业务逻辑代码如下:先扣库存再创建订单
@GlobalTransactionalpublic void purchase(String userId, String commodityCode, int orderCount, boolean rollback) {String xid = RootContext.getXID();LOGGER.info("New Transaction Begins: " + xid);//扣库存服务String result = stockFeignClient.deduct(commodityCode, orderCount);if (!SUCCESS.equals(result)) {throw new RuntimeException("库存服务调用失败,事务回滚!");}//创建订单服务result = orderFeignClient.create(userId, commodityCode, orderCount);if (!SUCCESS.equals(result)) {throw new RuntimeException("订单服务调用失败,事务回滚!");}if (rollback) {throw new RuntimeException("Force rollback ... ");}}
要保证分布式事务需要添加注解 @GlobalTransactional代表开启了全局的事务,具体源码分析将会在后面的文章。再来看下调用库存和订单服务,使用了Feign的组件。
//调用库存服务@FeignClient(name = "seata-stock")public interface StockFeignClient {@GetMapping("/deduct")String deduct(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") int count);}
//调用订单服务@FeignClient(name = "seata-order")public interface OrderFeignClient {@GetMapping("/create")String create(@RequestParam("userId") String userId, @RequestParam("commodityCode") String commodityCode,@RequestParam("orderCount") int orderCount);}
启动业务服务,可以在nacos管理后台查看服务列表,这里已经将所有的服务列表都注册到nacos中了。

其中的seata-server为seata的服务端,也就是在seata中充当TC的角色,TC的作用是维护这事务的各种状态信息并作出提交和回滚的决策。
账户服务

当前服务提供给订单调用,订单在创建的时候需要对用户进行余额扣除操作,同样的也需要纳入到全局的分布式事务中,加入seata的依赖和配置信息。
订单服务

看下生成订单的核心逻辑
public void create(String userId, String commodityCode, Integer count) {String xid = RootContext.getXID();LOGGER.info("create order in transaction: " + xid);// 定单总价 = 订购数量(count) * 商品单价(100)int orderMoney = count * 100;// 生成订单jdbcTemplate.update("insert order_tbl(user_id,commodity_code,count,money) values(?,?,?,?)",new Object[] {userId, commodityCode, count, orderMoney});// 调用账户余额扣减String result = accountFeignClient.reduce(userId, orderMoney);if (!SUCCESS.equals(result)) {throw new RuntimeException("Failed to call Account Service. ");}}
调用了扣减账户余额服务,具体代码如下
@FeignClient(name = "seata-account")public interface AccountFeignClient {@GetMapping("/reduce")String reduce(@RequestParam("userId") String userId, @RequestParam("money") int money);}
库存服务

//扣库存业务逻辑public void deduct(String commodityCode, int count) {String xid = RootContext.getXID();LOGGER.info("deduct stock balance in transaction: " + xid);jdbcTemplate.update("update stock_tbl set count = count - ? where commodity_code = ?",new Object[] {count, commodityCode});}
四个服务已经就绪,在验证之前需要配置数据源的代码,我们需要验证AT和XA的模式,他们两个在开发上的区别就是数据源代理不同,AT模式使用了DataSourceProxy,XA则使用了DataSourceProxyXA,其它都是一样子的,只是在AT的模式下,我们需要在分支事务对应的数据库创建undo_log表,因为回滚的时候,AT模式是基于修改之前的日志记录来回滚的。来看下数据源代理的配置代码,如下所示,每个数据源都需要配置代理。
@Configurationpublic class StockXADataSourceConfiguration {@Bean@ConfigurationProperties(prefix = "spring.datasource.druid")public DruidDataSource druidDataSource() {return new DruidDataSource();}@Bean("dataSourceProxy")public DataSource dataSource(DruidDataSource druidDataSource) {//DataSourceProxy for AT modereturn new DataSourceProxy(druidDataSource);// DataSourceProxyXA for XA mode//return new DataSourceProxyXA(druidDataSource);}@Bean("jdbcTemplate")public JdbcTemplate jdbcTemplate(DataSource dataSourceProxy) {return new JdbcTemplate(dataSourceProxy);}}
先验证在AT数据源代理下分布式事务的演示过程。
Seata的AT模式演示流程
1、启动所有服务并注册到nacos,上文中我们已经看过一共4个微服务和1个seata-server服务。

2、初始化数据,将个人账户的余额设置为1000,库存设置为100,订单表为空
库存表数据如下图所示

个人账户表数据如下图所示

订单表数据如下图所示

3、访问业务服务提供的购买接口,设置购买数量为1
http://localhost:8072/purchase?count=1先验证正确的逻辑,访问接口得到SUCCESS结果

再看下后台打印的一些日志信息

我们将扣减账户余额的接口添加一个错误逻辑,让它抛异常,然后看下数据是否全部都回滚了。

再来请求一下,并且将数据重置一下。可以看到订单服务失败,不是账户服务异常了吗,我们看下后台日志

看下订单服务报的错误,是因为账户服务错误,订单服务中有回滚的日志

库存服务也打印了回滚日志,后台数据没有任何变化

如果我们在异常上面打个断点,其实我们可以看到此时后端的数据是被更新了,undo_log表中也会有日志,一起来验证一下,此时需要设置feign的超时时间为60s以便调试。配置为
feign:client:config:default:ConnectTimeOut: 10000ReadTimeOut: 60000
此时我们要注意,我们的断点是打在了出异常的地方,也就是此时还没有出现异常,现在来看下后台数据库数据的变化。
发现库存被扣了一个

生成了一个订单

账户余额没有变,因为在账户的服务上打的断点,所以账户的事务根据就没有提交

所以可以得出结论,在AT模式下的一阶段部分事务已经提交,如果此时进行查询数据,则会查询到中间状态数据,如果没有发生异常则不会出现问题,但是如果发生了异常,则会根据undo_log中的数据进行回滚,此时回滚到之前的数据,那么刚才出现和查询的数据就是脏数据,所以seata的AT模式下可能会产生脏数据,它是一种补偿机制,并不是基于每个分支事务的状态来做判断,就是因为有这个缺点,所以XA就出现了,XA是基于每个分支事务的状态进行提交和回滚,没有中间的undo_log日志。下文将会继续探索XA模式下的案例和源码分析,案例的代码和AT一模一样,只是数据源代理更换一下即可。