Redis里头怎么搞超时队列,技术细节和处理经验聊聊
- 问答
- 2025-12-30 21:19:14
- 1
在Redis里搞超时队列,说白了就是处理一堆“到点儿了就该干某件事”的任务,比如用户下了单半小时没付款,系统得自动关单;或者你叫了个外卖,骑手接单后五分钟没动,系统得提醒一下,核心思想就是想办法让Redis告诉我们哪个任务到时间了。
最直接、也是最经典的办法就是用Redis的有序集合(Sorted Set),这个结构特别适合干这个事儿,你把每个任务当成一个成员(member),然后给这个成员分配一个分数(score),这个分数不是别的,就是任务该被执行的时间戳,比如一个订单应该在下午2点30分过期,你就把它的过期时间戳(1653981000)作为分数存进去。
你需要一个家伙不停地去检查这个有序集合,怎么检查呢?很简单,用一个命令 ZRANGEBYSCORE,让它把分数小于等于“当前时间戳”的所有成员都捞出来,比方说,现在是下午2点30分01秒,你一下令,所有在2点30分01秒及之前就该过期的订单就全被你查出来了,这些就是到点的任务。
捞出来之后,你就要处理它们了,比如修改数据库里的订单状态为“已关闭”,处理完之后,你得记得把这些任务从有序集合里删掉(用 ZREM),不然下次检查它们还会被捞出来,就重复处理了。
那这个“不停地检查”的动作由谁来做呢?通常你得自己写一个后台线程或者定时任务(比如用Cron,或者Java里的ScheduledExecutorService),让它每隔一小段时间(比如一秒,或者更短)就执行一遍上面的“查询-处理-删除”流程,这个方法非常可靠,因为你完全掌控着处理的节奏和逻辑,但缺点就是这个间隔时间不好设定:设短了,Redis压力大;设长了,任务处理就不够及时,可能有几分钟的延迟,这就是个权衡。

另一个现在特别流行的方案是利用Redis的流(Stream) 数据结构,这是Redis 5.0之后才有的新玩意儿,它本身就更像一个消息队列,它搞超时队列的思路更巧妙一些,不是靠轮询,而是靠“阻塞等待”。
你可以把每个任务当作一条消息放进一个Stream里,关键点在于,你在放消息的时候,可以指定一个“空闲时间”,你可以启动一个或多个消费者(Consumer)来读这个Stream,这些消费者可以执行一个阻塞读取的命令,意思是“给我下一个消息,如果现在没有,你就等着,但最多等N秒”。
这里面的魔法在于,如果一个消息在Stream里待的时间超过了它被设定的“空闲时间”,它就会自动被标记为“待处理”,这时候,那些正在阻塞等待的消费者就能立刻拿到这条“超时”的消息去处理,这种方式更像是“事件驱动”,有任务超时了才触发处理,而不是傻傻地每隔一秒去问一次,这样既及时,又减轻了不必要的请求压力,但缺点是Stream的结构相对复杂一些,要理解消费者组、确认机制等概念,不然消息可能会丢。

根据“程序员囧辉”在他的文章《探秘Redis的五种数据结构》中提到Stream的应用时,也强调了其作为延迟队列的潜力,指出其基于事件通知的特性比轮询方式更高效。
在实际处理中,不管你用哪种方法,有几个坑是必须要小心跳过去的: 第一,重复处理问题,比如你用有序集合方案,万一你的程序在从集合里捞出任务、处理成功、但还没从集合里删除这个任务的节骨眼上崩溃了,等你程序重启,这个任务又会被捞出来再处理一次,这就可能造成一个订单被关闭两回,解决办法通常是让处理逻辑具备幂等性,简单说就是,你处理一次和处理N次的结果都一样,比如关单前先查一下数据库,如果订单已经是关闭状态,那就直接跳过,别傻乎乎地再关一次。
第二,消息丢失问题,主要发生在程序处理任务的过程中,可能任务刚从Redis里取出来,程序就崩了,这个任务就再也没人管了,对于有序集合,因为你是一次捞一批,这个问题更明显,对于Stream,它有完善的消息确认(ACK)机制,如果消费者崩溃,超时后消息会重新被其他人处理,相对更安全,所以如果用有序集合,有时需要考虑更复杂的补偿机制。
第三,性能问题,如果你的超时任务量非常大,比如一秒有上万个,那你那个轮询的线程可能忙不过来,或者一次从Redis拉取太多数据导致网络阻塞,这时候你可能需要搞多个队列,或者用多个消费者并行处理,也就是做分片,把不同的订单号哈希到不同的队列里,每个队列用一个线程处理,压力就分摊了。
所以总结一下,在Redis搞超时队列,有序集合是经典通用方案,控制简单但需要轮询;Stream是现代化方案,更高效但概念复杂点,选哪个看你的业务场景和对及时性、可靠性的具体要求,核心经验就是处理好幂等性,防丢防重复,再根据数据量做好性能规划。
本文由度秀梅于2025-12-30发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://haoid.cn/wenda/71493.html
