核心概念
延时任务(Delayed Task)是指需要在指定时间后执行的任务,而不是立即执行。典型场景包括:
- 订单超时自动取消(下单 30 分钟未支付)
- 优惠券到期提醒(到期前 3 天提醒用户)
- 定时消息推送(预约时间发送通知)
- 会员到期处理(会员到期降级)
延时任务的核心挑战在于:如何高效、可靠地在未来某个时间点触发任务执行。
常见实现方案
1. 定时轮询数据库(最简单但不推荐)
原理:启动定时任务(如每分钟一次),扫描数据库中到期的记录并处理。
@Scheduled(cron = "0 * * * * ?") // 每分钟执行
public void scanExpiredOrders() {
List<Order> expiredOrders = orderMapper.selectList(
new QueryWrapper<Order>()
.eq("status", "UNPAID")
.lt("create_time", LocalDateTime.now().minusMinutes(30))
);
for (Order order : expiredOrders) {
cancelOrder(order.getId());
}
}
优点:实现简单,不依赖额外组件
缺点:
- 数据库压力大(频繁全表扫描)
- 时效性差(最多延迟 1 分钟)
- 分布式环境下需处理重复执行问题
适用场景:数据量小、时效性要求不高的小型系统
2. JDK DelayQueue(单机内存方案)
原理:基于优先级队列(PriorityQueue)+ 延时元素(Delayed)实现的阻塞队列,元素按到期时间排序,只有到期元素才能被取出。
public class DelayedTask implements Delayed {
private final String taskId;
private final long executeTime; // 执行时间戳
public DelayedTask(String taskId, long delayMs) {
this.taskId = taskId;
this.executeTime = System.currentTimeMillis() + delayMs;
}
@Override
public long getDelay(TimeUnit unit) {
long diff = executeTime - System.currentTimeMillis();
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.executeTime, ((DelayedTask) o).executeTime);
}
}
// 使用示例
@Component
public class DelayTaskManager {
private final DelayQueue<DelayedTask> delayQueue = new DelayQueue<>();
@PostConstruct
public void startConsumer() {
new Thread(() -> {
while (true) {
try {
DelayedTask task = delayQueue.take(); // 阻塞直到有到期任务
processTask(task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
public void addTask(String taskId, long delayMs) {
delayQueue.offer(new DelayedTask(taskId, delayMs));
}
}
优点:性能高,时效性准确
缺点:
- 单机方案,不支持集群
- 数据存在内存中,重启后丢失
- 无法处理大量任务(内存限制)
适用场景:单机应用,任务量少,可容忍重启丢失
3. Redis 有序集合 ZSet + 定时扫描
原理:利用 ZSet 的 score 存储任务执行时间戳,定时扫描到期任务并执行。
@Component
public class RedisDelayQueue {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String DELAY_QUEUE_KEY = "delay:queue";
// 添加延时任务
public void addTask(String taskId, long delayMs) {
long executeTime = System.currentTimeMillis() + delayMs;
redisTemplate.opsForZSet().add(DELAY_QUEUE_KEY, taskId, executeTime);
}
// 定时扫描(可用 @Scheduled 或独立线程)
@Scheduled(fixedDelay = 1000) // 每秒扫描
public void consumeTasks() {
long now = System.currentTimeMillis();
// 查询到期任务(score <= now)
Set<String> tasks = redisTemplate.opsForZSet()
.rangeByScore(DELAY_QUEUE_KEY, 0, now);
if (tasks != null && !tasks.isEmpty()) {
for (String taskId : tasks) {
// 使用 Lua 保证原子性(避免重复消费)
Long removed = redisTemplate.execute(
new DefaultRedisScript<>(
"if redis.call('zscore', KEYS[1], ARGV[1]) then " +
" redis.call('zrem', KEYS[1], ARGV[1]) " +
" return 1 " +
"else " +
" return 0 " +
"end",
Long.class
),
Collections.singletonList(DELAY_QUEUE_KEY),
taskId
);
if (removed != null && removed == 1) {
processTask(taskId);
}
}
}
}
}
优点:
- 支持分布式,多实例部署
- 数据持久化,不怕重启
- 时效性较好(秒级)
缺点:
- 需要定时扫描,仍有轮询开销
- 分布式锁保证原子性略复杂
适用场景:中小型分布式系统,延时精度要求秒级
4. Redis Keyspace Notifications(过期通知)
原理:利用 Redis 的 key 过期事件通知机制,key 过期时触发回调。
# redis.conf 配置
notify-keyspace-events Ex
@Configuration
public class RedisExpireConfig {
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory factory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
// 监听所有 DB 的过期事件
container.addMessageListener(new KeyExpirationListener(container),
new PatternTopic("__keyevent@*__:expired"));
return container;
}
}
@Component
public class KeyExpirationListener extends KeyExpirationEventMessageListener {
public KeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString();
if (expiredKey.startsWith("order:")) {
String orderId = expiredKey.substring(6);
cancelOrder(orderId);
}
}
}
// 使用示例
public void createOrder(String orderId) {
// 业务逻辑...
// 设置 30 分钟过期
redisTemplate.opsForValue().set(
"order:" + orderId,
orderId,
30,
TimeUnit.MINUTES
);
}
优点:实现简单,无需轮询
缺点:
- 过期通知不可靠(Redis 文档明确说明可能丢失)
- 通知时 key 已删除,无法获取原始数据
- 高并发下可能丢消息
适用场景:辅助方案,不适合核心业务
5. RabbitMQ 延时队列(推荐)
原理:利用 RabbitMQ 的 死信交换机(DLX) + TTL 实现延时消息。
@Configuration
public class DelayQueueConfig {
// 延时队列(消息过期后转到死信交换机)
@Bean
public Queue delayQueue() {
return QueueBuilder.durable("delay.queue")
.withArgument("x-dead-letter-exchange", "business.exchange")
.withArgument("x-dead-letter-routing-key", "business.key")
.build();
}
// 业务队列(接收死信消息)
@Bean
public Queue businessQueue() {
return new Queue("business.queue", true);
}
@Bean
public DirectExchange businessExchange() {
return new DirectExchange("business.exchange");
}
@Bean
public Binding businessBinding() {
return BindingBuilder.bind(businessQueue())
.to(businessExchange())
.with("business.key");
}
}
// 发送延时消息
public void sendDelayMessage(String orderId, long delayMs) {
rabbitTemplate.convertAndSend("", "delay.queue", orderId, message -> {
message.getMessageProperties().setExpiration(String.valueOf(delayMs));
return message;
});
}
// 消费业务消息
@RabbitListener(queues = "business.queue")
public void handleExpiredOrder(String orderId) {
cancelOrder(orderId);
}
RabbitMQ 3.6+ 延时插件方案:
# 安装插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
@Bean
public CustomExchange delayExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange("delay.exchange", "x-delayed-message", true, false, args);
}
// 发送延时消息
rabbitTemplate.convertAndSend("delay.exchange", "delay.key", orderId, message -> {
message.getMessageProperties().setHeader("x-delay", delayMs);
return message;
});
优点:
- 消息可靠性高(持久化 + 确认机制)
- 支持分布式
- 时效性好(毫秒级)
缺点:
- 需要引入 RabbitMQ
- 延时插件需单独安装
适用场景:生产环境首选,特别是已有 RabbitMQ 的系统
6. 时间轮算法(Netty/Kafka 使用)
原理:类似时钟,将任务分配到不同时间刻度的槽位,指针滚动触发任务。
// Netty 的 HashedWheelTimer
HashedWheelTimer timer = new HashedWheelTimer(
100, TimeUnit.MILLISECONDS, // tickDuration: 每格时长
10 // ticksPerWheel: 时间轮格数
);
// 添加延时任务
timer.newTimeout(timeout -> {
cancelOrder(orderId);
}, 30, TimeUnit.MINUTES);
优点:高性能,适合大量短延时任务
缺点:精度受限于 tick 间隔,单机方案
适用场景:框架内部(如 Dubbo 超时检测),高性能场景
方案选型建议
| 方案 | 时效性 | 可靠性 | 分布式 | 适用场景 |
|---|---|---|---|---|
| 定时轮询 DB | 分钟级 | ⭐⭐ | ✅ | 小型系统 |
| DelayQueue | 毫秒级 | ⭐⭐ | ❌ | 单机应用 |
| Redis ZSet | 秒级 | ⭐⭐⭐ | ✅ | 中小型系统 |
| Redis 过期通知 | 秒级 | ⭐ | ✅ | 辅助方案 |
| RabbitMQ | 毫秒级 | ⭐⭐⭐⭐⭐ | ✅ | 生产推荐 |
| 时间轮 | 毫秒级 | ⭐⭐⭐ | ❌ | 高性能场景 |
线程安全与分布式考量
1. 分布式锁防止重复执行
public void processTask(String taskId) {
String lockKey = "lock:task:" + taskId;
Boolean lock = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(lock)) {
try {
// 执行任务
doProcess(taskId);
} finally {
redisTemplate.delete(lockKey);
}
}
}
2. 幂等性设计
- 任务执行前检查状态(如订单是否已取消)
- 使用唯一 ID 防重表
- 数据库乐观锁(version 字段)
面试答题总结
核心考点:
- 延时任务的常见场景(订单超时、定时提醒等)
- 多种实现方案的原理对比(重点是 RabbitMQ 和 Redis)
- 分布式环境下的重复执行问题
- 方案选型依据(可靠性、性能、时效性)
回答框架:
- 先说典型场景(如订单超时取消)
- 列举方案:简单的轮询 DB → DelayQueue → Redis → RabbitMQ
- 重点讲 RabbitMQ 死信队列原理(生产常用)
- 补充分布式锁和幂等性设计
- 结合项目经验(如”我们用 RabbitMQ 延时队列处理订单超时”)