🚀如何基于Redis的ZSet数据结构设计一个通用的,简单的,可靠的延迟消息队列,以RedisTemplate为例

本文基于 Redis 的 ZSet 数据结构设计一个简单、通用的延迟消息队列,可以利用 ZSet 的有序特性(按分数排序)来实现延迟触发。以下是设计方案和关键实现代码(使用 Spring 的 RedisTemplate):

设计思路

  1. 核心数据结构

    • ZSet (Sorted Set) :存储消息,score = 消息触发时间戳(毫秒),value = 序列化的消息内容。
    • List:存储已到期的待消费消息(避免轮询 ZSet 的开销)。
  2. 工作流程

    • 生产者:将消息 + 延迟时间放入 ZSet。
    • 转移任务:定时扫描 ZSet,将到期的消息移动到 List 队列。
    • 消费者:从 List 中阻塞获取并处理消息。
  3. 优势

    • 简单:仅依赖 Redis 基础数据结构。
    • 可靠:消息不会丢失(持久化由 Redis 保证)。
    • 高效:通过 List 解耦,消费者无竞争。

关键代码实现

(假设redis配置完成了)

1. 生产者添加消息到延迟队列

java

arduino 复制代码
@Component
public class DelayedQueueProducer {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 添加延迟消息
    public void addDelayedMessage(String message, long delay, TimeUnit unit) {
        String delayedQueueKey = "delayed_queue";
        long triggerTime = System.currentTimeMillis() + unit.toMillis(delay);
        
        // 使用 ZSet 存储: score=触发时间戳, value=消息
        redisTemplate.opsForZSet().add(delayedQueueKey, message, triggerTime);
    }
}
2. 转移任务(定时将到期消息移动到 List)

java

typescript 复制代码
@Component
public class DelayedQueueTransferTask {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Scheduled(fixedRate = 1000) // 每秒执行一次
    public void transferExpiredMessages() {
        String delayedQueueKey = "delayed_queue";
        String readyQueueKey = "ready_queue";
        long now = System.currentTimeMillis();

        // 原子操作: 查询并移除到期消息
        Set<Object> messages = redisTemplate.opsForZSet().rangeByScore(
            delayedQueueKey, 0, now
        );

        if (messages != null && !messages.isEmpty()) {
            // 将消息添加到待消费队列 (List)
            redisTemplate.opsForList().rightPushAll(readyQueueKey, messages);
            
            // 从 ZSet 中移除已转移的消息
            redisTemplate.opsForZSet().removeRangeByScore(
                delayedQueueKey, 0, now
            );
        }
    }
}
3. 消费者(从2中的 List 获取并消费消息,ps: 阻塞式)
typescript 复制代码
@Component
public class DelayedQueueConsumer {
    private static final String READY_QUEUE = "ready_queue";
    //volatile关键字标记
    private volatile boolean running = true;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @PostConstruct
    public void startConsumer() {
        Executors.newSingleThreadExecutor().execute(() -> {
            while (running) {
                try {
                    // 阻塞式获取消息(最长等待30秒)
                    Object message = redisTemplate.opsForList().leftPop(
                        READY_QUEUE, 30, TimeUnit.SECONDS
                    );
                    
                    if (message == null) {
                        // 超时未获取到消息,继续等待
                        continue;
                    }
                    
                    // 处理消息(增加异常捕获)
                    processMessage((String) message);
                } catch (Exception e) {
                    // 处理异常并记录日志
                    handleConsumerError(e);
                }
            }
        });
    }

    private void processMessage(String message) {
        // 实际业务处理逻辑
    }
    
    @PreDestroy
    public void stopConsumer() {
        running = false; // 优雅关闭
    }
}

说明:RedisTemplate 的阻塞式 pop

  1. 底层命令

    • 该方法底层使用的是 Redis 的 BLPOP 命令(Blocking Left Pop)
    • 命令格式:BLPOP key [key ...] timeout
  2. 工作方式

    java

    ini 复制代码
    Object message = redisTemplate.opsForList().leftPop(
        "ready_queue", 
        30, 
        TimeUnit.SECONDS
    );
    • 当队列中有消息时:立即返回消息
    • 当队列为空时:阻塞当前线程,最多等待30秒
    • 等待期间如果有消息入队:立即返回该消息
    • 超时后仍无消息:返回 null
  3. 与轮询的区别

    方式 CPU 使用 延迟 网络请求
    阻塞式 BLPOP 极低 实时 1次/响应
    while轮询 取决于间隔 持续不断

总结

  • 简单性:仅用 ZSet + List 实现核心逻辑。
  • 可靠性:消息在 Redis 中持久化,转移任务通过定时扫描保证到期执行。
  • 扩展性:可通过增加消费者实例横向扩展吞吐量。

此方案适用于大部分简单场景下的延迟队列场景(如订单超时、定时任务),如需更高吞吐或严格的消息顺序,可结合 Redis Streams 优化。

相关推荐
jakeswang4 小时前
应用缓存不止是Redis!——亿级流量系统架构设计系列
redis·分布式·后端·缓存
.Shu.6 小时前
Redis zset 渐进式rehash 实现原理、触发条件、执行流程以及数据一致性保障机制【分步源码解析】
数据库·redis·缓存
君不见,青丝成雪6 小时前
大数据技术栈 —— Redis与Kafka
数据库·redis·kafka
悟能不能悟6 小时前
排查Redis数据倾斜引发的性能瓶颈
java·数据库·redis
切糕师学AI6 小时前
.net core web程序如何设置redis预热?
redis·.netcore
Mi_Manchikkk7 小时前
Java高级面试实战:Spring Boot微服务与Redis缓存整合案例解析
java·spring boot·redis·缓存·微服务·面试
xiao-xiang7 小时前
redis-集成prometheus监控(k8s)
数据库·redis·kubernetes·k8s·grafana·prometheus
TT哇1 天前
@[TOC](计算机是如何⼯作的) JavaEE==网站开发
java·redis·java-ee
蚰蜒螟1 天前
Spring 和 Lettuce 源码分析 Redis 节点状态检查与失败重连的工作原理
java·redis·spring