前言

  在上一篇博文(分布式事务与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. Linux Shell 统计一(行\列)数值的总和及行、列转换

    (对一列数字求和) 在日常工作当中需要对文本过滤出来的数字进行求和运算,例如想统计一个MySQL分区表现在有多大 # ls -lsh AdPlateform#P#p*.ibd  |grep G 2.6 ...

  2. 8、Spring教程之静态代理/动态代理

    为什么要学习代理模式,因为AOP的底层机制就是动态代理! 代理模式: 静态代理 动态代理 学习aop之前 , 我们要先了解一下代理模式! 静态代理 静态代理角色分析 抽象角色 : 一般使用接口或者抽象 ...

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

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

  4. SqlServer游标的创建与使用

    前言 大家都对SqlServer视图.存储过程.触发器的创建与使用有一定的了解了,我们来看下什么是游标,怎么使用,什么时候用. SqlServer视图的创建与使用 SqlServer存储过程的创建与使 ...

  5. 【JVM进阶之路】十:JVM调优总结

    1.调优原则 JVM调优听起来很高大上,但是要认识到,JVM调优应该是Java性能优化的最后一颗子弹. 比较认可廖雪峰老师的观点,要认识到JVM调优不是常规手段,性能问题一般第一选择是优化程序,最后的 ...

  6. Kotlin编写Processing程序(使用函数式编程思维和面向接口方式)

    写一例Kotlin编写的Processing程序,充分调用函数式编程思维和面向接口的编程思维,供自己和读者参考学习. 初衷 想要实现一行行的文字排版功能,每一行作为一个单位,可制定显示的位置.大小.文 ...

  7. (十三)struts2的输入校验

    输入校验是web应用必须处理的问题,要防止用户的误输入和恶意非法输入.struts2给我们提供了非常强大的输入校验体系. 输入校验分为客户端校验和服务器端校验.一般开发中两者都用,但是服务端校验必须使 ...

  8. JMeter自定义采样器插件开发

    JMeter自定义采样器插件开发 目录 JMeter自定义采样器插件开发 1. 简介 2. 需求简介 3.成品展示 成功展示 失败展示 4. 准备开发环境 4.1 准备pom文件 4.2 新建Java ...

  9. NumPy之:NumPy简介教程

    目录 简介 安装NumPy Array和List 创建Array Array操作 sort concatenate 统计信息 reshape 增加维度 index和切片 从现有数据中创建Array 算 ...

  10. .NET6 平台系列1 .NET发展史之.NET Framework简介

    系列目录     [已更新最新开发文章,点击查看详细] 自1995年互联网战略日以来最雄心勃勃的事业 -- 微软.NET战略, 2000年6月30日. 微软公司于2002年2月13日正式推出第一代.N ...