为什么需要延时任务
我们来看一下几个非常常见的业务场景:
- 某电商平台,用户下单半个小时未支付的情况下需要自动取消订单。
- 某媒体聚合平台,每 10 分钟动态抓取某某网站的数据为自己所用。
这些场景往往都要求我们在某指定时间之后去做某个事情,也就是延时去做某个事情。
方案选择
| 方案类型 | 核心优点 | 核心缺点 |
|---|---|---|
| 定时轮询 | 实现极简、无额外中间件、数据持久化可靠 | DB 压力大、实时性差、存在资源浪费 |
| Redis ZSet | 低 DB 压力、支持分布式、查询效率高 | 实时性依赖扫描间隔、需处理分布式锁 |
| MQ 延迟队列 | 秒级实时性、异步解耦、高并发高扩展 | 需 MQ 插件、配置复杂、需处理消息作废 |
| MQ 死信队列 | 基于 MQ 原生特性、无需插件、异常消息隔离 | 实时性略差、超时时间不灵活、消息删除复杂 |
| 时间轮算法 | 执行效率极高(O (1))、无全量轮询、短延迟场景响应快 | 单机版易丢任务、分布式改造需依赖 Redis + 锁、仅适配短延迟场景 |
1.定时轮询
基于 SpringBoot 的 Scheduled 实现,通过定时任务扫描数据库中的订单。详细实现思路:
任务配置:在 SpringBoot 项目中通过
@Scheduled注解配置定时任务,可设置固定频率;(Cron 表达式:是定时任务系统定义执行时间 / 频率的核心,可设置任务按天、按月等规则执行)扫描逻辑:任务执行时,执行 SQL 查询数据库中 "创建时间≤当前时间 - 超时阈值(如 30 分钟)且状态为待支付" 的订单;
取消处理:遍历查询结果,对每个订单先加数据库行锁(
FOR UPDATE)防止并发修改,校验订单状态未变更后,执行 "更新订单状态为已取消 + 恢复商品库存" 操作
2.时间轮算法
时间轮算法是一种高效的定时任务调度算法,核心思想类似钟表的表盘:把时间分成固定的 "刻度",每个刻度对应一个待执行的任务列表;通过一个 "指针" 匀速转动,指针走到哪个刻度,就执行该刻度下的所有任务。
以 "订单 30 分钟超时取消" 为例,时间轮的实现流程可以分为 3 步:
第一步:初始化时间轮
设定 1 分钟 1 个刻度、共 60 个刻度(对应 1 小时),用 Map 存 "刻度 - 订单 ID 列表";
启动独立线程让指针每分钟走 1 格,超过 59 就回到 0 循环。
第二步:添加订单任务到时间轮
当用户创建订单时,获取当前时间的分钟数(比如现在是 5 分)。
计算订单要放入的目标刻度:当前分钟 + 超时时间(30 分钟) →
5+30=35,如果超过 59 就取模(比如 50+30=80 → 80%60=20)。把订单 ID 添加到目标刻度的任务列表中,等待指针到达时处理。
第三步:指针转动,处理超时订单
线程每分钟触发一次,指针向前移动 1 格,获取当前指针指向的刻度。
取出该刻度下的所有订单 ID,执行取消逻辑:先加分布式锁(防止多实例重复处理)→ 校验订单状态还是 "待支付" → 更新订单状态为 "已取消" + 恢复商品库存。
处理完成后,清空该刻度的任务列表,避免重复执行。
核心特点
高效:无需轮询全量数据,仅处理当前刻度的到期任务,时间复杂度 O (1);
适用场景:高并发、短延迟(分钟级 / 小时级)的定时任务,如订单超时、红包过期;
注意:分布式场景需结合 Redis 实现时间轮集群同步,避免单机故障丢失任务。
3.Redis Zset
有序集合(Sorted Set):利用有序集合的特性,定时轮询查找已超时的任务。
数据存储:创建订单时,计算订单超时时间戳(当前时间戳 + 超时时间),将订单 ID 作为
member,超时时间戳作为score,存入 Redis 有序集合;定时扫描:通过 SpringBoot 定时任务(如每 1 分钟执行一次),执行
ZRANGEBYSCORE命令筛选出score ≤ 当前时间戳的订单 ID;分布式锁控制:扫描到超时订单 ID 后,先通过 Redis 的
SETNX获取分布式锁(如SET lock:order:cancel 1 EX 30 NX),防止多实例重复处理;取消处理:持有锁的实例遍历超时订单 ID,校验订单状态后执行取消逻辑,处理完成后通过
ZREM命令将订单 ID 从有序集合中移除;
4.MQ 延迟队列
生产者发消息时,给消息设置延迟时间;
消息先发送到专门的延迟交换机(通常是
x-delayed-message类型);延迟交换机不会立即转发消息,而是暂存消息,直到延迟时间到期;
时间到后,延迟交换机将消息转发到业务队列;
消费者监听业务队列,到点接收消息并执行 "取消订单" 逻辑。
5.MQ 死信队列
消息先发送到「无消费者的普通队列」,设置队列 TTL=30 分钟,队列绑定死信交换机;
消息过期后成为死信,自动路由到死信队列,由消费端处理。