3. 延迟队列核心原理:基于 ZSET 的延迟队列模型
延迟队列的核心在于将任务的执行时间与当前系统时间解耦,让任务在“未来某个时刻”才被消费者取出并处理。Redis 的 ZSET(有序集合) 天然支持按分数排序,这一特性使其成为实现延迟队列的理想底层结构。
3.1 基于 ZSET 的延迟队列模型
ZSET 延迟队列的基本模型包含三个核心角色:生产者、ZSET 存储层、消费者。生产者将任务写入 ZSET,消费者通过轮询或定时任务扫描 ZSET 中分数(时间戳)小于等于当前时间的元素,取出并执行。
核心流程:生产者写入任务时,将“期望执行时间戳”作为 score;消费者使用
ZRANGEBYSCORE 命令获取已到期的任务,并使用 ZREM 移除,确保任务不被重复消费。
3.2 时间轮与 ZSET 对比
时间轮(Timing Wheel)是另一种经典的延迟任务调度算法,常见于 Netty、Kafka 等高性能中间件。下表从多个维度对比两种方案:
| 对比维度 | ZSET 延迟队列 | 时间轮(Timing Wheel) |
|---|---|---|
| 底层存储 | Redis 有序集合(内存 + 持久化) | 环形数组 + 链表(纯内存) |
| 精度 | 毫秒级(取决于时间戳精度) | 受 tick 间隔限制(通常毫秒级) |
| 容量 | 受 Redis 内存限制,可扩展 | 受槽位数量限制,大容量需多层时间轮 |
| 持久化 | 支持(RDB/AOF) | 不支持(进程重启后任务丢失) |
| 分布式支持 | 天然支持(Redis 集群) | 需额外协调(通常单机) |
| 任务取消/更新 | 方便(ZREM / ZADD 更新 score) | 较复杂(需遍历链表查找) |
| 适用场景 | 分布式系统、需要持久化、任务量中等 | 单机高吞吐、低延迟、任务量极大 |
选型建议:在报表削峰场景中,任务通常需要跨服务、跨实例协调,且要求任务不丢失。ZSET 延迟队列凭借 Redis 的持久化和集群能力,更适合作为分布式延迟队列的选型。时间轮更适合单机、超低延迟的定时任务场景。
3.3 Score 作为时间戳的设计思路
ZSET 的 score 是一个 64 位浮点数,可以精确表示毫秒级时间戳。设计时通常采用以下方案:
- 时间戳单位:统一使用毫秒级时间戳(13 位数字),避免秒级精度导致任务提前或延迟执行。
- score 计算公式:
score = 当前系统时间戳(ms) + 延迟时间(ms)。例如:当前时间 1700000000000ms,延迟 5 分钟,则 score = 1700000300000。 - 消费者轮询策略:使用
ZRANGEBYSCORE queue 0 current_timestamp_ms获取所有已到期任务,配合ZREM原子移除。 - 避免分数冲突:如果同一毫秒内写入多个任务,ZSET 会按 member(任务 ID)排序。建议在 member 中拼接随机数或序列号,确保唯一性。
// 生产者:写入延迟任务
long delayMs = 5 * 60 * 1000; // 5分钟
long executeAt = System.currentTimeMillis() + delayMs;
String taskId = UUID.randomUUID().toString();
jedis.zadd("delay_queue", executeAt, taskId);
// 消费者:轮询到期任务
Set<String> readyTasks = jedis.zrangeByScore("delay_queue", 0, System.currentTimeMillis());
for (String taskId : readyTasks) {
// 尝试移除任务,防止并发消费
long removed = jedis.zrem("delay_queue", taskId);
if (removed == 1) {
// 成功获取任务,执行处理逻辑
processTask(taskId);
}
}
注意:ZRANGEBYSCORE 和 ZREM 不是原子操作。在高并发场景下,多个消费者可能同时获取到同一个任务。建议使用 Lua 脚本将“获取并移除”封装为原子操作,或使用 ZPOPMIN(Redis 5.0+)替代。
3.4 延迟队列的可靠性保障
基于 ZSET 的延迟队列虽然简单,但在生产环境中需要额外关注以下可靠性问题:
- 任务丢失:消费者在 ZREM 之后、业务处理完成之前崩溃,任务会丢失。解决方案:引入“处理中”队列或任务状态表。
- 重复消费:多个消费者同时轮询到同一任务。解决方案:使用 Lua 脚本原子操作,或利用 Redis 分布式锁。
- 队列积压:消费者处理速度跟不上任务到期速度。解决方案:增加消费者实例,或使用 Redis 集群分片。
- 时间精度:ZSET 的 score 是浮点数,高并发下可能出现精度误差。解决方案:使用毫秒级时间戳,避免微秒级精度。
最佳实践:在报表削峰场景中,建议将 ZSET 延迟队列与数据库任务表结合使用。ZSET 负责“定时触发”,数据库负责“任务持久化与状态管理”,两者互补,既保证实时性,又保证可靠性。