Seata XA 模式示例分析
@
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 角色之间的交互方式如下:
- TM 向 AP 提供应用程序编程接口, AP 通过 TM 提交和回滚事务。
- 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 ... ");
}
}
具体步骤如下:
- purchase 方法标注了 @GlobalTransactional,表示开启全局事务。
- 利用
RootContext.getXID()
来获取事务 ID。 - 调用库存服务
stockFeignClient.deduct(commodityCode, orderCount)
,扣减商品库存。 - 调用订单服务
orderFeignClient.create(userId, commodityCode, orderCount)
,创建商品订单。 - 如果是强制回滚模式,就直接抛出 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]。
初始化数据逻辑如下:
- 删除账户表、订单表与库存表中的数据。
- 插入一条账户信息,账户金额为 10000。
- 插入一条库存信息,库存量为 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});
}
具体步骤如下:
- 利用
RootContext.getXID()
来获取事务 ID。 - 扣减商品的库存数量。
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. ");
}
}
具体步骤如下:
- 利用
RootContext.getXID()
来获取事务 ID。一般用于记录日志。 - 计算定单总价,公式为订购数量(count) * 商品单价(100)。这里的商品单价写死在代码中。
- 创建一条订单。
- 调用账户服务扣减账户余额。
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 ...");
}
}
处理逻辑如下:
- 使用 @Transactional 来包裹该方法。
- 利用
RootContext.getXID()
来获取事务 ID。 - 扣减账户剩余金额。
- 查询当前账户剩余金额并返回结果。
以上四个服务(业务服务、库存服务、订单服务、账户服务),只有这个方法标注了 @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
Seata XA 模式示例分析的更多相关文章
- Seata AT 模式启动源码分析
从上一篇文章「分布式事务中间件Seata的设计原理」讲了下 Seata AT 模式的一些设计原理,从中也知道了 AT 模式的三个角色(RM.TM.TC),接下来我会更新 Seata 源码分析系列文章. ...
- 微服务架构 | 11.1 整合 Seata AT 模式实现分布式事务
目录 前言 1. Seata 基础知识 1.1 Seata 的 AT 模式 1.2 Seata AT 模式的工作流程 1.3 Seata 服务端的存储模式 1.4 Seata 与 Spring Clo ...
- 基于Java 生产者消费者模式(详细分析)
Java 生产者消费者模式详细分析 本文目录:1.等待.唤醒机制的原理2.Lock和Condition3.单生产者单消费者模式4.使用Lock和Condition实现单生产单消费模式5.多生产多消费模 ...
- 分布式事务(Seata) 四大模式详解
前言 在上一节中我们讲解了,关于分布式事务和seata的基本介绍和使用,感兴趣的小伙伴可以回顾一下<别再说你不知道分布式事务了!> 最后小农也说了,下期会带给大家关于Seata中关于sea ...
- 三种工厂模式的分析以及C++实现
三种工厂模式的分析以及C++实现 以下是我自己学习设计模式的思考总结. 简单工厂模式 简单工厂模式是工厂模式中最简单的一种,他可以用比较简单的方式隐藏创建对象的细节,一般只需要告诉工厂类所需要的类型, ...
- ngRx 官方示例分析 - 3. reducers
上一篇:ngRx 官方示例分析 - 2. Action 管理 这里我们讨论 reducer. 如果你注意的话,会看到在不同的 Action 定义文件中,导出的 Action 类型名称都是 Action ...
- ngRx 官方示例分析 - 2. Action 管理
我们从 Action 名称开始. 解决 Action 名称冲突问题 在 ngRx 中,不同的 Action 需要一个 Action Type 进行区分,一般来说,这个 Action Type 是一个字 ...
- ngRx 官方示例分析 - 1. 介绍
ngRx 的官方示例演示了在具体的场景中,如何使用 ngRx 管理应用的状态. 示例介绍 示例允许用户通过查询 google 的 book API 来查询图书,并保存自己的精选书籍列表. 菜单有两 ...
- ROS_Kinetic_29 kamtoa simulation学习与示例分析(一)
致谢源代码网址:https://github.com/Tutorgaming/kamtoa-simulation kamtoa simulation学习与示例分析(一) 源码学习与分析是学习ROS,包 ...
随机推荐
- 网管必须必须知道的知识!ARP攻击与欺骗的原理!
ARP攻击与ARP欺骗原理及应用 1.ARP概述以及攻击原理 2.ARP欺骗原理 3.ARP故障处理 1.什么是ARP协议?将一个已知的IP地址解析成MAC地址.无论是ARP攻击还是ARP欺骗,它们都 ...
- Oracle - Trunc() 函数截取日期&截取数值
Oracle TRUNC函数可以截取数字和日期类型:截取日期:select to_char(sysdate,'yyyy-mm-dd hh24:mi:ss') from dual; --显示当前时间 s ...
- JTAG 标准IEEE STD 1149.1-2013学习笔记(一·)Test logic architecture、Instruction register以及Test data registers
我是 雪天鱼,一名FPGA爱好者,研究方向是FPGA架构探索和SOC设计. 关注公众号[集成电路设计教程],拉你进"IC设计交流群". 注:转载请注明出处 一.Test logic ...
- Solution -「AGC 019F」「AT 2705」Yes or No
\(\mathcal{Description}\) Link. 有 \(n+m\) 个问题,其中 \(n\) 个答案为 yes,\(m\) 个答案为 no.每次你需要回答一个问题,然后得知这个 ...
- 通过修改注册表将右alt键映射为application键
通过修改注册表将右alt键映射为application键的方法有许多键盘没有APPLICATION(上下文菜单)键,本文将教您如何把右ALT键映射为apps键.1.映射请将以下注册表信息用记事本保存为 ...
- CoRR 2015 | MXNet: A Flexible and Efficient Machine Learning Library for Heterogeneous Distributed Systems
MXNet是一个支持多种编程语言的机器学习库,使用MXNet可以方便地实现机器学习算法,尤其是深度神经网络.通过嵌入在宿主语言中,它将声明式符号表达与命令式张量计算相结合.它提供自动求导以计算梯度.M ...
- 【Kotlin】初识Kotlin(二)
[Kotlin]初识Kotlin(二) 1.Kotlin的流程控制 流程控制是一门语言中最重要的部分之一,从最经典的if...else...,到之后的switch,再到循环控制的for循环和while ...
- ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法
一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...
- 报表工具和BI商业智能的区别,你真的弄清楚了吗?
许多人在投身大数据行业的时候,肯定会听到的两个词就是"报表工具"和"BI商业智能".但是大部分人并不太清楚这两者之间的概念和区别,认为报表就是BI,BI就是报表 ...
- 太骚了,用Excel玩机器学习
最近发现了一个好玩的Python库,它可以将训练好的机器学习模型转换为Java.C.JavaScript.Go.Ruby,VBA 本地代码,可以让连Python和机器学习一无所知的同学也能感受预测的神 ...