前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >构建企业级业务高可用的延时消息中

构建企业级业务高可用的延时消息中

作者头像
孙玄@奈学教育
发布2019-11-25 23:03:53
1.1K0
发布2019-11-25 23:03:53
举报
文章被收录于专栏:架构之美架构之美

1.业务场景剖析

公司业务系统(比如:电商系统)中有大量涉及定时任务的业务场景,例如:实现买卖双方在线沟通的IM系统,为了确保接收方能够收到消息,服务端一般都会有重试策略,即服务端在消息发出的一段时间内,如果没收到接收方的确认信息,则重新发送消息。这就是一个典型的定时任务场景—消息发出等待固定的时间后,触发消息重发逻辑,重发逻辑首先判断所发消息是否收到确认信息,如果没有就将对应的消息再发送一次。类似的场景有很多,例如:自动取消长时间未支付的订单、买家收货一段时间以后自动确认打款等等。 应对上述场景比较粗暴的解决方案是定时扫库,例如:业务将订单的支付超时时间定义为2小时。可以每1分钟扫一次订单库,将超时订单取消。显然,此方案不够优雅,主要问题如下:

1.增加数据库读压力;

2.不够精确,会有最长1分钟的滞后;

扫库的方案一般体量不大时可以使用,当业务发展到一定规模后就不再适用。对IM消息重发秒级别的定时需求,只能增加扫库的频率,但过于频繁的扫库很可能会将数据库拖垮。显然需要更优雅的技术方案解决定时任务问题。

2.时间轮算法剖析

时间轮算法可以高效的处理定时任务,并且有非常高的精度。我们以IM的消息重发功能为例介绍下时间轮算法的应用。假设消息发出15秒后触发重发逻辑,可以设计如图1所示的数据结构:

图1 时间轮算法

1.一个包含15个元素的数组,数组每个元素指向一个链表,可以理解为15个桶;

2.Current Pos指向数组中某个桶,每秒钟向下移动一次,指向下个桶;

3.如果Current Pos已经指向最后一个桶,移动时返回数组头部,指向第一个桶;

4.发消息时将相关信息放入Current Pos指向的桶中(作为链表中的一个元素)。

根据图1可以看出,Current Pos的下一个桶(图1数组中下标5)中的数据,就是所有已经发出15秒的消息,我们可以遍历链表,取出数据,逐个触发消息重发逻辑。

需要注意的是Current Pos是一个循环指针(指向数组末端后,下次偏移会重新指向数组头)。由此我们可以用更形象的方式描述这个结构,把数组首尾相连,形成一个“轮子”,也就是时间轮。如图2所示:

图2 时间轮

3.基于Redis实现时间轮

上面介绍的时间轮是将数据放在应用进程内存中的,可靠性比较差,我们可以进一步优化,将时间轮放到公共的存储中,很自然的会想到使用Redis。可以用Redis中的List和String两种数据类型实现时间轮。设计多个List,每个List相当于时间轮中的一个桶,再用一个String保存当前List的Key。如图3所示:

图3 Redis实现时间轮

应用程序通过修改CurrentPos的Value实现时间轮指针的移动。很轻松的将进程内存中的时间轮放到了Redis中,提高了数据可靠性,同时可以多个实例访问时间轮,避免了单点问题。

4.长时间跨度定时需求实现

新的问题来了,现在我们看到的时间轮,可以用来触发秒级别的定时任务,但如果时间跨度比较大,例如小时或者天级别的定时场景,我们就需要一个非常“大”的轮子,将会占用非常多的内存资源。显然不是最优的方案,我们可以继续优化,使用磁盘文件+内存时间轮结合的方案,如图4所示:

图4 长时间跨度定时需求实现方案

1.将数据(需要触发的事件)按触发时间分散存储在多个文件中;

2.每个文件负责存储触发时间在指定区间内的事件,例如:文件A负责区间为2019年11月21日14点~2019年11月21日14点30,则所有在这个时间区间内触发的事件都会存储在这个文件中;

3.内存中只装载最近半小时要触发的事件,并以时间轮形式组织。

应用程序需要选择合适的时机将文件装载到内存,并建立时间轮索引,控制好时间轮转动,将到期事件触发即可。

5.延时服务中台化

到现在为止,上面介绍的模型已经可以满足业务的定时任务需求,但它还只是一个功能逻辑,我们不能让每个有需求的业务方都去实现一个时间轮,重复造轮子。所以需要将方案进一步地下沉,抽象为一个基础的中台服务,提供通用的延时触发能力,对外提供服务。系统架构设计如图5所示:

图5 延时服务中台化

业务模块与延时服务的交互可以使用RPC Over TCP,但是对于延时服务来说,需要调用业务模块的RPC接口来触发延时任务,延时服务与业务模块耦合,不利于系统的稳定性,同时业务也需要实现回调接口,侵入比较大,易用性也不强。我们自然可以想到使用消息队列解耦,新的架构如图6所示:

图6 消息队列解耦

6.延时消息

看到这里很多同学会说,直接用延时消息不是更好嘛?为什么还要花这么大的篇幅,把事情搞这么复杂(架构设计哲学在于大道至简)。确实是这样,但问题在于不是所有的消息队列都支持延时消息,更不是都能支持任意时间的延时,例如:现在使用非常广泛的RocketMQ对延迟消息的支持就不是很友好:只能支持固定几个档位,不能支持任意时间的延迟。所以为了能够满足业务需求,我们使用外部服务+Redis+MQ的方案(图6),以较低的投入快速实现任意时间的延时消息。

7.改造MQ实现延时消息

图6架构设计依赖了外部服务以及Redis等来实现延时消息,由于引入过多的组件,整体服务稳定性会受影响,并不是最好的实现方案。更优雅的方案可以通过改造MQ来实现,把时间轮逻辑做到MQ内部。下面以RocketMQ为例介绍延时消息的实现方案,RocketMQ消息存储模型如图7所示:

图7 RocketMQ消息存储模型

1.消息按顺序存储在CommitLog文件中;

2.Dispatch线程将消息按主题分发到不同的Queue中。

8.思考

基于RocketMQ实现延时消息,除了实现时间轮算法外还会涉及哪些改动?

1.延时消息的特殊处理;

2.主从Broker之间的数据同步;

3.Broker的故障恢复;

4.......

可见,实际情况要复杂的多,还有很多点需要注意,这里也没有全部列出,欢迎大家在留言区讨论补充。

9.总结

针对同一业务需求,会有多种技术方案,单从技术角度看很容易判断出方案的好坏,但我们看问题需要多角度和多维度,不能只关注于方案本身,从上面延时消息实现来看,最优雅的方案同时也是最复杂、实现难度最大方案。反之,借助外部组件可以用较低的投入实现同样的使用效果,虽然有缺陷,但对业务来说感受不到差别,所以我们选择技术方案时不一定要过于追求完美,要结合公司实际情况和团队技术实力,计算投入产出比(ROI),作出合理选择。

架构师最核心的能力是根据场景给出优雅的解决方案,适合就是最好的,既不保守设计又不过度设计。NX的架构师,是把复杂问题简单化,简单问题做没;SB(SomeBody)的架构师,刚好相反,是把简单问题复杂化,复杂问题搞不定!

希望您是一个NX的架构师!那么问题来了,做一个NX的架构师,需要具备哪些思维方式?欢迎留言交流。


近期热文

  1. 大中台模式下如何构建复杂业务核心状态机组件
  2. 基于CAP模型设计企业级真正高可用的分布式锁
  3. 如何设计真正高性能高并发分布式系统(万字长文)
  4. 微服务架构中分布式事务实现方案如何取舍
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 架构之美 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 公司业务系统(比如:电商系统)中有大量涉及定时任务的业务场景,例如:实现买卖双方在线沟通的IM系统,为了确保接收方能够收到消息,服务端一般都会有重试策略,即服务端在消息发出的一段时间内,如果没收到接收方的确认信息,则重新发送消息。这就是一个典型的定时任务场景—消息发出等待固定的时间后,触发消息重发逻辑,重发逻辑首先判断所发消息是否收到确认信息,如果没有就将对应的消息再发送一次。类似的场景有很多,例如:自动取消长时间未支付的订单、买家收货一段时间以后自动确认打款等等。 应对上述场景比较粗暴的解决方案是定时扫库,例如:业务将订单的支付超时时间定义为2小时。可以每1分钟扫一次订单库,将超时订单取消。显然,此方案不够优雅,主要问题如下:
  • 2.时间轮算法剖析
  • 3.基于Redis实现时间轮
  • 4.长时间跨度定时需求实现
  • 5.延时服务中台化
  • 6.延时消息
  • 7.改造MQ实现延时消息
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档