1 背景
在实际的业务开发过程中,经常会遇到任务需要延时执行,这和定时执行有区别,定时强调的是在指定的时间点执行,延时强调的是延迟一段时间后执行,本文主要讲一下定时和延时任务的常用方案。
常见应用场景:
1 订单30分钟未支付则自动取消
2 店铺3天未上新则发送消息提醒
3 购物车里的商品降价通知
4 预订会议室,开始前10分钟提醒
5 用户注册后,3天内完善用户信息提醒
…
2 定时任务
Linux
* * * * *
- - - - -
| | | | |
| | | | ±---- day of week (0 - 7) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
| | | ±--------- month (1 - 12) OR jan,feb,mar,apr …
| | ±-------------- day of month (1 - 31)
| ±------------------- hour (0 - 23)
±------------------------ minute (0 - 59)
字段 | 是否必填 | 允许值 | 允许特殊字符 | 备注 |
---|---|---|---|---|
Seconds | 是 | 0-59 | ,- | 标准实现不支持此字段 |
Minutes | 是 | 0-59 | ,- | |
Hours | 是 | 0-23 | ,- | |
Day of month | 是 | 1-31 | ,- | |
Month | 是 | 1–12 or JAN–DEC | ,- | |
Day of week | 是 | 0–7 or SUN–SAT | ,- | 0/7为周日 |
其中:
/ 表示间隔多久执行一次
, 表示在指定时间分别执行
* 表示每分钟,每小时…,,都要执行
定时强调的是在某个时间点开始执行,强调的是周期性。例如:
★每日凌晨1点30分统计
30 1 * * * cmd
★整点执行
0 * * * *
★每隔15分执行
*/15 * * * *
★某小时每隔15分钟执行
*/15 12 * * *
★在12点和16点,每隔15分钟执行一次
*/15 12,16 * * *
命令
crontab -l 查看
crontab -e 编辑
3 延时任务
2.1 MySQL表实现
将要延时执行的任务信息写入mysql,定时扫描是否有任务到期执行,具体见下图。
ID | 业务ID | 业务类型 | 状态 | 任务执行时间 | 备注 |
---|---|---|---|---|---|
1 | 1001 | 订单 | 0 | 2022-07-01 00:00:00 | 七夕礼物 |
2 | 1002 | 统计 | 1 | 2022-06-01 00:00:00 | 月度总结 |

潜在问题
1 延时能不能精准时间执行?
目前看做不到,crontab存在执行时间粒度问题,如果任务契合crontab的时间粒度,该方案可行
2 任务量大的时候,执行器并发控制问题
执行器的并发和业务服务的并发
3 扫库的性能问题
如果同时生效的任务不多,建议把已执行的任务归档,如果很多,则需要提前处理将要执行的任务,提前取出一段时间的任务,放入Redis缓存起来
2.2 Redis实现
实际上是使用有序集合ZSET结构来实现
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
while (true) {
zrangebyscore key min max WITHSCORES 1
//do something
sleep(ts)
}

步骤
1 将延时任务加到Sorted Set,将延迟时间设为score
2 启动一个线程不断判断Sorted Set中第一个元素的score是否大于当前时间。如果大于,从Sorted Set中移除任务并添加到执行队列中,否则进行短暂休眠后重试
优点
1 时间粒度比crontab小,切可以自定义
2 Redis ZSET和内存上操作,性能更佳
3 集群和持久化保证了高可用
2.3 MQ中间件实现
2.3.1 RabbitMQ 私信队列
原理即producer
为消息设置expiration
值或者为队列设置x-message-ttl
属性,当消息在ttl指定时间内没有被消费,则消息进入私信队列
,只需启动consumer
来消费私信队列即可

2.3.2 ActiveMQ scheduler
1 设置activemq schedulerSupport=“true”
2 设置message AMQ_SCHEDULED_DELAY
$ts = strtotime('2022-06-01 00:00:00');
$queue = new Queue();
return $queue->send($body, 1, $ts);
class Queue
{
public function send($type, $params, $time) {
......
if ($type === self::EXEC_DELAY) {
$delaytime = ($time - time()) * 1000;
$defaultParam = ['AMQ_SCHEDULED_DELAY' => $delaytime];
$param = array_merge($defaultParam, $param);
......
}
}
}
发送后,在activemq的管理界面下的scheduled下,看到消息信息,见下图。

总结:延时方案很多,知其然知其不然。最符合业务的方案就是最好的方案。