聊聊MQ的消息必达和消息幂等与消息延时

【1】MQ之如何做到消息必达

① MQ简单架构

MQ要想尽量消息必达,架构上有两个核心设计点:

  • 消息落地
  • 消息超时、重传、确认

在这里插入图片描述
上图是一个MQ的核心架构图,基本可以分为三大块:

  • 发送方–左侧粉色部分
  • MQ核心集群–中间蓝色部分
  • 接收方–右侧黄色部分

粉色发送方也就是发送方又由两部分构成:业务调用方与MQ-client-sender。其中后者向前者提供了两个核心API:

//发送消息
SendMsg(bytes[] msg);
//发送回调
SendCallback();

蓝色MQ核心集群又分为四个部分:MQ-server,zk,db,管理后台web。

黄色接收方也由两部分构成:业务接收方与MQ-client-receiver。其中后者向前者提供了两个核心API:

RecvCallback(bytes[] msg)
    
SendAck()

MQ是一个系统间解耦的利器,它能够很好的解除发布订阅者之间的耦合,它将上下游的消息投递解耦成两个部分,如上述架构图中的1箭头和2箭头:

  • 发送方将消息投递给MQ,上半场
  • MQ将消息投递给接收方,下半场

② MQ消息可靠投递核心流程

MQ既然将消息投递拆成了上下半场,为了保证消息的可靠投递,上下半场都必须尽量保证消息必达。
在这里插入图片描述

MQ消息投递上半场

MQ-client-sender到MQ-server流程见上图1-3:

  • MQ-client将消息发送给MQ-server(此时业务方调用的是API:SendMsg)
  • MQ-server将消息落地,落地后即为发送成功
  • MQ-server将应答发送给MQ-client(此时回调业务方是API:SendCallback)

MQ消息投递下半场

MQ-server到MQ-client-receiver流程见上图4-6:

  • MQ-server将消息发送给MQ-client(此时回调业务方是API:RecvCallback)
  • MQ-client回复应答给MQ-server(此时业务方主动调用API:SendAck)
  • MQ-server收到ack,将之前已经落地的消息删除,完成消息的可靠投递

如果消息丢了怎么办?

MQ消息投递的上下半场,都可以出现消息丢失,为了降低消息丢失的概率,MQ需要进行超时和重传。

上半场的超时与重传

MQ上半场的1或者2或者3如果丢失或者超时,MQ-client-sender内的timer会重发消息,直到期望收到3,如果重传N次后还未收到,则SendCallback回调发送失败,需要注意的是,这个过程中MQ-server可能会收到同一条消息的多次重发

下半场的超时与重传

MQ下半场的4或者5或者6如果丢失或者超时,MQ-server内的timer会重发消息,直到收到5并且成功执行6。这个过程可能会重发很多次消息,一般采用指数退避的策略,先隔x秒重发,2x秒重发,4x秒重发,以此类推。需要注意的是,这个过程中MQ-client-receiver也可能会收到同一条消息的多次重发。

MQ-clientMQ-server如何进行消息去重,如何进行架构幂等性设计,下面讲述,此处暂且认为为了保证消息必达,可能收到重复的消息。


【2】MQ之如何做到消息幂等

① 上半场的幂等性设计

在这里插入图片描述
MQ消息发送上半场,即上图中的1-3

  • 1,发送端MQ-client将消息发给服务端MQ-server
  • 2,服务端MQ-server将消息落地
  • 3,服务端MQ-serverACK给发送端MQ-client

如果3丢失,发送端MQ-client超时后会重发消息,可能导致服务端MQ-server收到重复消息。

此时重发是MQ-client发起的,消息的处理是MQ-server,为了避免步骤2落地重复的消息,对每条消息,MQ系统内部必须生成一个inner-msg-id,作为去重和幂等的依据,这个内部消息ID的特性是:

  • 全局唯一
  • MQ生成,具备业务无关性,对消息发送方和消息接收方屏蔽

有了这个inner-msg-id,就能保证上半场重发,也只有1条消息落到MQ-server的DB中,实现上半场幂等。

② 下半场的幂等性设计

在这里插入图片描述
MQ消息发送下半场,即上图中的4-6

  • 4,服务端MQ-server将消息发给接收端MQ-client

  • 5,接收端MQ-clientACK给服务端

  • 6,服务端MQ-server将落地消息删除

需要强调的是,接收端MQ-clientACK给服务端MQ-server是消息消费业务方的主动调用行为,不能由MQ-client自动发起,因为MQ系统不知道消费方什么时候真正消费成功

如果5丢失,服务端MQ-server超时后会重发消息,可能导致MQ-client收到重复的消息。

此时重发是MQ-server发起的,消息的处理是消息消费业务方,消息重发势必导致业务方重复消费。为了保证业务幂等性,业务消息体中,必须有一个biz-id,作为去重和幂等的依据,这个业务ID的特性是:

  • 对于同一个业务场景,全局唯一
  • 由业务消息发送方生成,业务相关,对MQ透明
  • 由业务消息消费方负责判重,以保证幂等

最常见的业务ID有:支付ID,订单ID,帖子ID等。具体到支付购卡场景,发送方必须将支付ID放到消息体中,消费方必须对同一个支付ID进行判重,保证购卡的幂等。

有了这个业务ID,才能够保证下半场消息消费业务方即使收到重复消息,也只有1条消息被消费,保证了幂等。

③ 总结

MQ为了保证消息必达,消息上下半场均可能发送重复消息,如何保证消息的幂等性呢?

上半场

MQ生成inner-msg-id,保证上半场幂等。这个ID全局唯一,业务无关,由MQ保证。

下半场

业务发送方带入biz-id,业务接收方去重保证幂等。这个ID对单业务唯一,业务相关,对MQ透明。

结论:幂等性,不仅对MQ有要求,对业务上下游也有要求。


【3】MQ之如何做到消息延时

很多时候,业务有“在一段时间之后,完成一个工作任务”的需求。

例如:滴滴打车订单完成后,如果用户一直不评价,48小时后会将自动评价为5星。一般来说怎么实现这类“48小时后自动评价为5星”需求呢?

常见方案:

启动一个cron定时任务,每小时跑一次,将完成时间超过48小时的订单取出,置为5星,并把评价状态置为已评价。假设订单表的结构为:t_order(oid, finish_time, stars, status, …),更具体的,定时任务每隔一个小时会这么做一次:select oid from t_order where finish_time > 48hours and status=0;update t_order set stars=5 and status=1 where oid in[…];如果数据量很大,需要分页查询,分页update,这将会是一个for循环。

方案的不足:

  • 轮询效率比较低
  • 每次扫库,已经被执行过记录,仍然会被扫描(只是不会出现在结果集中),有重复计算的嫌疑
  • 时效性不够好,如果每小时轮询一次,最差的情况下,时间误差会达到1小时
  • 如果通过增加cron轮询频率来减少③中的时间误差,①中轮询低效和②中重复计算的问题会进一步凸显。

高效延时消息设计与实现

高效延时消息,包含两个重要的数据结构:

  • 环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组)
  • 任务集合,环上每一个slot是一个Set

同时,启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的slot。

Task结构中有两个很重要的属性:

  • Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务
  • Task-Function:需要执行的任务指针

在这里插入图片描述

假设当前Current Index指向第一格,当有延时消息到达之后,例如希望3610秒之后,触发一个延时消息任务,只需:

  • 计算这个Task应该放在哪一个slot,现在指向1,3610秒之后,应该是第11格,所以这个Task应该放在第11个slot的Set
  • 计算这个Task的Cycle-Num,由于环形队列是3600格(每秒移动一格,正好1小时),这个任务是3610秒后执行,所以应该绕3610/3600=1圈之后再执行,于是Cycle-Num=1

Current Index不停的移动,每秒移动到一个新slot,这个slot中对应的Set,每个Task看Cycle-Num是不是0:

  • 如果不是0,说明还需要多移动几圈,将Cycle-Num减1
  • 如果是0,说明马上要执行这个Task了,取出Task-Funciton执行(可以用单独的线程来执行Task),并把这个Task从Set中删除

使用了“延时消息”方案之后,“订单48小时后关闭评价”的需求,只需将在订单关闭时,触发一个48小时之后的延时消息即可:

  • 无需再轮询全部订单,效率高
  • 一个订单,任务只执行一次
  • 时效性好,精确到秒(控制timer移动频率可以控制精度)

总结

环形队列是一个实现“延时消息”的好方法,开源的MQ好像都不支持延迟消息,不妨自己实现一个简易的“延时消息队列”,能解决很多业务问题,并减少很多低效扫库的cron任务。

参考博文:
MQ之如何做到消息延时

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值