基于 Redis 的延迟队列的实现

业务场景

我们常会遇见这样的一些业务场景:在延迟的一段时间后执行某个任务:

  • 当用户报名了某项活动,在活动开始的前一天自动发送短信提醒用户。
  • 当用户下了订单,如果超过半小时还未支则将订单设置成取消状态,不再让用户进行支付。

方案设计

1、最简单的做法是用定时任务扫描业务表,发现符合相关的条件,则执行相关的业务逻辑。但是这样做的缺点是:
(1)、需要不断的去查询数据库,频繁的进行IO。比如:要将超过半小时还未支付的订单状态该为已取消,可以设置每隔30秒查询出未支付的订单,并计算当前时间与订单创建时间的时间差,如果这个时间差大于等于半小时,则将订单状态更新为已取消。
(2)、在分布式多个节点同时运行的情况下,可能会出现重复执行的情况。
2、基于 Redis 的延时队列进行实现。可以参考有赞团队给出的:有赞延迟队列设计。这样做的好处是:
(1)、解耦。基于消息通知机制,将任务的投递与执行分离。
(2)、减少不必要的数据库IO查询。
(3)、利用简单分布式锁可以实现防止消息的重复投递。

核心逻辑

这个解决方案的核心是:维护一个延迟队列,队列按照延迟任务的执行时间点对任务进行存储。通过一个轮询器不断对延迟队列中的数据进行轮询,判断当前时间是否大于等于队列当中的首条数据的执行时间,如果是,则说明任务的执行时间点到了,将任务放入消费队列(如:使用消息中间件:Kafka,消费者监听到Kafka队列中有数据,则对其进行消费),并删除延迟队列当中的数据。

角色&整体流程

以用户订单超过半小时未支付则将状态设置为取消状态为例,分析一下这里面所设及到的角色和流程。

角色

1、任务池(Job Pool):存储任务元数据。可以手动对任务池的数据进行删除,比如:用户在半小时之内支付完订单,则这个任务就作废了,需要进行删除,这个时候只要在用户支付订单的同时将任务池中的数据删除即可,相当于取消这个任务的执行。
2、延时桶(Delay Bucket):根据业务类型设置多个 Delay Bucket,以执行时间点为纬度存放延时任务,在任务创建时就计算延时执行时间点根据业务类型放入到不同的 Delay Bucket 中。
3、定时器(Timer):不断去轮询各个 Delay Bucket,判断 Delay Bucket 的任务是否到达执行时间点。
4、就绪队列(Ready Queue):相当于一个消费者队列,用于存放要被消费的任务。

流程

1、在用户下单的同时,创建一个任务,设置任务的状态为 Delay,将任务加入到任务池(Job Pool)中,并根据当前的系统时间推算出延时执行时间,将任务放入到延迟桶中(Delay Bucket)。
2、轮询器会轮询 Delay Bucket 中的任务,将符合执行时间的任务放到消费队列当中,消费者监听到消费队列当中有任务,获取到消费队列中的任务,并查询任务池中的元数据是否存在,如果存在,则说明订单还未支付,需要将订单状态改为已取消,如果不存在,则说明订单已经被支付,无需执行额外的逻辑。
3、如果用户在半小时内支付了订单,则根据任务的 Key 值删除掉任务池中的元数据。这样,这条任务就不会被执行。

实现

任务池(Job Pool)

采用 Redis 的 Hash 数据结构进行实现。可以将任务池想象成一个容器。

延时桶队列(Delay Bucket)

采用 Redis 中的 ZSet 数据结构进行实现。以执行时间作为分数。

就绪队列

可以使用消息中间件进行实现,或者直接使用 Redis 也行。

最后

所涉及的代码可以点击这里

  • 参考:有赞延迟队列设计

个人博客:https://www.kangpeiqin.cn/#/index
欢迎与我交流,关注公众号(sunny的技术小屋),获取更多技术相关知识

你可能感兴趣的:(JavaWeb开发,redis,kafka,数据库)