前言

  在上一篇博文(分布式事务与Seate框架(1)——分布式事务理论)中了解了足够的分布式事务的理论知识后,到了实践部分,在工作中虽然用到了Seata,但是自己却并没有完全实践过,所以自己私下花点时间实践以加深理解,实际上在实践过程中遇到了很多的坑(比如Seata与SpringCloudAlibaba的整合中版本兼容性问题,是个很让人头疼的一块,甚至专门去github提过issue),有时候甚至得跟踪源码进行分析,这也使我加强了对阅读源码的能力。总之都是要code的。本篇博文主要结合实践深入讲解Seata AT模式!

  参考资料《Spring Cloud Alibaba 微服务原理与实战》(PDF电子书资源,有需要的小伙伴可以评论私信我)、官方wiki

  博文中源码已上传至github(https://github.com/Jian0110/learning-cloudalibaba),欢迎小伙伴们star...


一、实践准备工作

1、框架介绍

实践主要是以“订单-库存-账户”系统演示,主要的框架图如下,图中各个部分充当的分布式事务角色已标明。

    

  具体流程:

  1)用户登录XXX商品购物系统(假设已有账户),

  2)点击购买某个商品,发起创建订单请求;

  3)检查购买商品的库存量,如果不够则创建订单失败提示库存不足;否则锁定该商品---->减少库存--->创建订单;

  4)订单创建成功后点击付款(或直接付款无需点击,实际上整个Demo中下单之后模拟立马支付,并不会点击付款);

  5)如果购买成功则对账户进行余额进行判断,余额足够则进行减扣,余额不够则进行提示说明

  6)返回购买成功失败提示说明。

2、项目结构

项目结构如下:

mvn package打包运行seata服务,即运行TC服务器(这里只展示单机)

初始化Seata库,导入sql脚本

二、代码实践

这里只展示关键代码,全部代码已提交gituhb:,有需要的小伙伴可以自行获取

1、“订单-库存-账户”服务

订单服务:

    TM(microService):seata-order-service

    RM(DB Resources):jdbc:mysql://127.0.0.1:3306/order

OrderService:

@GlobalTransactional // TM开启全局事务
@Transactional(rollbackFor = Exception.class)
public void createOrder(Long productId, BigDecimal price){
// 这里模拟获取的是用户的账户ID
// 通过上下文获取userId再获取accountId(单个账户)
Long accountId = 1L; // 假设已经获取到了账户ID // 1.rpc调用库存微服务检查库存并减库存操作
Boolean deductStorageSuccess = storageFeignClient.deduct(productId);
if (!deductStorageSuccess) {
throw new RuntimeException("storage deduct failed!");
}
// 2.创建订单
ProductOrder order = ProductOrder.builder()
.productId(productId)
.accountId(accountId)
.payAmount(price)
.build();
log.info("create order : {}", order);
// 这里为了模拟回滚,所以先对价格的判断放到了创建订单之后,抛出runtime exception
if (price.compareTo(BigDecimal.ZERO) < 0) {
throw new NumberFormatException("product price must greater than zero!");
}
orderMapper.insertSelective(order); // 3.rpc调用账户微服务对余额检查并扣款操作
Boolean deductAccountSuccess = accountFeignClient.deduct(accountId, price);
if (!deductAccountSuccess) {
throw new RuntimeException("account deduct failed!");
}
// 4. 反馈结果
}

OrderController:

 /**
* 模拟创建订单
* @param productId
* @param price
* @return
*/
@PostMapping("/create")
public String create(Long productId, BigDecimal price){
try {
orderService.createOrder(productId, price);
} catch (Exception e) {
log.error("order failed: ", e);
return "order failed";
}
return "order success";
}

调用的Feign:

@FeignClient(name="seata-account-service")
public interface AccountFeignClient {
@PostMapping("/account/deduct")
Boolean deduct(@RequestParam("accountId") Long accountId, @RequestParam("payAmount") BigDecimal payAmount);
} @FeignClient(name="seata-storage-service")
public interface StorageFeignClient {
@PostMapping("/storage/deduct")
Boolean deduct(@RequestParam("productId") Long productId);
}

库存服务:

    microService:seata-storage-service

    RM(DB Resources):jdbc:mysql://127.0.0.1:3306/storage

StorageService

public Boolean deduct(Long productId){
// 这里先检查有没有库存了, 生产环境下这里是需要for update数据库锁,或者分布式锁
Repo repoFromDB = repoMapper.selectByPrimaryKey(productId);
if (repoFromDB == null) {
throw new RuntimeException("product not exist!");
}
// 对库存减一
int afterCount = repoFromDB.getAmount()-1;
// 没有库存剩余了
if (afterCount < 0) {
throw new RuntimeException("product storage is no remaining!");
}
Repo repo = Repo.builder()
.id(productId)
.amount(afterCount)
.build();
repoMapper.updateAmount(repo);
log.info("deduct product[{}] storage, current amount is {}", productId, afterCount);
return true;
}

StorageController:

/**
* 模拟对商品库存减一
* @param productId
* @return
*/
@PostMapping("/deduct")
public Boolean deduct(Long productId){
try {
storageService.deduct(productId);
} catch (Exception e) {
return false;
}
return true;
}

账户服务

    microService:seata-account-service

    RM(DB Resources):jdbc:mysql:127.0.0.1/account

AccountService:

public void deduct(Long accountId, BigDecimal payAmount){
// 这里先检查有没有账户存在, 生产环境下这里是需要for update数据库锁,或者分布式锁
UserAccount userAccountFromDB = userAccountMapper.selectByPrimaryKey(accountId);
if (userAccountFromDB == null) {
throw new RuntimeException("account not exist!");
}
// 检查余额是否足够
BigDecimal afterBalance = userAccountFromDB.getBalance().subtract(payAmount);
if (afterBalance.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException("the balance is not enough!");
}
UserAccount userAccount = UserAccount.builder()
.id(accountId)
.balance(afterBalance)
.build();
log.info("deduct account[{}] , current balance is {}", accountId, afterBalance);
userAccountMapper.updateBalance(userAccount);
}

AccountController:

/**
* 模拟账户扣款
* @param accountId
* @param payAmount
* @return
*/
@PostMapping("/deduct")
public Boolean deduct(Long accountId, BigDecimal payAmount){
try {
accountService.deduct(accountId, payAmount);
} catch (Exception e) {
return false;
}
return true;
}

2、Seata服务器,即TC角色

  首先初始化seata的sql脚本(sql脚本参考官方wiki),并开启seata库,之后开启Seata Server(具体的配置与启动前nacos配置,事务分组等相关概念请参考官方wiki)

    

3、检查Nacos服务与配置列表

  微服务模块启动后快速注册到dev命名空间下的SEATA_GROUP分组,此时TM、RM、TC都已经具备        

启动微服务模块后可以看到日志输出,说明启动成功并且已经成功注册

RM will register :jdbc:mysql://127.0.0.1:3306/account
nacos registry, SEATA_GROUP seata-account-service 192.168.99.1:6009 register finished
Started SeataAccountApplication in 30.115 seconds (JVM running for 33.158)
.......
NettyPool create channel to transactionRole:TMROLE,address:169.254.6.29:8091,msg:< RegisterTMRequest{applicationId='seata-account-service', transactionServiceGroup='my_test_tx_group'} >
register TM success. client version:1.4.0, server version:1.4.0,channel:[id: 0xa77dc065, L:/169.254.6.29:52794 - R:/169.254.6.29:8091]
register success, cost 4 ms, version:1.4.0,role:TMROLE,channel:[id: 0xa77dc065, L:/169.254.6.29:52794 - R:/169.254.6.29:8091]

三、运行测试

1、模拟购买支付成功情况

运行启动所有的微服务后,在TC Serve的日志可以看到所有的TM、RM都已经注册了

此时productId=1库存还剩998

accountId=1的用户余额还剩1000元

接下来就是模拟用户购买商品环节,调用http://localhost:6008/order/create,表示用户想买商品ID=1,价格为12.25的商品

清空日志,并发起请求查看日志:

16:10:45.167  INFO --- [rverHandlerThread_1_4_500] i.s.s.coordinator.DefaultCoordinator     : Begin new global transaction applicationId: seata-order-service,transactionServiceGroup: my_test_tx_group, transactionName: createOrder(java.lang.Long, java.math.BigDecimal),timeout:60000,xid:169.254.6.29:8091:136139747123908608
16:10:45.964 INFO --- [ batchLoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : SeataMergeMessage xid=169.254.6.29:8091:136139747123908608,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/storage,lockKey=repo:1
,clientIp:169.254.6.29,vgroup:my_test_tx_group
16:10:46.086 INFO --- [rverHandlerThread_1_5_500] i.seata.server.coordinator.AbstractCore : Register branch successfully, xid = 169.254.6.29:8091:136139747123908608, branchId = 136139750928142336, resourceId = jdbc:mysql://127.0.0.1:3306/storage ,lockKeys = repo:1
16:10:46.788 INFO --- [ batchLoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : SeataMergeMessage xid=169.254.6.29:8091:136139747123908608,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/account,lockKey=user_account:1
,clientIp:169.254.6.29,vgroup:my_test_tx_group
16:10:46.918 INFO --- [rverHandlerThread_1_6_500] i.seata.server.coordinator.AbstractCore : Register branch successfully, xid = 169.254.6.29:8091:136139747123908608, branchId = 136139754342305793, resourceId = jdbc:mysql://127.0.0.1:3306/account ,lockKeys = user_account:1
16:10:47.015 INFO --- [ batchLoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : xid=169.254.6.29:8091:136139747123908608,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/order,lockKey=product_order:6,clientIp:169.254.6.29,vgroup:my_test_tx_group
16:10:47.073 INFO --- [rverHandlerThread_1_7_500] i.seata.server.coordinator.AbstractCore : Register branch successfully, xid = 169.254.6.29:8091:136139747123908608, branchId = 136139755294412801, resourceId = jdbc:mysql://127.0.0.1:3306/order ,lockKeys = product_order:6
16:10:47.184 INFO --- [ batchLoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : xid=169.254.6.29:8091:136139747123908608,extraData=null,clientIp:169.254.6.29,vgroup:my_test_tx_group
16:10:48.084 INFO --- [ AsyncCommitting_1_1] io.seata.server.coordinator.DefaultCore : Committing global transaction is successfully done, xid = 169.254.6.29:8091:136139747123908608.
16:10:53.908 INFO --- [ TxTimeoutCheck_1_1] i.s.s.coordinator.DefaultCoordinator : Global transaction[169.254.6.29:8091:136139530647490560] is timeout and will be rollback.
16:10:54.947 INFO --- [ RetryRollbacking_1_1] io.seata.server.coordinator.DefaultCore : Rollback global transaction successfully, xid = 169.254.6.29:8091:136139530647490560.

从日志中我们可以看到:

1)全局事务XID已经生成,各个分支注册成功,

2)branchId也已经生成并在全局事务XID下,资源已被锁住

3)全局事务提交成功

查看此时的库存与余额,都已经进行了减扣

2、模拟库存不足情况

修改productId=1的商品库存为0:

再次发起请求,查看TC Server日志,可以查出明显发生了全局事务的回滚

16:20:24.258  INFO --- [verHandlerThread_1_12_500] i.s.s.coordinator.DefaultCoordinator     : Begin new global transaction applicationId: seata-order-service,transactionServiceGroup: my_test_tx_group, transactionName: createOrder(java.lang.Long, java.math.BigDecimal),timeout:60000,xid:169.254.6.29:8091:136142176250875904
16:20:24.279 INFO --- [ batchLoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : xid=169.254.6.29:8091:136142176250875904,extraData=null,clientIp:169.254.6.29,vgroup:my_test_tx_group
16:20:24.420 INFO --- [verHandlerThread_1_13_500] io.seata.server.coordinator.DefaultCore : Rollback global transaction successfully, xid = 169.254.6.29:8091:136142176250875904.

查看库存与余额情况,库存仍然是0,余额仍然是987.75

3、模拟余额不足情况

修改accountId=1的账户余额小于12.25

再次发起请求,查看日志

16:27:41.811  INFO --- [verHandlerThread_1_14_500] i.s.s.coordinator.DefaultCoordinator     : Begin new global transaction applicationId: seata-order-service,transactionServiceGroup: my_test_tx_group, transactionName: createOrder(java.lang.Long, java.math.BigDecimal),timeout:60000,xid:169.254.6.29:8091:136144011456008192
16:27:41.836 INFO --- [ batchLoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : SeataMergeMessage xid=169.254.6.29:8091:136144011456008192,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/storage,lockKey=repo:1
,clientIp:169.254.6.29,vgroup:my_test_tx_group
16:27:41.889 INFO --- [verHandlerThread_1_15_500] i.seata.server.coordinator.AbstractCore : Register branch successfully, xid = 169.254.6.29:8091:136144011456008192, branchId = 136144011762192385, resourceId = jdbc:mysql://127.0.0.1:3306/storage ,lockKeys = repo:1
16:27:42.088 INFO --- [ batchLoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : xid=169.254.6.29:8091:136144011456008192,extraData=null,clientIp:169.254.6.29,vgroup:my_test_tx_group
16:27:42.632 INFO --- [verHandlerThread_1_16_500] io.seata.server.coordinator.DefaultCore : Rollback branch transaction successfully, xid = 169.254.6.29:8091:136144011456008192 branchId = 136144011762192385
16:27:42.754 INFO --- [verHandlerThread_1_16_500] io.seata.server.coordinator.DefaultCore : Rollback global transaction successfully, xid = 169.254.6.29:8091:136144011456008192.

不同于库存不足的情况的是,这里库存服务分支事务是先注册TC Server的,因为有异常的并不是库存服务,需要注意的是因为我模拟的是下单之后立马支付,支付失败的话订单也是不会存在,实际生活中应该是订单显示“支付失败”。

查看库存与余额情况,库存仍然是997,余额仍然是10.75

分布式事务与Seate框架(2)——Seata实践的更多相关文章

  1. 分布式事务与Seate框架(3)——Seata的AT模式实现原理

    前言 在上两篇博文(分布式事务与Seate框架(1)--分布式事务理论.分布式事务与Seate框架(2)--Seata实践)中已经介绍并实践过Seata AT模式,这里一些例子与概念来自这两篇(特别是 ...

  2. 分布式事务与Seate框架(1)——分布式事务理论

    前言 虽然在实际工作中,由于公司与项目规模限制,实际上所谓的微服务分布式事务都不会涉及,更别提单独部署构建Seata集群.但是作为需要不断向前看的我,还是有必要记录下相关的分布式事务理论与Seate框 ...

  3. 分布式事务(七)之Seata简介

    在前面的文章中,我们介绍了分布式事务的概念以及一些解决方案.fenSeata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务.Seata将为用户提供了AT.TCC.SAGA和 ...

  4. Spring Cloud同步场景分布式事务怎样做?试试Seata

    一.概述 在微服务架构下,虽然我们会尽量避免分布式事务,但是只要业务复杂的情况下这是一个绕不开的问题,如何保证业务数据一致性呢?本文主要介绍同步场景下使用Seata的AT模式来解决一致性问题. Sea ...

  5. 分布式事务解决方案,中间件 Seata 的设计原理详解

    作者:张乘辉 前言 在微服务架构体系下,我们可以按照业务模块分层设计,单独部署,减轻了服务部署压力,也解耦了业务的耦合,避免了应用逐渐变成一个庞然怪物,从而可以轻松扩展,在某些服务出现故障时也不会影响 ...

  6. 开发者说 | 分布式事务中间件 Seata 的设计原理

    导读 微服务架构体系下,我们可以按照业务模块分层设计,单独部署,减轻了服务部署压力,也解耦了业务的耦合,避免了应用逐渐变成一个庞然怪物,从而可以轻松扩展,在某些服务出现故障时也不会影响其它服务的正常运 ...

  7. 分析 5种分布式事务方案,还是选了阿里的 Seata(原理 + 实战)

    好长时间没发文了,最近着实是有点忙,当爹的第 43 天,身心疲惫.这又赶上年底,公司冲 KPI 强制技术部加班到十点,晚上孩子隔两三个小时一醒,基本没睡囫囵觉的机会,天天处于迷糊的状态,孩子还时不时起 ...

  8. 微服务痛点-基于Dubbo + Seata的分布式事务(TCC模式)

    前言 Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务.Seata 将为用户提供了 AT.TCC.SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案. ...

  9. 浅谈,seata在使用feign-url通过域名调用时分布式事务不生效的问题及解决

    浅谈,seata在使用feign-url通过域名调用时分布式事务不生效的问题及解决 ​ 在前几个月时,我们项目出现了分布式事务的问题,那么什么是分布式事务问题呢,简单的说,我们有俩服务A和B,它们对应 ...

随机推荐

  1. java 动态规划解决上楼梯问题

    问题描述: 你正在爬楼梯. 它需要n步才能达到顶峰. 每次你可以爬1或2步. 您可以通过多少不同的方式登顶? 注意:给定n将是一个正整数. Example 1: Input: 2 Output: 2 ...

  2. python 集合详解

    字符串 一个个字符组成的有序的序列,时字符的集合 使用单引,双引,三引 引住的字符序列 字符时不可变的对象 bytes定义 bytes不可变字节序列 使用b前缀定义 只允许基本ASCII使用字符形式 ...

  3. 让你弄懂js中的闭包

    目录 闭包 闭包如何产生 闭包是什么 常见的闭包 闭包的作用 闭包的生命周期 闭包的应用 闭包的缺点 内存泄露 内存溢出 闭包面试题 闭包 之前在我执行上下文执行上下文栈这篇文章中,出现了这样一个题目 ...

  4. 清明节特辑 |记忆存储、声音还原、性格模仿……AI可以让人类永生吗?

    摘要:如果能用AI "复活"逝去的亲人 你愿意吗? 清明节,很少有人会去特地想这样一个问题:我们为什么要给过世的人修墓,然后每年固定的时间去扫墓?当农耕文化的色彩褪去,清明节的祭祀 ...

  5. 【SqlServer】管理全文索引(FULL TEXT INDEX)

    Sql Server中的全文索引(下面统一使用FULLTEXT INDEX来表示全文索引),是一种特定语言搜索索引功能.它和LIKE的不一样,LIKE主要是根据搜索模板搜索数据,它的效率比FULLTE ...

  6. Golang 基于Prometheus Node_Exporter 开发自定义脚本监控

    Golang 基于Prometheus Node_Exporter 开发自定义脚本监控 公司是今年决定将一些传统应用从虚拟机上迁移到Kubernetes上的,项目多而乱,所以迁移工作进展缓慢,为了建立 ...

  7. 使用ffmpeg 操作音频文件前后部分静音移除.

    指令特别简单, 但是却琢磨了一下午. 总结看文档时要细心, 主要ffmpeg的版本要 8.2.1 以上 ffmpeg -i in.mp3 -af silenceremove=start_periods ...

  8. 第一个真正的 GUI 程序——Tkinter教程系列02

    第一个真正的 GUI 程序--Tkinter教程系列02 前言 欢迎光临我的个人博客 chens.life Tk 系列教程: Tkinter教程系列01--引言和安装Tk 我们将编写一个英尺和米的转换 ...

  9. 【DB宝47】企业知识分享+团队协作神器之Confluence

    目录 一.Confluence简介 二.知识库软件对比 三.快速安装confluence 7.4.6版本 四.confluence基本操作简介 4.1.创建空间(Space) 4.2.配置空间权限 4 ...

  10. Dynamics CRM实体系列之字段

    本节开始讲实体中的基础数据存储对象,也就是字段. Dynamics CRM目前总共有13种字段类型,分别为单行文本.选项集.多选选项集.两个选项.图像.整数.浮点数.十进制数.货币.多行文本.日期和时 ...