终于有人把“TCC分布式事务”实现原理讲明白了!

更新时间:2023-05-29 13:17:11 阅读: 评论:0

终于有⼈把“TCC分布式事务”实现原理讲明⽩了!
之前⽹上看到很多写分布式事务的⽂章,不过⼤多都是将分布式事务各种技术⽅案简单介绍⼀下。很多朋友看了还是不知道分布式事务到底怎么回事,在项⽬⾥到底如何使⽤。所以这篇⽂章,就⽤⼤⽩话+⼿⼯绘图,并结合⼀个电商系统的案例实践,来给⼤家讲清楚到底什么是 TCC 分布式事务。
⾸先说⼀下,这⾥可能会牵扯到⼀些 Spring Cloud 的原理,如果有不太清楚的同学,可以参考之前的⽂章:。
业务场景介绍
咱们先来看看业务场景,假设你现在有⼀个电商系统,⾥⾯有⼀个⽀付订单的场景。
那对⼀个订单⽀付之后,我们需要做下⾯的步骤:
更改订单的状态为“已⽀付”
扣减商品库存
给会员增加积分
创建销售出库单通知仓库发货
这是⼀系列⽐较真实的步骤,⽆论⼤家有没有做过电商系统,应该都能理解。
进⼀步思考
好,业务场景有了,现在我们要更进⼀步,实现⼀个 TCC 分布式事务的效果。
什么意思呢?也就是说,[1] 订单服务-修改订单状态,[2] 库存服务-扣减库存,[3] 积分服务-增加积分,[4] 仓储服务-创建销售出库单。
上述这⼏个步骤,要么⼀起成功,要么⼀起失败,必须是⼀个整体性的事务。
举个例⼦,现在订单的状态都修改为“已⽀付”了,结果库存服务扣减库存失败。那个商品的库存原来是 100 件,现在卖掉了 2 件,本来应该是 98 件了。
结果呢?由于库存服务操作数据库异常,导致库存数量还是 100。这不是在坑⼈么,当然不能允许这种情况发⽣了!
但是如果你不⽤ TCC 分布式事务⽅案的话,就⽤个 Spring Cloud 开发这么⼀个微服务系统,很有可能会⼲出这种事⼉来。
我们来看看下⾯的这个图,直观的表达了上述的过程:
所以说,我们有必要使⽤ TCC 分布式事务机制来保证各个服务形成⼀个整体性的事务。
上⾯那⼏个步骤,要么全部成功,如果任何⼀个服务的操作失败了,就全部⼀起回滚,撤销已经完成的操作。
⽐如说库存服务要是扣减库存失败了,那么订单服务就得撤销那个修改订单状态的操作,然后得停⽌执⾏增加积分和通知出库两个操作。
说了那么多,⽼规矩,给⼤家上⼀张图,⼤伙⼉顺着图来直观的感受⼀下:
落地实现 TCC 分布式事务
那么现在到底要如何来实现⼀个 TCC 分布式事务,使得各个服务,要么⼀起成功?要么⼀起失败呢?
⼤家稍安勿躁,我们这就来⼀步⼀步的分析⼀下。咱们就以⼀个 Spring Cloud 开发系统作为背景来解释。
TCC 实现阶段⼀:Try
⾸先,订单服务那⼉,它的代码⼤致来说应该是这样⼦的:
public class OrderService {
// 库存服务
@Autowired
private InventoryService inventoryService;
// 积分服务
@Autowired
private CreditService creditService;
// 仓储服务
@Autowired
private WmsService wmsService;
// 对这个订单完成⽀付
public void pay(){
//对本地的的订单数据库修改订单状态为"已⽀付"
orderDAO.updateStatus(OrderStatus.PAYED);
//调⽤库存服务扣减库存
//调⽤积分服务增加积分
creditService.addCredit();
//调⽤仓储服务通知发货
wmsService.saleDelivery();
}
}
如果你之前看过 Spring Cloud 架构原理那篇⽂章,同时对 Spring Cloud 有⼀定的了解的话,应该是可以理解上⾯那段代码的。
其实就是订单服务完成本地数据库操作之后,通过 Spring Cloud 的 Feign 来调⽤其他的各个服务罢了。
但是光是凭借这段代码,是不⾜以实现 TCC 分布式事务的啊?!兄弟们,别着急,我们对这个订单服务修改点⼉代码好不好。
⾸先,上⾯那个订单服务先把⾃⼰的状态修改为:OrderStatus.UPDATING。
这是啥意思呢?也就是说,在 pay() 那个⽅法⾥,你别直接把订单状态修改为已⽀付啊!你先把订单状态修改为 UPDATING,也就是修改中的意思。
这个状态是个没有任何含义的这么⼀个状态,代表有⼈正在修改这个状态罢了。
然后呢,库存服务直接提供的那个 reduceStock() 接⼝⾥,也别直接扣减库存啊,你可以是冻结掉库存。女性游戏名字
举个例⼦,本来你的库存数量是 100,你别直接 100 - 2 = 98,扣减这个库存!
你可以把可销售的库存:100 - 2 = 98,设置为 98 没问题,然后在⼀个单独的冻结库存的字段⾥,设置⼀个 2。也就是说,有 2 个库存是给冻结了。
积分服务的 addCredit() 接⼝也是同理,别直接给⽤户增加会员积分。你可以先在积分表⾥的⼀个预增加积分字段加⼊积分。
⽐如:⽤户积分原本是 1190,现在要增加 10 个积分,别直接 1190 + 10 = 1200 个积分啊!
你可以保持积分为 1190 不变,在⼀个预增加字段⾥,⽐如说 prepare_add_credit 字段,设置⼀个 10,表⽰有 10 个积分准备增加。
仓储服务的 saleDelivery() 接⼝也是同理啊,你可以先创建⼀个销售出库单,但是这个销售出库单的状态是“UNKNOWN”。
也就是说,刚刚创建这个销售出库单,此时还不确定它的状态是什么呢!
上⾯这套改造接⼝的过程,其实就是所谓的 TCC 分布式事务中的第⼀个 T 字母代表的阶段,也就是 Try 阶段。
总结上述过程,如果你要实现⼀个 TCC 分布式事务,⾸先你的业务的主流程以及各个接⼝提供的业
务含义,不是说直接完成那个业务操作,⽽是完成⼀个 Try 的操作。这个操作,⼀般都是锁定某个资源,设置⼀个预备类的状态,冻结部分数据,等等,⼤概都是这类操作。
咱们来⼀起看看下⾯这张图,结合上⾯的⽂字,再来捋⼀捋整个过程:
TCC 实现阶段⼆:Confirm
然后就分成两种情况了,第⼀种情况是⽐较理想的,那就是各个服务执⾏⾃⼰的那个 Try 操作,都执⾏成功了,Bingo!
这个时候,就需要依靠 TCC 分布式事务框架来推动后续的执⾏了。这⾥简单提⼀句,如果你要玩⼉ TCC 分布式事务,必须引⼊⼀款 TCC 分布式事务框架,⽐如国内开源的ByteTCC、Himly、TCC-transaction。
否则的话,感知各个阶段的执⾏情况以及推进执⾏下⼀个阶段的这些事情,不太可能⾃⼰⼿写实现,太复杂了。
如果你在各个服务⾥引⼊了⼀个 TCC 分布式事务的框架,订单服务⾥内嵌的那个 TCC 分布式事务框架可以感知到,各个服务的 Try 操作都成功了。
此时,TCC 分布式事务框架会控制进⼊ TCC 下⼀个阶段,第⼀个 C 阶段,也就是 Confirm 阶段。
为了实现这个阶段,你需要在各个服务⾥再加⼊⼀些代码。⽐如说,订单服务⾥,你可以加⼊⼀个 Confirm 的逻辑,就是正式把订单的状态设置为“已⽀付”了,⼤概是类似下⾯这样⼦:
public class OrderServiceConfirm {
public void pay(){
orderDao.updateStatus(OrderStatus.PAYED);
很简单英文
}
}
库存服务也是类似的,你可以有⼀个 InventoryServiceConfirm 类,⾥⾯提供⼀个 reduceStock() 接⼝的 Confirm 逻辑,这⾥就是将之前冻结库存字段的 2 个库存扣掉变为 0。这样的话,可销售库存之前就已经变为 98 了,现在冻结的 2 个库存也没了,那就正式完成了库存的扣减。
积分服务也是类似的,可以在积分服务⾥提供⼀个 CreditServiceConfirm 类,⾥⾯有⼀个 addCredit() 接⼝的 Confirm 逻辑,就是将预增加字段的 10 个积分扣掉,然后加⼊实际的会员积分字段中,从 1190 变为 1120。
仓储服务也是类似,可以在仓储服务中提供⼀个 WmsServiceConfirm 类,提供⼀个 saleDelivery() 接⼝的 Confirm 逻辑,将销售出库单的状态正式修改为“已创建”,可以供仓储管理⼈员查看和使⽤,⽽不是停留在之前的中间状态“UNKNOWN”了。
好了,上⾯各种服务的 Confirm 的逻辑都实现好了,⼀旦订单服务⾥⾯的 TCC 分布式事务框架感知到各个服务的 Try 阶段都成功了以后,就会执⾏各个服务的 Confirm 逻辑。订单服务内的 TCC 事务框架会负责跟其他各个服务内的 TCC 事务框架进⾏通信,依次调⽤各个服务的 Confirm 逻辑。然后,正式完成各个服务的所有业务逻辑的执⾏。
同样,给⼤家来⼀张图,顺着图⼀起来看看整个过程:
TCC 实现阶段三:Cancel
好,这是⽐较正常的⼀种情况,那如果是异常的⼀种情况呢?
讽刺英语举个例⼦:在 Try 阶段,⽐如积分服务吧,它执⾏出错了,此时会怎么样?
那订单服务内的 TCC 事务框架是可以感知到的,然后它会决定对整个 TCC 分布式事务进⾏回滚。
也就是说,会执⾏各个服务的第⼆个 C 阶段,Cancel 阶段。同样,为了实现这个 Cancel 阶段,各个服务还得加⼀些代码。
⾸先订单服务,它得提供⼀个 OrderServiceCancel 的类,在⾥⾯有⼀个 pay() 接⼝的 Cancel 逻辑,就是可以将订单的状态设置为“CANCELED”,也就是这个订单的状态是已取消。
库存服务也是同理,可以提供 reduceStock() 的 Cancel 逻辑,就是将冻结库存扣减掉 2,加回到可销售库存⾥去,98 + 2 = 100。
积分服务也需要提供 addCredit() 接⼝的 Cancel 逻辑,将预增加积分字段的 10 个积分扣减掉。
仓储服务也需要提供⼀个 saleDelivery() 接⼝的 Cancel 逻辑,将销售出库单的状态修改为“CANCELED”设置为已取消。
然后这个时候,订单服务的 TCC 分布式事务框架只要感知到了任何⼀个服务的 Try 逻辑失败了,就会跟各个服务内的 TCC 分布式事务框架进⾏通信,然后调⽤各个服务的Cancel 逻辑。
⼤家看看下⾯的图,直观的感受⼀下:
总结与思考
好了,兄弟们,聊到这⼉,基本上⼤家应该都知道 TCC 分布式事务具体是怎么回事了!
总结⼀下,你要玩⼉ TCC 分布式事务的话:⾸先需要选择某种 TCC 分布式事务框架,各个服务⾥就会有这个 TCC 分布式事务框架在运⾏。
然后你原本的⼀个接⼝,要改造为 3 个逻辑,Try-Confirm-Cancel:
先是服务调⽤链路依次执⾏ Try 逻辑。
如果都正常的话,TCC 分布式事务框架推进执⾏ Confirm 逻辑,完成整个事务。
如果某个服务的 Try 逻辑有问题,TCC 分布式事务框架感知到之后就会推进执⾏各个服务的 Cancel 逻辑,撤销之前执⾏的各种操作。
这就是所谓的 TCC 分布式事务。TCC 分布式事务的核⼼思想,说⽩了,就是当遇到下⾯这些情况时:
某个服务的数据库宕机了。
某个服务⾃⼰挂了。
那个服务的 Redis、Elasticarch、MQ 等基础设施故障了。
某些资源不⾜了,⽐如说库存不够这些。
先来 Try ⼀下,不要把业务逻辑完成,先试试看,看各个服务能不能基本正常运转,能不能先冻结我需要的资源。
如果 Try 都 OK,也就是说,底层的数据库、Redis、Elasticarch、MQ 都是可以写⼊数据的,并且你保留好了需要使⽤的⼀些资源(⽐如冻结了⼀部分库存)。
接着,再执⾏各个服务的 Confirm 逻辑,基本上 Confirm 就可以很⼤概率保证⼀个分布式事务的完成了。
那如果 Try 阶段某个服务就失败了,⽐如说底层的数据库挂了,或者 Redis 挂了,等等。
此时就⾃动执⾏各个服务的 Cancel 逻辑,把之前的 Try 逻辑都回滚,所有服务都不要执⾏任何设计的业务逻辑。保证⼤家要么⼀起成功,要么⼀起失败。
等⼀等,你有没有想到⼀个问题?如果有⼀些意外的情况发⽣了,⽐如说订单服务突然挂了,然后再次重启,TCC 分布式事务框架是如何保证之前没执⾏完的分布式事务继续执⾏的呢?
所以,TCC 事务框架都是要记录⼀些分布式事务的活动⽇志的,可以在磁盘上的⽇志⽂件⾥记录,也可以在数据库⾥记录。保存下来分布式事务运⾏的各个阶段和状态。
问题还没完,万⼀某个服务的 Cancel 或者 Confirm 逻辑执⾏⼀直失败怎么办呢?
那也很简单,TCC 事务框架会通过活动⽇志记录各个服务的状态。举个例⼦,⽐如发现某个服务的 Cancel 或者 Confirm ⼀直没成功,会不停的重试调⽤它的 Cancel 或者Confirm 逻辑,务必要它成功!
当然了,如果你的代码没有写什么 Bug,有充⾜的测试,⽽且 Try 阶段都基本尝试了⼀下,那么其实⼀般 Confirm、Cancel 都是可以成功的!
最后,再给⼤家来⼀张图,来看看给我们的业务,加上分布式事务之后的整个执⾏流程:
不少⼤公司⾥,其实都是⾃⼰研发 TCC 分布式事务框架的,专门在公司内部使⽤,⽐如我们就是这样。
不过如果⾃⼰公司没有研发 TCC 分布式事务框架的话,那⼀般就会选⽤开源的框架。
这⾥笔者给⼤家推荐⼏个⽐较不错的框架,都是咱们国内⾃⼰开源出去的:ByteTCC,TCC-transaction,Himly。
⼤家有兴趣的可以去它们的 GitHub 地址,学习⼀下如何使⽤,以及如何跟 Spring Cloud、Dubbo 等服务框架整合使⽤。
只要把那些框架整合到你的系统⾥,很容易就可以实现上⾯那种奇妙的 TCC 分布式事务的效果了。
下⾯,我们来讲讲可靠消息最终⼀致性⽅案实现的分布式事务,同时聊聊在实际⽣产中遇到的运⽤该⽅案的⾼可⽤保障架构。
最终⼀致性分布式事务如何保障实际⽣产中 99.99% ⾼可⽤?
上⾯咱们聊了聊 TCC 分布式事务,对于常见的微服务系统,⼤部分接⼝调⽤是同步的,也就是⼀个服务直接调⽤另外⼀个服务的接⼝。
这个时候,⽤ TCC 分布式事务⽅案来保证各个接⼝的调⽤,要么⼀起成功,要么⼀起回滚,是⽐较合适的。
但是在实际系统的开发过程中,可能服务间的调⽤是异步的。也就是说,⼀个服务发送⼀个消息给 MQ,即消息中间件,⽐如 RocketMQ、RabbitMQ、Kafka、ActiveMQ 等等。
然后,另外⼀个服务从 MQ 消费到⼀条消息后进⾏处理。这就成了基于 MQ 的异步调⽤了。
那么针对这种基于 MQ 的异步调⽤,如何保证各个服务间的分布式事务呢?也就是说,我希望的是基于 MQ 实现异步调⽤的多个服务的业务逻辑,要么⼀起成功,要么⼀起失败。
这个时候,就要⽤上可靠消息最终⼀致性⽅案,来实现分布式事务。
⼤家看上图,如果不考虑各种⾼并发、⾼可⽤等技术挑战的话,单从“可靠消息”以及“最终⼀致性”两个⾓度来考虑,这种分布式事务⽅案还是⽐较简单的。
ppt母版
可靠消息最终⼀致性⽅案的核⼼流程
①上游服务投递消息
如果要实现可靠消息最终⼀致性⽅案,⼀般你可以⾃⼰写⼀个可靠消息服务,实现⼀些业务逻辑。
⾸先,上游服务需要发送⼀条消息给可靠消息服务。这条消息说⽩了,你可以认为是对下游服务⼀个接⼝的调⽤,⾥⾯包含了对应的⼀些请求参数。
然后,可靠消息服务就得把这条消息存储到⾃⼰的数据库⾥去,状态为“待确认”。
接着,上游服务就可以执⾏⾃⼰本地的数据库操作,根据⾃⼰的执⾏结果,再次调⽤可靠消息服务的接⼝。
如果本地数据库操作执⾏成功了,那么就找可靠消息服务确认那条消息。如果本地数据库操作失败了,那么就找可靠消息服务删除那条消息。
此时如果是确认消息,那么可靠消息服务就把数据库⾥的消息状态更新为“已发送”,同时将消息发送给 MQ。早餐你喜欢吃什么>叶圣陶散文
这⾥有⼀个很关键的点,就是更新数据库⾥的消息状态和投递消息到 MQ。这俩操作,你得放在⼀个⽅法⾥,⽽且得开启本地事务。
啥意思呢?如果数据库⾥更新消息的状态失败了,那么就抛异常退出了,就别投递到 MQ;如果投递 MQ 失败报错了,那么就要抛异常让本地数据库事务回滚。这俩操作必须得⼀起成功,或者⼀起失败。
如果上游服务是通知删除消息,那么可靠消息服务就得删除这条消息。
②下游服务接收消息
下游服务就⼀直等着从 MQ 消费消息好了,如果消费到了消息,那么就操作⾃⼰本地数据库。骊山在哪里
如果操作成功了,就反过来通知可靠消息服务,说⾃⼰处理成功了,然后可靠消息服务就会把消息的状态设置为“已完成”。
③如何保证上游服务对消息的 100% 可靠投递?
上⾯的核⼼流程⼤家都看完:⼀个很⼤的问题就是,如果在上述投递消息的过程中各个环节出现了问题该怎么办?
芝加哥有什么大学我们如何保证消息 100% 的可靠投递,⼀定会从上游服务投递到下游服务?别着急,下⾯我们来逐⼀分析。

本文发布于:2023-05-29 13:17:11,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/82/806788.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:服务   事务   分布式   库存   状态   消息   订单   框架
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图