分布式事务学习条记(五)微服务实现Seata TCC模式、TC服务器高可用异地容灾 ...

打印 上一主题 下一主题

主题 857|帖子 857|积分 2571

前言

分布式事务学习条记(一)分布式事务题目、CAP定理、BASE理论、Seata
分布式事务学习条记(二)Seata架构、TC服务器部署、微服务集成Seata
分布式事务学习条记(三)微服务实现Seata XA模式
分布式事务学习条记(四)微服务实现Stata AT模式、Stata Saga模式介绍
6 Seata TCC 模式

6.1 实现原理

TCC 模式是 Seata 支持的一种由业务方细粒度控制的侵入式分布式事务办理方案,其架构如下图:

TCC 模式包含两个阶段,分别是:


  • 阶段一(Try):资源检测与预留阶段
  • 阶段二(Confirm):预留资源确认阶段
  • 阶段二(Cancel):预留资源释放阶段
以一个扣减用户余额的业务为例。假设账户A原本的余额为100,现在需要扣减30。


  • 阶段一(Try):检查余额是否充足,如果充足则冻结金额增加30,可用余额扣减30,总数还是100。完成后事务直接提交,无需等候。



  • 阶段二(Confirm):如果是确认提交操作,则冻结金额扣减30,可用余额不变,此时就只剩下可用余额70。



  • 阶段二(Cancel):如果是回滚操作,则冻结金额扣减30,可用余额增加30,规复到初始状态。

6.2 优缺点

TCC的长处:


  • 一阶段完成后直接提交事务,释放数据库资源,性能好
  • 相比AT模型,无需天生快照,无需使用全局锁,性能更强
  • 不依赖数据库事务,而是依赖业务补偿操作,可以用于非事务型数据库,且可以灵活选择业务资源的锁定粒度
TCC的缺点:


  • 有代码侵入,需要人为编写try、Confirm和Cancel接口,实现复杂
  • 有软状态,事务是最终一致
  • 需要考虑Confirm和Cancel的失败环境,实现贫苦
6.3 空回滚和业务悬挂

6.3.1 空回滚

当某分支事务的Try阶段壅闭时,可能导致全局事务超时而触发二阶段的Cancel操作。在未执行Try操作时先执行了Cancel操作,这时Cancel就不能做回滚,就是空回滚

因此,在执行Cancel操作时,应当判定Try是否已经执行,如果尚未执行,则应该空回滚。
6.3.2 业务悬挂

对于已经空回滚的业务,之前被壅闭的Try操作规复,继续执行Try,但已经永久不可能继续执行Confirm或Cancel操作,事务不停处于中心状态,这就是业务悬挂
因此,在执行Try操作时,应当判定Cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的Try操作,制止悬挂。
6.4 微服务实现TCC模式

6.4.1 思绪分析

要办理空回滚和业务悬挂题目,就必须要记录当前事务状态是在Try还是Cancel阶段。为此,可以在数据库定义一张表来记录:
  1. CREATE TABLE `t_account_freeze` (
  2. `xid` varchar(128) NOT NULL COMMENT '全局事务id',
  3. `user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
  4. `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
  5. `state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
  6. PRIMARY KEY (`xid`) USING BTREE
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
复制代码
业务逻辑分析如下:


  • Try业务

    • 记录冻结金额和事务状态0到 t_account_freeze 表
    • 扣减 t_account 表可用余额

  • Confirm业务

    • 根据xid删除 t_account_freeze 表的冻结记录

  • Cancel业务

    • 修改 t_account_freeze 表冻结金额为0,state为2
    • 修改 t_account 表,规复可用金额

  • 如何判定是否空回滚?

    • Cancel业务中,根据xid查询 t_account_freeze,如果为null则说明Try业务还没做,需要空回滚

  • 如何制止业务悬挂?

    • Try业务中,根据xid查询 t_account_freeze,如果已经存在则证明Cancel业务已经执行,拒绝执行Try业务

6.4.2 声明TCC接口

在jd-account-service微服务的com.hsgx.account.service包下新建一个AccountTCCService接口:
  1. @LocalTCC
  2. public interface AccountTCCService {
  3.     @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
  4.     void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
  5.                 @BusinessActionContextParameter(paramName = "money")int money);
  6.     boolean confirm(BusinessActionContext ctx);
  7.     boolean cancel(BusinessActionContext ctx);
  8. }
复制代码
6.4.3 编写实现类

在jd-account-service微服务的com.hsgx.account.service.impl包下新建一个AccountTCCService接口的实现类:
  1. @Slf4j
  2. @Service
  3. public class AccountTCCServiceImpl implements AccountTCCService {
  4.     @Autowired
  5.     private AccountMapper accountMapper;
  6.     @Autowired
  7.     private AccountFreezeMapper accountFreezeMapper;
  8.     @Override
  9.     @Transactional
  10.     public void deduct(String userId, int money) {
  11.         // 1.获取事务xid
  12.         String xid = RootContext.getXID();
  13.         // 2.扣减可用余额
  14.         accountMapper.deduct(userId, money);
  15.         // 3.记录冻结金额,事务状态
  16.         AccountFreeze freeze = new AccountFreeze();
  17.         freeze.setUserId(userId);
  18.         freeze.setFreezeMoney(money);
  19.         freeze.setState(0);
  20.         freeze.setXid(xid);
  21.         accountFreezeMapper.insert(freeze);
  22.     }
  23.     @Override
  24.     public boolean confirm(BusinessActionContext ctx) {
  25.         // 1.获取事务xid
  26.         String xid = ctx.getXid();
  27.         // 2.根据xid删除冻结记录
  28.         int count = accountFreezeMapper.deleteById(xid);
  29.         return count == 1;
  30.     }
  31.     @Override
  32.     public boolean cancel(BusinessActionContext ctx) {
  33.         // 1.查询冻结记录
  34.         String xid = ctx.getXid();
  35.         AccountFreeze freeze = accountFreezeMapper.selectById(xid);
  36.         // 2.恢复可用余额
  37.         accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
  38.         // 3.将冻结金额清零,状态改为CANCEL
  39.         freeze.setFreezeMoney(0);
  40.         freeze.setState(2);
  41.         int count = accountFreezeMapper.updateById(freeze);
  42.         return count == 1;
  43.     }
  44. }
复制代码
6.4.4 Controller类调用TCC接口

修改AccountController类的deduct方法,改为调用刚刚新建的TCC接口:
  1. @RestController
  2. @RequestMapping("account")
  3. public class AccountController {
  4.     @Autowired
  5.     private AccountService accountService;
  6.     @Autowired
  7.     private AccountTCCService accountTCCService;
  8.     @PutMapping("/{userId}/{money}")
  9.     public ResponseEntity<Void> deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money){
  10.         // accountService.deduct(userId, money);
  11.         accountTCCService.deduct(userId, money);
  12.         return ResponseEntity.noContent().build();
  13.     }
  14. }
复制代码
6.4.5 修改设置文件application.yml

修改微服务下的设置文件application.yml,注释掉分布式事务的模式:
  1. seata:
  2.   # data-source-proxy-mode: AT
复制代码
6.4.6 重启微服务并测试

假设现在商品库存为10,用户余额为1000。用户使用300金额购买了6件商品:

库存和资金充足,下单成功。此时库存剩余4,余额为700。
此时用户使用300金额再次购买了6件商品,由于库存不敷,会下单失败:

检察jd-account-service微服务的日记,可以看到冻结金额被写入 t_account_freeze 表(Try阶段):

随后,事务回滚,先查询 t_account_freeze 表,在规复余额:

由此可见,TCC模式的分布式事务生效了。
7 TC服务高可用

TC服务作为分布式事务的核心,肯定要保证其高可用,因此需要搭建TC服务集群。
7.1 高可用架构模型

搭建TC服务集群,只需要启动多个TC服务,注册到Nacos即可,相对简单。
同时还支持异地容灾,例如一个TC服务集群在上海,另一个TC服务集群在杭州,当其中一个集群故障时主动切换到别的一个集群。

如上图所示,微服务基于事务组(tx-service-group属性)与TC服务集群的映射关系,来查找当前应该使用哪个集群。
7.2 实现TC服务高可用

7.2.1 模仿异地容灾

筹划启动两台TC服务器:
节点名称IP地址端标语集群名称Seata1127.0.0.19091SHSeata2127.0.0.19092HZ 修改Seata服务的设置文件,启动9091服务:

将Seata服务目次服务一份,修改设置文件,启动9092服务:

此时可以在Nacos控制台看到这两个节点的服务:

进入“详情”可以看到它们分属两个集群:

7.2.2 将事务组映射设置到Nacos

新建一个设置,将tx-service-group与cluster的映射关系设置到Nacos设置中心:

设置的内容如下:
  1. # 事务组映射关系
  2. service.vgroupMapping.default_tx_group=SH
  3. service.enableDegrade=false
  4. service.disableGlobalTransaction=false
  5. # 与TC服务的通信配置
  6. transport.type=TCP
  7. transport.server=NIO
  8. transport.heartbeat=true
  9. transport.enableClientBatchSendRequest=false
  10. transport.threadFactory.bossThreadPrefix=NettyBoss
  11. transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
  12. transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
  13. transport.threadFactory.shareBossWorker=false
  14. transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
  15. transport.threadFactory.clientSelectorThreadSize=1
  16. transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
  17. transport.threadFactory.bossThreadSize=1
  18. transport.threadFactory.workerThreadSize=default
  19. transport.shutdown.wait=3
  20. # RM配置
  21. client.rm.asyncCommitBufferLimit=10000
  22. client.rm.lock.retryInterval=10
  23. client.rm.lock.retryTimes=30
  24. client.rm.lock.retryPolicyBranchRollbackOnConflict=true
  25. client.rm.reportRetryCount=5
  26. client.rm.tableMetaCheckEnable=false
  27. client.rm.tableMetaCheckerInterval=60000
  28. client.rm.sqlParserType=druid
  29. client.rm.reportSuccessEnable=false
  30. client.rm.sagaBranchRegisterEnable=false
  31. # TM配置
  32. client.tm.commitRetryCount=5
  33. client.tm.rollbackRetryCount=5
  34. client.tm.defaultGlobalTransactionTimeout=60000
  35. client.tm.degradeCheck=false
  36. client.tm.degradeCheckAllowTimes=10
  37. client.tm.degradeCheckPeriod=2000
  38. # undo日志配置
  39. client.undo.dataValidation=true
  40. client.undo.logSerialization=jackson
  41. client.undo.onlyCareUpdateColumns=true
  42. client.undo.logTable=undo_log
  43. client.undo.compress.enable=true
  44. client.undo.compress.type=zip
  45. client.undo.compress.threshold=64k
  46. client.log.exceptionRate=100
复制代码
7.2.3 微服务读取Nacos设置

修改每一个微服务的application.yml文件,让微服务读取Nacos中的client.properties文件:
  1. seata:
  2.   config:
  3.     type: nacos
  4.     nacos:
  5.       server-addr: 127.0.0.1:8848
  6.       username:
  7.       password:
  8.       group: DEFAULT_GROUP
  9.       data-id: client.properties
  10.   tx-service-group: default_tx_group # 事务组名称
复制代码
从启动日记可以看到此时毗连的TC服务端口是9091,集群是SH:

如果此时上海节点故障了,只需要在Nacos设置中心修改设置:

修改后,微服务主动切换到9092端口:

可见,TC服务集群的高可用已生效。

本节完,更多内容请查阅分类专栏:微服务学习条记
感爱好的读者还可以查阅我的别的几个专栏:


  • SpringBoot源码解读与原理分析
  • MyBatis3源码深度解析
  • Redis从入门到醒目
  • MyBatisPlus详解
  • SpringCloud学习条记

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

尚未崩坏

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表