跳到主要内容

延迟任务系统设计

问题

如何设计一个高精度、高可靠的延迟任务系统?

答案

典型场景

  • 订单 30 分钟未支付自动取消
  • 预约提醒(开会前 15 分钟推送)
  • 优惠券到期前通知
  • 超时未确认收货自动完成

方案对比

方案精度可靠性适用场景
数据库轮询分钟级高(持久化)低频、低精度
Redis ZSet秒级中(需持久化保障)中等量级、秒级精度
RocketMQ 延迟消息固定级别已有 RocketMQ 基建
RabbitMQ 死信 + TTL秒级已有 RabbitMQ 基建
时间轮 (HashedWheelTimer)毫秒级低(内存,单机)单机、高精度
Redisson 延迟队列秒级中高分布式、开箱即用

Redis ZSet 方案

生产者:添加延迟任务
public void addDelayTask(String taskId, String payload, long delaySeconds) {
double executeTime = System.currentTimeMillis() + delaySeconds * 1000;
// ZSet score = 预期执行时间戳
redisTemplate.opsForZSet().add("delay:tasks",
taskId + ":" + payload, executeTime);
}
消费者:轮询到期任务
@Scheduled(fixedDelay = 1000)  // 每秒轮询
public void pollDelayTasks() {
long now = System.currentTimeMillis();
// 取出所有到期的任务(score <= 当前时间)
Set<String> tasks = redisTemplate.opsForZSet()
.rangeByScore("delay:tasks", 0, now, 0, 100);

for (String task : tasks) {
// 原子删除 + 判断:只有删除成功的节点才处理(防并发重复消费)
Long removed = redisTemplate.opsForZSet().remove("delay:tasks", task);
if (removed != null && removed > 0) {
executeTask(task);
}
}
}
Redis ZSet 方案注意点
  • 持久化:Redis 重启可能丢数据,需配合 AOF 或数据库兜底
  • 多实例竞争:用 ZREM 原子性保证只有一个节点消费
  • 大量任务:ZSet 数据量过大影响性能,可按业务类型拆分多个 key

RocketMQ 延迟消息方案

RocketMQ 延迟消息
// RocketMQ 开源版支持 18 个固定延迟级别
// 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
public void sendDelayMessage(String topic, String body, int delayLevel) {
Message message = new Message(topic, body.getBytes());
message.setDelayTimeLevel(delayLevel); // 16 = 30min
rocketMQTemplate.getProducer().send(message);
}

// RocketMQ 5.x 支持任意延迟时间
public void sendTimerMessage(String topic, String body, long delayMs) {
Message message = new Message(topic, body.getBytes());
message.setDeliveryTimestamp(System.currentTimeMillis() + delayMs);
rocketMQTemplate.getProducer().send(message);
}

时间轮(Netty HashedWheelTimer)

Netty 时间轮(单机)
HashedWheelTimer timer = new HashedWheelTimer(
1, TimeUnit.SECONDS, // tick间隔1秒
512 // 槽数
);

// 添加延迟任务
timer.newTimeout(timeout -> {
// 30秒后执行
cancelUnpaidOrder(orderId);
}, 30, TimeUnit.SECONDS);

生产级方案:多层架构

层级职责说明
数据库持久化所有延迟任务入库,状态:待执行/已执行
MQ / Redis触发器到期时触发执行
补偿任务兜底定时扫描数据库中超时未执行的任务

常见面试问题

Q1: 订单超时取消用什么方案最合适?

答案

  • 首选 RocketMQ 延迟消息:可靠性高,不怕丢失
  • 次选 Redis ZSet:精度高、性能好,但需做持久化保障
  • 兜底方案:数据库定时扫描待支付超时订单

详见 订单系统设计

Q2: Redis ZSet 和 MQ 延迟消息怎么选?

答案

对比Redis ZSetMQ 延迟消息
精度秒级(取决于轮询频率)取决于 MQ 实现
可靠性中(需 AOF + 补偿)高(MQ 持久化)
灵活性任意延迟时间开源 RocketMQ 仅固定级别
复杂度需自己写轮询消费MQ 自动投递

Q3: 时间轮的优缺点?

答案

  • 优点:添加/取消任务 O(1)O(1),适合海量定时器(如 Netty 管理数万连接超时)
  • 缺点:单机内存存储,服务重启丢失
  • 适合短时间、高精度、可丢失的场景

Q4: 如何保证延迟任务不丢不重?

答案

  • 不丢:任务入数据库(持久化) + 补偿定时扫描兜底
  • 不重:消费时 ZREM 原子操作 / 数据库状态 CAS 更新 / 幂等处理

相关链接