本文基于 Redis 的 ZSet 数据结构设计一个简单、通用的延迟消息队列,可以利用 ZSet 的有序特性(按分数排序)来实现延迟触发。以下是设计方案和关键实现代码(使用 Spring 的 RedisTemplate
):
设计思路
-
核心数据结构:
- ZSet (Sorted Set) :存储消息,
score
= 消息触发时间戳(毫秒),value
= 序列化的消息内容。 - List:存储已到期的待消费消息(避免轮询 ZSet 的开销)。
- ZSet (Sorted Set) :存储消息,
-
工作流程:
- 生产者:将消息 + 延迟时间放入 ZSet。
- 转移任务:定时扫描 ZSet,将到期的消息移动到 List 队列。
- 消费者:从 List 中阻塞获取并处理消息。
-
优势:
- 简单:仅依赖 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
-
底层命令:
- 该方法底层使用的是 Redis 的
BLPOP
命令(Blocking Left Pop) - 命令格式:
BLPOP key [key ...] timeout
- 该方法底层使用的是 Redis 的
-
工作方式:
java
iniObject message = redisTemplate.opsForList().leftPop( "ready_queue", 30, TimeUnit.SECONDS );
- 当队列中有消息时:立即返回消息
- 当队列为空时:阻塞当前线程,最多等待30秒
- 等待期间如果有消息入队:立即返回该消息
- 超时后仍无消息:返回 null
-
与轮询的区别:
方式 CPU 使用 延迟 网络请求 阻塞式 BLPOP 极低 实时 1次/响应 while轮询 高 取决于间隔 持续不断
总结
- 简单性:仅用 ZSet + List 实现核心逻辑。
- 可靠性:消息在 Redis 中持久化,转移任务通过定时扫描保证到期执行。
- 扩展性:可通过增加消费者实例横向扩展吞吐量。
此方案适用于大部分简单场景下的延迟队列场景(如订单超时、定时任务),如需更高吞吐或严格的消息顺序,可结合 Redis Streams 优化。