生产级实战:基于Spring Boot + Redis的分布式延迟队列设计与实现
一、为什么不用 Redis ZSet 的 Simple 方案?
很多教程会教你用 ZADD 添加分数(时间戳),然后用 ZRANGEBYSCORE 轮询。这在生产环境有一个致命问题:CPU空转与惊群效应。
-
Simple方案:每隔100ms全量扫描Key,即使没有数据也会执行。
-
生产级方案:使用 Redis Sorted Set + List + Pub/Sub,结合 Time Wheel (时间轮) 思想,大幅降低Redis IO压力。
二、核心设计:分级时间轮 (Hierarchical Time Wheels)
我们将延迟时间分为两级:
-
近实时轮 (Near-time Wheel):处理接下来1小时内的任务,精度秒级。
-
远时轮 (Far-time Wheel):处理超过1小时的任务,精度分钟级。
数据结构设计:
-
delay:near:{slot}(Sorted Set): score为时间戳,member为jobId。 -
delay:far:{slot}(List): 存储序列化后的Job数据。 -
delay:bucket(List): 暂存区,用于原子化迁移数据。 -
delay:processing(Hash): 正在消费的消息,用于ACK确认。
三、生产级代码实现
1. Maven依赖
2. 核心配置类 (RedisConfig)
为了生产级性能,必须配置连接池和序列化方式。
3. 延迟任务实体 (DelayJob)
4. 生产者:投递延迟消息
这里使用了 Lua脚本 保证原子性,这是生产级代码的标配。
5. 消费者:时间轮驱动与消息处理
这是最核心的部分。我们需要一个后台线程不断扫描时间轮,并将到期的任务转移到就绪队列。
6. 优雅停机与健康检查
生产环境必须考虑JVM退出时的任务回收,否则会导致消息丢失。
四、生产环境优化建议 (大牛视角)
-
幂等性设计:
-
所有的消费者逻辑必须支持幂等。因为网络抖动或重试机制,同一条消息可能会被处理两次。建议使用
jobId作为唯一键,配合数据库的唯一索引或Redis的SETNX。
-
-
内存与持久化:
-
AOF: 务必开启
appendfsync everysec,防止Redis宕机导致大量延迟任务丢失。 -
Key过期: 对于
processingHash中的字段,设置合理的TTL,防止死信堆积。
-
-
监控告警:
-
监控
delay:ready的长度。如果长度持续增长,说明消费能力不足,需要扩容消费者。 -
监控
delay:processing的长度。如果长度过大,说明有大量任务处理超时或被卡住。
-
-
集群模式下的分布式锁:
-
本文使用的是单Redis实例的简单锁。在生产集群环境中,强烈建议使用 Redisson 的
RLock(红锁)或 Zookeeper 来实现分布式选主,确保同一时间只有一个消费者线程在执行scanNearTimeWheel,避免资源浪费和惊群效应。
-
五、总结
这套方案相比简单的 ZRANGEBYSCORE,引入了时间轮分槽和双缓冲队列,极大地降低了Redis的CPU负载。同时,通过 Lua脚本 保证了操作的原子性,通过 分布式锁 和 重试机制 保证了消息的可靠性。
在高并发场景下(如QPS 10k+),此架构已在多个生产项目中验证稳定。如果你面临更高的吞吐需求,可以考虑将 Ready Queue 替换为 Kafka,由 Redis 负责调度,Kafka 负责削峰填谷。
本文由

浙公网安备 33010602011771号