您好,欢迎来到尚车旅游网。
搜索
您的当前位置:首页终于有人把“TCC分布式事务”实现原理讲明白了!

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

来源:尚车旅游网
终于有⼈把“TCC分布式事务”实现原理讲明⽩了!

本⽂转载⾃:

之前⽹上看到很多写分布式事务的⽂章,不过⼤多都是将分布式事务各种技术⽅案简单介绍⼀下。很多朋友看了还是不知道分布式事务到底怎么回事,在项⽬⾥到底如何使⽤。

所以这篇⽂章,就⽤⼤⽩话+⼿⼯绘图,并结合⼀个电商系统的案例实践,来给⼤家讲清楚到底什么是 TCC 分布式事务。

1|0业务场景介绍

咱们先来看看业务场景,假设你现在有⼀个电商系统,⾥⾯有⼀个⽀付订单的场景。那对⼀个订单⽀付之后,我们需要做下⾯的步骤:

更改订单的状态为“已⽀付”扣减商品库存给会员增加积分

创建销售出库单通知仓库发货

这是⼀系列⽐较真实的步骤,⽆论⼤家有没有做过电商系统,应该都能理解。

2|0进⼀步思考

好,业务场景有了,现在我们要更进⼀步,实现⼀个 TCC 分布式事务的效果。

什么意思呢?也就是说,[1] 订单服务-修改订单状态,[2] 库存服务-扣减库存,[3] 积分服务-增加积分,[4] 仓储服务-创建销售出库单。上述这⼏个步骤,要么⼀起成功,要么⼀起失败,必须是⼀个整体性的事务。

举个例⼦,现在订单的状态都修改为“已⽀付”了,结果库存服务扣减库存失败。那个商品的库存原来是 100 件,现在卖掉了 2 件,本来应该是 98 件了。

结果呢?由于库存服务操作数据库异常,导致库存数量还是 100。这不是在坑⼈么,当然不能允许这种情况发⽣了!但是如果你不⽤ TCC 分布式事务⽅案的话,就⽤个 Spring Cloud 开发这么⼀个微服务系统,很有可能会⼲出这种事⼉来。我们来看看下⾯的这个图,直观的表达了上述的过程:

所以说,我们有必要使⽤ TCC 分布式事务机制来保证各个服务形成⼀个整体性的事务。

上⾯那⼏个步骤,要么全部成功,如果任何⼀个服务的操作失败了,就全部⼀起回滚,撤销已经完成的操作。

⽐如说库存服务要是扣减库存失败了,那么订单服务就得撤销那个修改订单状态的操作,然后得停⽌执⾏增加积分和通知出库两个操作。说了那么多,⽼规矩,给⼤家上⼀张图,⼤伙⼉顺着图来直观的感受⼀下:

3|0落地实现 TCC 分布式事务

那么现在到底要如何来实现⼀个 TCC 分布式事务,使得各个服务,要么⼀起成功?要么⼀起失败呢?⼤家稍安勿躁,我们这就来⼀步⼀步的分析⼀下。咱们就以⼀个 Spring Cloud 开发系统作为背景来解释。

3|1TCC 实现阶段⼀:Try

⾸先,订单服务那⼉,它的代码⼤致来说应该是这样⼦的:public class OrderService { // 库存服务

@Autowired

private InventoryService inventoryService; // 积分服务 @Autowired

private CreditService creditService; // 仓储服务 @Autowired

private WmsService wmsService;

// 对这个订单完成⽀付 public void pay(){

//对本地的的订单数据库修改订单状态为\"已⽀付\" orderDAO.updateStatus(OrderStatus.PAYED); //调⽤库存服务扣减库存

inventoryService.reduceStock(); //调⽤积分服务增加积分 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 的操作。

这个操作,⼀般都是锁定某个资源,设置⼀个预备类的状态,冻结部分数据,等等,⼤概都是这类操作。咱们来⼀起看看下⾯这张图,结合上⾯的⽂字,再来捋⼀捋整个过程:

3|2TCC 实现阶段⼆: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 逻辑。然后,正式完成各个服务的所有业务逻辑的执⾏。

同样,给⼤家来⼀张图,顺着图⼀起来看看整个过程:

3|3TCC 实现阶段三: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 逻辑。⼤家看看下⾯的图,直观的感受⼀下:

3|4总结与思考

好了,兄弟们,聊到这⼉,基本上⼤家应该都知道 TCC 分布式事务具体是怎么回事了!

总结⼀下,你要玩⼉ TCC 分布式事务的话:⾸先需要选择某种 TCC 分布式事务框架,各个服务⾥就会有这个 TCC 分布式事务框架在运⾏。然后你原本的⼀个接⼝,要改造为 3 个逻辑,Try-Confirm-Cancel:

先是服务调⽤链路依次执⾏ Try 逻辑。

如果都正常的话,TCC 分布式事务框架推进执⾏ Confirm 逻辑,完成整个事务。

如果某个服务的 Try 逻辑有问题,TCC 分布式事务框架感知到之后就会推进执⾏各个服务的 Cancel 逻辑,撤销之前执⾏的各种操作。这就是所谓的 TCC 分布式事务。TCC 分布式事务的核⼼思想,说⽩了,就是当遇到下⾯这些情况时:

某个服务的数据库宕机了。某个服务⾃⼰挂了。

那个服务的 Redis、Elasticsearch、MQ 等基础设施故障了。某些资源不⾜了,⽐如说库存不够这些。

先来 Try ⼀下,不要把业务逻辑完成,先试试看,看各个服务能不能基本正常运转,能不能先冻结我需要的资源。

如果 Try 都 OK,也就是说,底层的数据库、Redis、Elasticsearch、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 分布式事务的效果了。

下⾯,我们来讲讲可靠消息最终⼀致性⽅案实现的分布式事务,同时聊聊在实际⽣产中遇到的运⽤该⽅案的⾼可⽤保障架构。

4|0最终⼀致性分布式事务如何保障实际⽣产中 99.99% ⾼可⽤?

上⾯咱们聊了聊 TCC 分布式事务,对于常见的微服务系统,⼤部分接⼝调⽤是同步的,也就是⼀个服务直接调⽤另外⼀个服务的接⼝。这个时候,⽤ TCC 分布式事务⽅案来保证各个接⼝的调⽤,要么⼀起成功,要么⼀起回滚,是⽐较合适的。

但是在实际系统的开发过程中,可能服务间的调⽤是异步的。也就是说,⼀个服务发送⼀个消息给 MQ,即消息中间件,⽐如 RocketMQ、RabbitMQ、Kafka、ActiveMQ 等等。

然后,另外⼀个服务从 MQ 消费到⼀条消息后进⾏处理。这就成了基于 MQ 的异步调⽤了。

那么针对这种基于 MQ 的异步调⽤,如何保证各个服务间的分布式事务呢?也就是说,我希望的是基于 MQ 实现异步调⽤的多个服务的业务逻辑,要么⼀起成功,要么⼀起失败。

这个时候,就要⽤上可靠消息最终⼀致性⽅案,来实现分布式事务。

⼤家看上图,如果不考虑各种⾼并发、⾼可⽤等技术挑战的话,单从“可靠消息”以及“最终⼀致性”两个⾓度来考虑,这种分布式事务⽅案还是⽐较简单的。

4|1可靠消息最终⼀致性⽅案的核⼼流程

①上游服务投递消息

如果要实现可靠消息最终⼀致性⽅案,⼀般你可以⾃⼰写⼀个可靠消息服务,实现⼀些业务逻辑。

⾸先,上游服务需要发送⼀条消息给可靠消息服务。这条消息说⽩了,你可以认为是对下游服务⼀个接⼝的调⽤,⾥⾯包含了对应的⼀些请求参数。然后,可靠消息服务就得把这条消息存储到⾃⼰的数据库⾥去,状态为“待确认”。

接着,上游服务就可以执⾏⾃⼰本地的数据库操作,根据⾃⼰的执⾏结果,再次调⽤可靠消息服务的接⼝。

如果本地数据库操作执⾏成功了,那么就找可靠消息服务确认那条消息。如果本地数据库操作失败了,那么就找可靠消息服务删除那条消息。此时如果是确认消息,那么可靠消息服务就把数据库⾥的消息状态更新为“已发送”,同时将消息发送给 MQ。

这⾥有⼀个很关键的点,就是更新数据库⾥的消息状态和投递消息到 MQ。这俩操作,你得放在⼀个⽅法⾥,⽽且得开启本地事务。

啥意思呢?如果数据库⾥更新消息的状态失败了,那么就抛异常退出了,就别投递到 MQ;如果投递 MQ 失败报错了,那么就要抛异常让本地数据库事务回滚。这俩操作必须得⼀起成功,或者⼀起失败。

如果上游服务是通知删除消息,那么可靠消息服务就得删除这条消息。

②下游服务接收消息

下游服务就⼀直等着从 MQ 消费消息好了,如果消费到了消息,那么就操作⾃⼰本地数据库。

如果操作成功了,就反过来通知可靠消息服务,说⾃⼰处理成功了,然后可靠消息服务就会把消息的状态设置为“已完成”。

③如何保证上游服务对消息的 100% 可靠投递?

上⾯的核⼼流程⼤家都看完:⼀个很⼤的问题就是,如果在上述投递消息的过程中各个环节出现了问题该怎么办?我们如何保证消息 100% 的可靠投递,⼀定会从上游服务投递到下游服务?别着急,下⾯我们来逐⼀分析。

如果上游服务给可靠消息服务发送待确认消息的过程出错了,那没关系,上游服务可以感知到调⽤异常的,就不⽤执⾏下⾯的流程了,这是没问题的。

如果上游服务操作完本地数据库之后,通知可靠消息服务确认消息或者删除消息的时候,出现了问题。

⽐如:没通知成功,或者没执⾏成功,或者是可靠消息服务没成功的投递消息到 MQ。这⼀系列步骤出了问题怎么办?其实也没关系,因为在这些情况下,那条消息在可靠消息服务的数据库⾥的状态会⼀直是“待确认”。此时,我们在可靠消息服务⾥开发⼀个后台定时运⾏的线程,不停的检查各个消息的状态。

如果⼀直是“待确认”状态,就认为这个消息出了点什么问题。此时的话,就可以回调上游服务提供的⼀个接⼝,问问说,兄弟,这个消息对应的数据库操作,你执⾏成功了没啊?

如果上游服务答复说,我执⾏成功了,那么可靠消息服务将消息状态修改为“已发送”,同时投递消息到 MQ。如果上游服务答复说,没执⾏成功,那么可靠消息服务将数据库中的消息删除即可。通过这套机制,就可以保证,可靠消息服务⼀定会尝试完成消息到 MQ 的投递。

④如何保证下游服务对消息的 100% 可靠接收?

那如果下游服务消费消息出了问题,没消费到?或者是下游服务对消息的处理失败了,怎么办?其实也没关系,在可靠消息服务⾥开发⼀个后台线程,不断的检查消息状态。

如果消息状态⼀直是“已发送”,始终没有变成“已完成”,那么就说明下游服务始终没有处理成功。此时可靠消息服务就可以再次尝试重新投递消息到 MQ,让下游服务来再次处理。只要下游服务的接⼝逻辑实现幂等性,保证多次处理⼀个消息,不会插⼊重复数据即可。

⑤如何基于 RocketMQ 来实现可靠消息最终⼀致性⽅案?

在上⾯的通⽤⽅案设计⾥,完全依赖可靠消息服务的各种⾃检机制来确保:

如果上游服务的数据库操作没成功,下游服务是不会收到任何通知。

如果上游服务的数据库操作成功了,可靠消息服务死活都会确保将⼀个调⽤消息投递给下游服务,⽽且⼀定会确保下游服务务必成功处理这条消息。

通过这套机制,保证了基于 MQ 的异步调⽤/通知的服务间的分布式事务保障。其实阿⾥开源的 RocketMQ,就实现了可靠消息服务的所有功能,核⼼思想跟上⾯类似。

只不过 RocketMQ 为了保证⾼并发、⾼可⽤、⾼性能,做了较为复杂的架构实现,⾮常的优秀。有兴趣的同学,⾃⼰可以去查阅 RocketMQ 对分布式事务的⽀持。

4|2可靠消息最终⼀致性⽅案的⾼可⽤保障⽣产实践

背景引⼊

上⾯那套⽅案和思想,很多同学应该都知道是怎么回事⼉,我们也主要就是铺垫⼀下这套理论思想。

在实际落地⽣产的时候,如果没有⾼并发场景的,完全可以参照上⾯的思路⾃⼰基于某个 MQ 中间件开发⼀个可靠消息服务。如果有⾼并发场景的,可以⽤ RocketMQ 的分布式事务⽀持上⾯的那套流程都可以实现。今天给⼤家分享的⼀个核⼼主题,就是这套⽅案如何保证 99.99% 的⾼可⽤。⼤家应该发现了这套⽅案⾥保障⾼可⽤性最⼤的⼀个依赖点,就是 MQ 的⾼可⽤性。

任何⼀种 MQ 中间件都有⼀整套的⾼可⽤保障机制,⽆论是 RabbitMQ、RocketMQ 还是 Kafka。

所以在⼤公司⾥使⽤可靠消息最终⼀致性⽅案的时候,我们通常对可⽤性的保障都是依赖于公司基础架构团队对 MQ 的⾼可⽤保障。

也就是说,⼤家应该相信兄弟团队,99.99% 可以保障 MQ 的⾼可⽤,绝对不会因为 MQ 集群整体宕机,⽽导致公司业务系统的分布式事务全部⽆法运⾏。

但是现实是很残酷的,很多中⼩型的公司,甚⾄是⼀些中⼤型公司,或多或少都遇到过 MQ 集群整体故障的场景。MQ ⼀旦完全不可⽤,就会导致业务系统的各个服务之间⽆法通过 MQ 来投递消息,导致业务流程中断。

⽐如最近就有⼀个朋友的公司,也是做电商业务的,就遇到了 MQ 中间件在⾃⼰公司机器上部署的集群整体故障不可⽤,导致依赖 MQ 的分布式事务全部⽆法跑通,业务流程⼤量中断的情况。

这种情况,就需要针对这套分布式事务⽅案实现⼀套⾼可⽤保障机制。

基于 KV 存储的队列⽀持的⾼可⽤降级⽅案

⼤家来看看下⾯这张图,这是我曾经指导过朋友的⼀个公司针对可靠消息最终⼀致性⽅案设计的⼀套⾼可⽤保障降级机制。

这套机制不算太复杂,可以⾮常简单有效的保证那位朋友公司的⾼可⽤保障场景,⼀旦 MQ 中间件出现故障,⽴马⾃动降级为备⽤⽅案。

①⾃⾏封装 MQ 客户端组件与故障感知

⾸先第⼀点,你要做到⾃动感知 MQ 的故障接着⾃动完成降级,那么必须动⼿对 MQ 客户端进⾏封装,发布到公司 Nexus 私服上去。然后公司需要⽀持 MQ 降级的业务服务都使⽤这个⾃⼰封装的组件来发送消息到 MQ,以及从 MQ 消费消息。在你⾃⼰封装的 MQ 客户端组件⾥,你可以根据写⼊ MQ 的情况来判断 MQ 是否故障。

⽐如说,如果连续 10 次重新尝试投递消息到 MQ 都发现异常报错,⽹络⽆法联通等问题,说明 MQ 故障,此时就可以⾃动感知以及⾃动触发降级开关。

②基于 KV 存储中队列的降级⽅案

如果 MQ 挂掉之后,要是希望继续投递消息,那么就必须得找⼀个 MQ 的替代品。

举个例⼦,⽐如我那位朋友的公司是没有⾼并发场景的,消息的量很少,只不过可⽤性要求⾼。此时就可以使⽤类似 Redis 的 KV 存储中的队列来进⾏替代。

由于 Redis 本⾝就⽀持队列的功能,还有类似队列的各种数据结构,所以你可以将消息写⼊ KV 存储格式的队列数据结构中去。PS:关于 Redis 的数据存储格式、⽀持的数据结构等基础知识,请⼤家⾃⾏查阅了,⽹上⼀⼤堆。

但是,这⾥有⼏个⼤坑,⼀定要注意⼀下:

第⼀个,任何 KV 存储的集合类数据结构,建议不要往⾥⾯写⼊数据量过⼤,否则会导致⼤ Value 的情况发⽣,引发严重的后果。因此绝不能在 Redis ⾥搞⼀个 Key,就拼命往这个数据结构中⼀直写⼊消息,这是肯定不⾏的。

第⼆个,绝对不能往少数 Key 对应的数据结构中持续写⼊数据,那样会导致热 Key 的产⽣,也就是某⼏个 Key 特别热。

⼤家要知道,⼀般 KV 集群,都是根据 Key 来 Hash 分配到各个机器上的,你要是⽼写少数⼏个 Key,会导致 KV 集群中的某台机器访问过⾼,负载过⼤。

基于以上考虑,下⾯是笔者当时设计的⽅案:

根据它们每天的消息量,在 KV 存储中固定划分上百个队列,有上百个 Key 对应。

这样保证每个 Key 对应的数据结构中不会写⼊过多的消息,⽽且不会频繁的写少数⼏个 Key。

⼀旦发⽣了 MQ 故障,可靠消息服务可以对每个消息通过 Hash 算法,均匀的写⼊固定好的上百个 Key 对应的 KV 存储的队列中。同时需要通过 ZK 触发⼀个降级开关,整个系统在 MQ 这块的读和写全部⽴马降级。

③下游服务消费 MQ 的降级感知

下游服务消费 MQ 也是通过⾃⾏封装的组件来做的,此时那个组件如果从 ZK 感知到降级开关打开了,⾸先会判断⾃⼰是否还能继续从 MQ 消费到数据?

如果不能了,就开启多个线程,并发的从 KV 存储的各个预设好的上百个队列中不断的获取数据。

每次获取到⼀条数据,就交给下游服务的业务逻辑来执⾏。通过这套机制,就实现了 MQ 故障时候的⾃动故障感知,以及⾃动降级。如果系统的负载和并发不是很⾼的话,⽤这套⽅案⼤致是没问题的。

因为在⽣产落地的过程中,包括⼤量的容灾演练以及⽣产实际故障发⽣时的表现来看,都是可以有效的保证 MQ 故障时,业务流程继续⾃动运⾏的。

④故障的⾃动恢复

如果降级开关打开之后,⾃⾏封装的组件需要开启⼀个线程,每隔⼀段时间尝试给 MQ 投递⼀个消息看看是否恢复了。

如果 MQ 已经恢复可以正常投递消息了,此时就可以通过 ZK 关闭降级开关,然后可靠消息服务继续投递消息到 MQ,下游服务在确认 KV 存储的各个队列中已经没有数据之后,就可以重新切换为从 MQ 消费消息。

⑤更多的业务细节

上⾯说的那套⽅案是⼀套通⽤的降级⽅案,但是具体的落地是要结合各个公司不同的业务细节来决定的,很多细节多没法在⽂章⾥体现。⽐如说你们要不要保证消息的顺序性?是不是涉及到需要根据业务动态,⽣成⼤量的 Key?等等。

此外,这套⽅案实现起来还是有⼀定的成本的,所以建议⼤家尽可能还是 Push 公司的基础架构团队,保证 MQ 的 99.99% 可⽤性,不要宕机。其次就是根据⼤家公司实际对⾼可⽤的需求来决定,如果感觉 MQ 偶尔宕机也没事,可以容忍的话,那么也不⽤实现这种降级⽅案。

但是如果公司领导认为 MQ 中间件宕机后,⼀定要保证业务系统流程继续运⾏,那么还是要考虑⼀些⾼可⽤的降级⽅案,⽐如本⽂提到的这种。最后再说⼀句,真要是⼀些公司涉及到每秒⼏万⼏⼗万的⾼并发请求,那么对 MQ 的降级⽅案会设计的更加的复杂,那就远远不是这么简单可以做到的。

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- sceh.cn 版权所有

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务