@

2PC 的传统方案是在数据库层面实现的,为了减少不必要的对接成本,国际开放标准组织 Open Group 定义了分布式事务处理模型 DTP(Distributed Transaction Processing Reference Model)。

  • AP (Application Program):使用 DTP 分布式事务的应用程序,一般是业务工程。

  • RM (Resource Manager):资源管理器,可以理解为事务的参与者,一般指的是数据库实例,通过资源管理噐对该数据库进行控制,资源管理噐控制着分支事务 。

  • TM (Transaction Manager):事务管理器,负责协调和管理事务,事务管理噐控制着全局事务,管理事务生命周期,并协调各个 RM 。 全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一项工作,这项工作即是一个全局事务。

DTP 模型定义了 TM 和 RM 之间通讯的接口规范叫 XA ,基于数据库的 XA 协议来实现 2PC 又称为 XA 方案。

AP、RM 与 TM 角色之间的交互方式如下:

  1. TM 向 AP 提供应用程序编程接口, AP 通过 TM 提交和回滚事务。
  2. TM 通过 XA 接口来通知 RM 数据库事务的开始 、 结束提交和回滚等。

seata 的 xa 模型如下图所示:

1 下载示例

利用 git 命令把 Seata XA 模式示例下载到本地,下载地址:https://github.com/seata/seata-samples/tree/master/seata-xa

2 示例结构

示例采用 spring-cloud 微服务架构,数据库采用 mysql,数据库连接池采用 druid,数据库 DAO 层采用 jdbcTemplate。

把 Seata XA 模式示例装载到 IDEA 中。

目录或文件 说明
account-xa 账户模块
business-xa 业务模块
order-xa 订单模块
sql 初始化脚本所在文件夹
stock-xa 库存模块
pom.xml 父类 pom 配置

3 业务服务 business-xa

触发点在业务服务中,所以我们先从这里开始分析。

3.1 模块结构

模块是标准的 Maven 结构。

目录或文件 说明
io/seata/sample/controller 控制层
io/seata/sample/feign feign客户端
BusinessXAApplication Spring 启动类
BusinessXADataSourceConfiguration seata XA 模式配置类
TestDatas 测试数据类

3.2 Controller 层

Controller 层定义了一个 BusinessController 类,里面只有一个 purchase “购买” 方法,入参为 rollback(是否强制回滚,默认为 false) 与 count(购买数量,默认为 30)。强制回滚指的是即使业务逻辑都执行成功,仍然主动进行回滚。

    @RequestMapping(value = "/purchase", method = RequestMethod.GET, produces = "application/json")
public String purchase(Boolean rollback, Integer count) {
int orderCount = 30;
if (count != null) {
orderCount = count;
}
try {
businessService.purchase(TestDatas.USER_ID, TestDatas.COMMODITY_CODE, orderCount,
rollback == null ? false : rollback.booleanValue());
} catch (Exception exx) {
return "Purchase Failed:" + exx.getMessage();
}
return SUCCESS;
}

3.3 Service 层

Service 层定义了 BusinessService 类,该类有两个方法,它们分别是 purchase 与 initData。

(1)purchase 方法

purchase 方法定义了购买逻辑。

 @GlobalTransactional
public 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 ... ");
}
}

具体步骤如下:

  1. purchase 方法标注了 @GlobalTransactional,表示开启全局事务。
  2. 利用 RootContext.getXID() 来获取事务 ID。
  3. 调用库存服务 stockFeignClient.deduct(commodityCode, orderCount),扣减商品库存。
  4. 调用订单服务 orderFeignClient.create(userId, commodityCode, orderCount),创建商品订单。
  5. 如果是强制回滚模式,就直接抛出 RuntimeException 异常。

(2)initData 方法

initData 方法用于初始化数据。

    @PostConstruct
public void initData() {
jdbcTemplate.update("delete from account_tbl");
jdbcTemplate.update("delete from order_tbl");
jdbcTemplate.update("delete from stock_tbl");
jdbcTemplate.update("insert into account_tbl(user_id,money) values('" + TestDatas.USER_ID + "','10000') ");
jdbcTemplate.update(
"insert into stock_tbl(commodity_code,count) values('" + TestDatas.COMMODITY_CODE + "','100') ");
}

该方法标注上了 @PostConstruct 注解。被@PostConstruct修饰的方法会在服务器加载 Servlet 的时候运行,并且只会被服务器执行一次[1]

初始化数据逻辑如下:

  1. 删除账户表、订单表与库存表中的数据。
  2. 插入一条账户信息,账户金额为 10000。
  3. 插入一条库存信息,库存量为 100。

3.4 stock Feign 客户端

stock Feign 客户端用于调用库存服务,配置的是本地服务。入参是商品代码(commodityCode)与数量(count)。

@FeignClient(name = "stock-xa", url = "127.0.0.1:8081")
public interface StockFeignClient { @GetMapping("/deduct")
String deduct(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") int count); }

3.5 order Feign 客户端

order Feign 客户端用于订单服务,配置的也是本地服务。入参是用户ID(userId)、商品代码(commodityCode)与数量(count)。

@FeignClient(name = "order-xa", url = "127.0.0.1:8082")
public interface OrderFeignClient { @GetMapping("/create")
String create(@RequestParam("userId") String userId, @RequestParam("commodityCode") String commodityCode,
@RequestParam("orderCount") int orderCount); }

4 库存服务 stock-xa

库存模块的结构与业务模块大同小异,所以我们直接来看它的 service 层。

4.1 服务层 StockService

StockService 定义了 deduct 方法,用于扣减商品的库存数量。

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});
}

具体步骤如下:

  1. 利用 RootContext.getXID() 来获取事务 ID。
  2. 扣减商品的库存数量。

4.2 数据源配置

使用了 DataSourceProxyXA 类代理了 DruidDataSource。

@Bean("dataSourceProxy")
public DataSource dataSource(DruidDataSource druidDataSource) {
// DataSourceProxy for AT mode
// return new DataSourceProxy(druidDataSource); // DataSourceProxyXA for XA mode
return new DataSourceProxyXA(druidDataSource);
}

5 订单服务 order-xa

数据源配置与库存服务相同,也是使用 DataSourceProxyXA 类来代理 DruidDataSource。

5.1 服务层 OrderService

service 层只有一个 OrderService 类,该类定义了 create 方法,用于创建库存。

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. ");
} }

具体步骤如下:

  1. 利用 RootContext.getXID() 来获取事务 ID。一般用于记录日志。
  2. 计算定单总价,公式为订购数量(count) * 商品单价(100)。这里的商品单价写死在代码中。
  3. 创建一条订单。
  4. 调用账户服务扣减账户余额。

5.2 account feign 客户端

account feign 客户端用于扣减账户余额,入参是用户ID(userId)与金额(money)。

@FeignClient(name = "account-xa", url = "127.0.0.1:8083")
public interface AccountFeignClient {
@GetMapping("/reduce")
String reduce(@RequestParam("userId") String userId, @RequestParam("money") int money);
}

6 账户服务 account-xa

数据源配置与库存服务相同,也是使用 DataSourceProxyXA 类来代理 DruidDataSource。

6.1 服务层 AccountService

我们来看 AccountService 类的 reduce 方法。

@Transactional
public void reduce(String userId, int money) {
String xid = RootContext.getXID();
LOGGER.info("reduce account balance in transaction: " + xid);
jdbcTemplate.update("update account_tbl set money = money - ? where user_id = ?", new Object[] {money, userId});
int balance = jdbcTemplate.queryForObject("select money from account_tbl where user_id = ?",
new Object[] {userId}, Integer.class);
LOGGER.info("balance after transaction: " + balance);
if (balance < 0) {
throw new RuntimeException("Not Enough Money ...");
}
}

处理逻辑如下:

  1. 使用 @Transactional 来包裹该方法。
  2. 利用 RootContext.getXID() 来获取事务 ID。
  3. 扣减账户剩余金额。
  4. 查询当前账户剩余金额并返回结果。

以上四个服务(业务服务、库存服务、订单服务、账户服务),只有这个方法标注了 @Transactional,所以不确定在什么情况下进行标注。

7 测试

7.1 执行测试脚本

脚本放置在 \seata\seata-samples\seata-xa\sql\all_in_one.sql 中,用于创建库存、订单与账户表。执行该脚本,写入本地 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;

7.2 配置数据源

分别进入 Account 服务、Business 服务、Order 服务和 Stock 服务,修改 application.properties 中与数据库相关的配置。形如:

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/cloud?useSSL=false&serverTimezone=UTC
spring.datasource.username=cloud_user
spring.datasource.password=xxx

7.2 启动 seata-server 服务

seata 需要一个 seata-server 作为分布式事务服务端。

7.3 启动服务

启动 Account 服务、Business 服务、Order 服务和 Stock 服务:

启动成功后,会初始化以下数据。初始化逻辑定义在 business-xa 服务中。

账户(account_tbl):新建一个账户,余额为 10000。

库存(stock_tbl):新建一个商品,库存为 100。

以上这些服务启动成功后,会在 seata-server 中注册并打印出相关日志:

7.4 触发购买流程

(1)正常执行

打开浏览器,访问 http://127.0.0.1:8084/purchase 触发购买流程。

利用以下语句来查询相关数据:

-- 账户
SELECT * FROM account_tbl; -- 订单
SELECT * FROM order_tbl; -- 库存
SELECT * FROM stock_tbl;

账户余额变为 7000(10000-3000):

订单的商品数量为 30 个,总金额为 3000:

库存量变为 70 个(100 -30):

这是正常执行的情况。

(2)异常执行

重启 businsess-xa 服务,重新初始化数据。

执行:http://127.0.0.1:8084/purchase?rollback=true,强制让 businsess-xa 服务方法抛出异常,这样所有的分布式事务全部回滚。

账户余额保持不变:

库存保持不变:

最终测试结果:

参考资料

https://github.com/seata/seata-samples/tree/master/seata-xa


  1. https://blog.csdn.net/qq360694660/article/details/82877222

Seata XA 模式示例分析的更多相关文章

  1. Seata AT 模式启动源码分析

    从上一篇文章「分布式事务中间件Seata的设计原理」讲了下 Seata AT 模式的一些设计原理,从中也知道了 AT 模式的三个角色(RM.TM.TC),接下来我会更新 Seata 源码分析系列文章. ...

  2. 微服务架构 | 11.1 整合 Seata AT 模式实现分布式事务

    目录 前言 1. Seata 基础知识 1.1 Seata 的 AT 模式 1.2 Seata AT 模式的工作流程 1.3 Seata 服务端的存储模式 1.4 Seata 与 Spring Clo ...

  3. 基于Java 生产者消费者模式(详细分析)

    Java 生产者消费者模式详细分析 本文目录:1.等待.唤醒机制的原理2.Lock和Condition3.单生产者单消费者模式4.使用Lock和Condition实现单生产单消费模式5.多生产多消费模 ...

  4. 分布式事务(Seata) 四大模式详解

    前言 在上一节中我们讲解了,关于分布式事务和seata的基本介绍和使用,感兴趣的小伙伴可以回顾一下<别再说你不知道分布式事务了!> 最后小农也说了,下期会带给大家关于Seata中关于sea ...

  5. 三种工厂模式的分析以及C++实现

    三种工厂模式的分析以及C++实现 以下是我自己学习设计模式的思考总结. 简单工厂模式 简单工厂模式是工厂模式中最简单的一种,他可以用比较简单的方式隐藏创建对象的细节,一般只需要告诉工厂类所需要的类型, ...

  6. ngRx 官方示例分析 - 3. reducers

    上一篇:ngRx 官方示例分析 - 2. Action 管理 这里我们讨论 reducer. 如果你注意的话,会看到在不同的 Action 定义文件中,导出的 Action 类型名称都是 Action ...

  7. ngRx 官方示例分析 - 2. Action 管理

    我们从 Action 名称开始. 解决 Action 名称冲突问题 在 ngRx 中,不同的 Action 需要一个 Action Type 进行区分,一般来说,这个 Action Type 是一个字 ...

  8. ngRx 官方示例分析 - 1. 介绍

    ngRx 的官方示例演示了在具体的场景中,如何使用 ngRx 管理应用的状态. 示例介绍 示例允许用户通过查询 google 的 book  API  来查询图书,并保存自己的精选书籍列表. 菜单有两 ...

  9. ROS_Kinetic_29 kamtoa simulation学习与示例分析(一)

    致谢源代码网址:https://github.com/Tutorgaming/kamtoa-simulation kamtoa simulation学习与示例分析(一) 源码学习与分析是学习ROS,包 ...

随机推荐

  1. iOS中属性 (nonatomic, copy, strong, weak)的使用 By hL

    以下内容来自Stackflow的详解 1.Nonatomicnonatomic is used for multi threading purposes. If we have set the non ...

  2. Node.js躬行记(15)——活动规则引擎

    在日常的业务开发中,会包含许多的业务规则,一般就是用if-else硬编码的方式实现,这样就会增加逻辑的维护成本,若无注释,可能都无法理解规则意图. 因为一旦规则有所改变,那么就需要修改代码再发布代码, ...

  3. opencv笔记---contours

    一 Contour Finding Contours使用 STL-style vector<> 表示,如 vector<cv::Point>, vector<cv::Po ...

  4. unity3d导出xcode项目使用afnetworking 3框架导致_kUTTagClassMIMEType 问题解决方案

    http://blog.csdn.net/huayu_huayu/article/details/51781953  (参考链接) Undefined symbols for architecture ...

  5. 技术管理进阶——为什么Leader的话有时候你听不懂

    原创不易,求分享.求一键三连 Hi,各位亲爱的小伙伴,小钗公号遵循日复盘->周复盘->月复盘->季度复盘->年总结策略,所以某类型文章到后期才会成体系. 今天这篇文章属于「月复 ...

  6. Python 单例模式的几种实现方式

    单例模式的几种实现方式 先来看几个魔法方法的简单运用:__new__, __init__, __call__. class A(object): def __init__(self, x): prin ...

  7. 60天shell脚本计划-12/12-渐入佳境

    --作者:飞翔的小胖猪 --创建时间:2021年3月23日 --修改时间:2021年3月27日 说明 每日上传更新一个shell脚本,周期为60天.如有需求的读者可根据自己实际情况选用合适的脚本,也可 ...

  8. 像追女神一样学好java~

    写在前面的话 ● 本文适合食用的观众大老爷和小建议: ----本文内容主要是围绕java这门语言展开~ 适合的食用的大老爷们: ★ 第一类:完全没学过其他编程语言入门java的小白 ★ 第二类:已经学 ...

  9. nodejs调用jar

    目前nodejs调用jar主要有两种方式: 通过创建子进程运行java -jar命令调用包含main方法的jar 使用node-java通过c++桥接调用jar 方法一(子进程运行): const { ...

  10. Java基本规范

    1.Java是区分大小写的语言,关键字的大小不能写错,例如把class写成Class或者CLASS,都会导致出错. 2.在一个类的内部不能定义其他的类,即类和类之间是平行而非嵌套的关系. 3.一个程序 ...