延时队列「模型设计」

587 阅读3分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

延迟队列:一种带有 延迟功能 的消息队列

  1. 延时 → 未来一个不确定的时间
  2. mq → 消费行为具有顺序性

这样解释,整个设计就清楚了。你的目的是 延时,承载容器是 mq。

背景

列举一下我日常业务中可能存在的场景:

  1. 建立延时日程,需要提醒老师上课
  2. 延时推送 → 推送老师需要的公告以及作业

为了解决以上问题,最简单直接的办法就是定时去扫表:

服务启动时,开启一个异步协程 → 定时扫描 msg table,到了事件触发事件,调用对应的 handler

几个缺点:

  1. 每一个需要定时/延时任务的服务,都需要一个 msg table 做额外存储 → 存储与业务耦合
  2. 定时扫描 → 时间不好控制,可能会错过触发时间
  3. 对 msg table instance 是一个负担。反复有一个服务不断对数据库产生持续不断的压力

最大问题其实是什么?

调度模型基本统一,不要做重复的业务逻辑

我们可以考虑将逻辑从具体的业务逻辑里面抽出来,变成一个公共的部分。

而这个调度模型,就是 延时队列

其实说白了:

延时队列模型,就是将未来执行的事件提前存储好,然后不断扫描这个存储,触发执行时间则执行对应的任务逻辑。

那么开源界是否已有现成的方案呢?答案是肯定的。Beanstalk (github.com/beanstalkd/…) 它基本上已经满足以上需求

设计目的

  1. 消费行为 at least
  2. 高可用
  3. 实时性
  4. 支持消息删除

一次说说上述这些目的的设计方向:

消费行为

这个概念取自 mq 。mq 中提供了消费投递的几个方向:

  • at most once → 至多一次,消息可能会丢,但不会重复
  • at least once → 至少一次,消息肯定不会丢失,但可能重复
  • exactly once → 有且只有一次,消息不丢失不重复,且只消费一次。

exactly once 尽可能是 producer + consumer 两端都保证。当 producer 没办法保证是,那 consumer 需要在消费前做一个去重,达到消费过一次不会重复消费,这个在延迟队列内部直接保证。

最简单:使用 redis 的 setNX 达到 job id 的唯一消费

高可用

支持多实例部署。挂掉一个实例后,还有后备实例继续提供服务。

这个对外提供的 API 使用 cluster 模型,内部将多个 node 封装起来,多个 node 之间冗余存储。

为什么不使用 kafka?

考虑过类似基于 kafka/rocketmq 等消息队列作为存储的方案,最后从存储设计模型放弃了这类选择。

举个例子,假设以 Kafka 这种消息队列存储来实现延时功能,每个队列的时间都需要创建一个单独的 topic(如: Q1-1s, Q1-2s..)。这种设计在延时时间比较固定的场景下问题不太大,但如果是延时时间变化比较大会导致 topic 数目过多,会把磁盘从顺序读写会变成随机读写从导致性能衰减,同时也会带来其他类似重启或者恢复时间过长的问题。

  1. topic 过多 → 存储压力
  2. topic 存储的是现实时间,在调度时对不同时间 (topic) 的读取,顺序读 → 随机读
  3. 同理,写入的时候顺序写 → 随机写

架构设计

image.png

简单来说:job的操作由 node 完成,node → beanstalk。

API 设计

整体上,只需要提供三个 api:

producer

  1. producer.At(msg []byte, at time.Time)
  2. producer.Delay(body []byte, delay time.Duration)
  3. producer.Revoke(ids string)

consumer

  1. consumer.Consume(consume handler)

handler → 执行函数类型