🚀如何基于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 优化。

相关推荐
奔跑吧邓邓子4 分钟前
【Java实战㊱】Spring Boot邂逅Redis:缓存加速的奇妙之旅
java·spring boot·redis·缓存·实战
失散1313 分钟前
分布式专题——4 大厂生产级Redis高并发分布式锁实战
java·redis·分布式·缓存·架构
AAA修煤气灶刘哥33 分钟前
别懵!从单机锁到 Redisson,分布式锁的坑我全帮你填了
java·redis·spring cloud
jc06201 小时前
4.1-中间件之Redis
数据库·redis·中间件
失散132 小时前
分布式专题——6 Redis缓存设计与性能优化
java·redis·分布式·缓存·架构
不爱洗脚的小滕2 小时前
【Redis】Scan 命令使用教程:高效遍历海量数据
数据库·redis·bootstrap
瑜伽娃娃3 小时前
基于Redis设计一个高可用的缓存
数据库·redis·缓存
一叶飘零_sweeeet8 小时前
从手写 Redis 分布式锁到精通 Redisson:分布式系统的并发控制终极指南
redis·分布式·redisson
睡觉的时候不会困9 小时前
Redis 主从复制详解:原理、配置与主从切换实战
数据库·redis·bootstrap
自学也学好编程11 小时前
【数据库】Redis详解:内存数据库与缓存之王
数据库·redis