当前位置:首页 > 问答 > 正文

Redis那消息延迟队列功能,真是让人意想不到的好用和方便

(引用来源:CSDN博客《Redis实现延迟队列的几种方式》,个人开发者实践分享)

Redis那个消息延迟队列的功能,我第一次用的时候,感觉就像是发现了一个隐藏的宝藏,以前一想到要做个延时任务,比如订单半小时后自动关闭,或者用户下单后提醒支付,脑子里冒出来的都是又重又复杂的方案,要么用数据库定时轮询,要么上专门的消息队列中间件,像RabbitMQ那种,数据库轮询太笨重了,隔几秒就去扫一遍表,对数据库压力大,而且延迟还不精确,专业的消息队列呢,学习成本和维护成本对当时我们那个小项目来说,又有点太高了,直到团队里一个前辈说,“为啥不用Redis试试?它那个有序集合和键过期通知就能搞,轻巧又方便。” 我抱着怀疑的态度试了试,结果真的被它的简洁和高效惊艳到了。

(引用来源:开源中国社区某技术讨论帖,关于Redis ZSet实现延迟队列的优缺点)

它的核心思路其实特别直观,一点都不绕,Redis不是有个数据结构叫有序集合(ZSet)嘛,它可以给每个成员配一个分数(score),这个功能平时可能也就是做个排行榜,但用在延迟队列上简直是绝配,具体怎么做呢?就是把需要延迟处理的消息本身作为成员,而那个分数,就设置成这个消息应该被处理的时间戳,比如现在是下午3点,我要让一个任务在3点30分执行,那我就把这个任务塞进ZSet,分数设为3点半对应的时间戳,后台我们起一个独立的线程或者服务,这个服务啥也不干,就不断地去这个ZSet里“扫一眼”,怎么扫呢?它不是漫无目的地扫,而是查询当前时间戳之前的所有成员,也就是分数小于等于当前时间戳的那些任务,因为这些任务的预定执行时间已经到了嘛,所以就把它们统统取出来,挨个处理掉,处理完之后,再从ZSet里把这些成员删除,避免下次又被扫到,这个循环检查的过程,其实就是个简单的死循环,里面用个小小的睡眠(比如1秒)来控制检查频率,代码写起来非常容易理解。

(引用来源:GitHub上某个热门开源项目TaskQueue的源码注释,其延迟任务模块采用Redis实现)

你可能觉得,这不还是轮询吗?跟扫数据库有啥区别?妙就妙在Redis是纯内存操作,速度极快,每次检查的开销微乎其微,即使你每秒查一次,对Redis的压力也几乎可以忽略不计,延迟的精度比数据库轮询高多了,你可以很轻松地做到秒级甚至更精确的延迟,我当时就用这个方式重构了项目的订单超时关单功能,代码量从原来用数据库定时任务时的一大坨,减少到了寥寥几十行,清晰明了,运维起来也省心,再也不用担心定时任务卡死或者漏执行了。

(引用来源:掘金专栏文章《小而美的Redis延迟队列方案》,作者对比了多种实现细节)

后来我又了解到另一种玩法,利用了Redis的键空间通知功能,就是当你给Redis里的一个键设置了过期时间(TTL),等这个键过期被删除的时候,Redis可以发布一个通知到特定的频道,我们可以利用这个机制:每当有延迟任务进来,我就生成一个唯一的键,把这个任务详情存进去(比如存成Hash),然后给这个键设置一个等于延迟时间的TTL,启动一个客户端订阅那些键过期的事件通知,一旦时间到了,键自动过期,Redis就会发出通知,我的订阅客户端收到通知后,就知道“哦,有个任务到点了”,然后根据键名去找到对应的任务详情进行处理,这种方式更像是事件驱动,看起来更“优雅”,因为它是被动接收通知,而不是主动轮询。

(引用来源:Stack Overflow上一个高赞回答,详细解释了基于TTL和Pub/Sub的延迟队列潜在问题)

在实际用的时候,我发现这种键过期通知的方式得小心点,Redis的键空间通知默认是关闭的,需要配置开启,而且它可能会有一些性能损耗,更关键的是,这个通知机制不是完全可靠的,它采用的是“发后即忘”的模式,如果当时订阅的客户端刚好断线了,可能就错过这个通知了,导致任务丢失,对于要求比较严格、不能丢任务的场景,用ZSet轮询的方式感觉更踏实一些,因为任务数据一直持久在ZSet里,直到被主动取走删除,心里更有底,也可以结合两种方式,取长补短。

(引用来源:个人项目实践经验总结)

Redis实现延迟队列,给我的最大感受就是“恰到好处的简单”,它没有那些专业消息队列令人眼花缭乱的高级功能,什么事务、持久化策略、复杂的路由规则,但对于最核心的“延迟一段时间后执行”这个需求,它用自己内置的、非常基础的数据结构和特性,就给出了一个极其轻量、高效且易于理解和实现的方案,特别适合那些不想引入重型中间件,但又需要可靠延迟功能的中小型项目,它让我意识到,有时候解决问题的好工具,不一定是最强大的那个,而是最贴合实际需求、用起来最顺手的那一个,Redis的延迟队列功能,就是这样一个意想不到却非常好用和方便的存在。

Redis那消息延迟队列功能,真是让人意想不到的好用和方便