mq不丢与延时
参考:
微信公众号:架构师之路
MQ不丢消息,究竟是怎么实现的?
通过消息队列(MsgQueue,MQ)发送任务和消息,万一MQ重启了怎么办?能否保证MQ不丢消息?
今天就聊聊MQ的消息必达性架构与流程。
不丢消息,MQ架构设计的核心方向是什么?MQ要想消息必达,架构上有两个核心设计点:(1)消息落地;(2)消息超时、重传、确认;
为了实现上述两个核心点,MQ架构如何?
上图是一个MQ的核心架构图,可以分为三大块:(1)发送方 -> 左侧粉色部分;(2)MQ核心集群 -> 中间蓝色部分;(3)接收方 -> 右侧屎黄色部分;
粉色发送方又由两部分构成:(1)业务调用方;(2)MQ-client-sender;其中后者向前者提供了两个核心API:
(1)SendMsg(bytes[] msg);
(2)SendCallback();
蓝色MQ核心集群又分为四个部分:
(1)MQ-server
(2)zk;
(3)db;
(4)管理后台web;
黄色接收方也由两部分构成:(1)业务接收方;(2)MQ-client-receiver;其中后者向前者提供了两个核心API:
(1)RecvCallback(bytes[] msg);
(2)SendAck();
MQ是一个系统间解耦的利器,它能够很好的解除发布订阅者之间的耦合,它将上下游的消息投递解耦成两个部分,如架构图中的1箭头和2箭头:
箭头1:发送方将消息投递给MQ,上半场;箭头2:MQ将消息投递给接收方,下半场;
MQ消息可靠投递核心流程如何?MQ既然将消息投递拆成了上下半场,为了保证消息的可靠投递,上下半场都必须保证消息必达。
MQ消息投递上半场,MQ-client-sender到MQ-server流程见上图1-3:(1)MQ-client将消息发送给MQ-server;画外音:此时业务方调用API:SendMsg。(2)MQ-server将消息落地,落地后即为发送成功;(3)MQ-server将应答发送给MQ-client;画外音:此时回调业务API:SendCallback。
MQ消息投递下半场,MQ-server到MQ-client-receiver流程见上图4-6:(4)MQ-server将消息发送给MQ-client;画外音:此时回调业务API:RecvCallback。(5)MQ-client回复应答给MQ-server;画外音:此时业务方主动调用API:SendAck。(6)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是系统之间的解耦利器,MQ为了保证消息必达,架构设计方向为:(1)消息收到先落地;(2)消息超时、重传、确认保证消息必达;
如何快速实现“延时消息”?
快狗打车订单完成后,如果用户一直不评价,48小时后会将自动评价为5星。
怎么实现这类“48小时后自动评价为5星”需求呢?
画外音:这类“一段时间之后,完成一个任务”的需求很常见。
cron是不是最容易想到的方案?
启动一个cron定时任务,每小时跑一次,将完成时间超过48小时,且仍未评价的订单取出,置为5星,并把评价状态置为已评价。
假设订单表的结构为:
order(oid, finish_time, stars, status, …)
更具体的,定时任务每隔一个小时会这么做一次:
select oid from order where finish_time > 48 and status=0;
update order set stars=5 and status=1 where oid in[…];
如果数据量很大,需要分页查询,分页update,这将会是一个for循环。
cron方案有什么不足?
(1)轮询效率比较低;
(2)每次扫库,已经被执行过记录,仍然会被扫描(只是不会出现在结果集中),有重复计算的嫌疑;
(3)时效性不够好,如果每小时轮询一次,最差的情况下,时间误差会达到1小时;
(4)如果通过增加cron轮询频率来减少时间误差,则轮询低效和重复计算的问题会进一步凸显;
对于这类需要延时执行的任务,如何保证效率的同时,又保证实时性呢?
答案是:高效延时消息。
高效延时消息,包含两个重要的数据结构:
(1)环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组);
(2)任务集合,环上每一个slot是一个Set<Task>;
同时,启动一个timer:
(1)此timer每隔1s,在环形队列中移动一格;
(2)用一个Current Index来标识正在检测的slot;
Task结构中有两个很重要的属性:
(1)Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务;
(2)Task-Function:需要执行的任务函数;
如上图,假设当前Current Index指向第一格,当有延时消息到达之后,例如希望3610秒之后,触发一个延时消息任务,只需:
(1)计算这个Task应该放在哪一个slot,现在指向1,3610秒之后,应该是第11格,所以这个Task应该放在第11个slot的Set<Task>中;
(2)计算这个Task的Cycle-Num,由于环形队列是3600格(每秒移动一格,正好1小时),这个任务是3610秒后执行,所以应该绕3610/3600=1圈之后再执行,于是Cycle-Num=1;
Current Index不停的移动,每秒移动一格,当移动到一个新slot,遍历这个slot中对应的Set<Task>,每个Task看Cycle-Num是不是0:
(1)如果不是0,说明还需要多移动几圈,将Cycle-Num减1;
(2)如果是0,说明马上要执行这个Task了,取出Task-Funciton执行,丢给工作线程执行,并把这个Task从Set<Task>中删除;
画外音:注意,不要用timer来执行任务,否则timer会越来越不准。
使用了“延时消息”方案之后,“订单48小时后关闭评价”的需求,只需将在订单关闭时,触发一个48小时之后的延时消息即可:
(1)无需再轮询全部订单,效率高;
(2)一个订单,任务只执行一次;
(3)时效性好,精确到秒;
画外音:控制timer移动频率可以控制精度。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· Ollama——大语言模型本地部署的极速利器
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· Windows编程----内核对象竟然如此简单?
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用