[特殊字符]如何基于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


@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


@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: 阻塞式)

@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

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

    方式CPU 使用延迟网络请求
    阻塞式 BLPOP极低实时1次/响应
    while轮询取决于间隔持续不断
总结
  • 简单性:仅用 ZSet + List 实现核心逻辑。
  • 可靠性:消息在 Redis 中持久化,转移任务通过定时扫描保证到期执行。
  • 扩展性:可通过增加消费者实例横向扩展吞吐量。

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值