背景
实际项目当中,经常会使用延迟队列,执行一些延迟任务,比如:
如果第一次通知失败,就延迟 X 分钟再通知一次,这在构建webhook的项目中很常见。 如果用户成功消费,X 天后没有评论,就默认给予好评。 这些任务具有的共同点可以抽象为:在某项任务执行完毕后,延迟一定时间执行另外一项任务。
我们需要什么
可以选择的技术方案
定时扫表
服务启动时,开启一个异步协程 → 定时扫描 msg table,到了事件触发事件,调用对应的 handler
- 优点:简单容易实现,而且容易理解
- 缺点:
- 每一个需要定时 / 延时任务的服务,都需要一个 msg table 做额外存储 → 存储与业务耦合,
- 定时扫描 → 时间不好控制,可能会错过触发时间。
- 对 msg table instance 是一个负担。反复有一个服务不断对数据库产生持续不断的压力
- 会面临每次需要遍历庞大任务数据,并且执行时间判断造成的误差会导致某些任务也许无法执行的窘境
Kafka
- 针对不同的延迟时间发布到不同的 topic 中,比如 topic_1s, topic_2s.
- 这种设计在延时时间比较固定的场景下问题不太大,但如果是延时时间变化比较大会导致 topic 数目过多,会把磁盘从顺序读写会变成随机读写从导致性能衰减,同时也会带来其他类似重启或者恢复时间过长的问题
Redis
- 延迟队列主要使用了 Redis 的数据结构:Sorted Set
- Sorted Set 是一个有序的 Set,Set 内元素的排序基于其加入集合时指定的 Score。通过 ZRANGEBYSCORE ,可以得到基于 Score 在指定区间内的元素(排序)。
- 基于 Sorted Set 的延时队列模型如下:
- SortSet 的 key 作为业务维度的属性(队列)名字,比如一种命名方式为 <业务: 命名空间: 队列名>
- SortSet 中的元素做为任务消息,Score 视为本任务延迟的时间(戳)
时间轮
- 时间轮,简单理解就是一个时钟表盘,指针每隔一段时间前进一格,走玩一圈是一个周期。而需要执行的任务就放置在表盘的刻度处,当指针走到该位置时,就执行相应的任务。具体图片如下所示
- 时间轮是一个环形队列,底层实现就是一个固定长度的数组,数组中的每个元素存储一个双向列表,选择双向列表的原因是在O(1)时间复杂度实现插入和删除操作。而这个双向链表存储的就是要在该位置执行的任务。
举例分析,假设时间轮盘每隔1s前进一格,那么上图中的时间轮盘的周期就是12s,如果此时时间在刻度为0的位置,此时需要添加一个定时任务,需要10s后执行,那么该任务就需要放到刻度10处。当指针到达刻度10时,执行在该位置上,双向链表存储的所有任务。
参考
|