什么是延时任务
延时任务,顾名思义,就是延迟一段时间后才执行的任务。举个例子,假设我们有个发布资讯的功能,运营需要在每天早上7点准时发布资讯,但是早上7点大家都还没上班,这个时候就可以使用延时任务来实现资讯的延时发布了。只要在前一天下班前指定第二天要发送资讯的时间,到了第二天指定的时间点资讯就能准时发出去了。如果大家有运营过公众号,就会知道公众号后台也有文章定时发送的功能。总而言之,延时任务的使用还是很广泛的。关于延时任务的实现方式,我知道的就不下于3种,后面会逐一介绍,今天就讲下如何用redis实现延时任务。
延时任务的特点
在介绍具体方案之前,我们不妨先想一下要实现一个延时系统,有哪些内容是必须存储下来的(这里的存储不一定是指持久化,也可以是放在内存中,取决于延时任务的重要程度)。首先要存储的就是任务的描述。假如你要处理的延时任务是延时发布资讯,那么你至少要存储资讯的id吧。另外,如果你有多种任务类型,比如:延时推送消息、延时清洗数据等等,那么你还需要存储任务的类型。以上总总,都归属于任务描述。除此之外,你还必须存储任务执行的时间点吧,一般来说就是时间戳。此外,我们还需要根据任务的执行时间进行排序,因为延时任务队列里的任务可能会有很多,只有到了时间点的任务才应该被执行,所以必须支持对任务执行时间进行排序。
使用Redis实现延时任务
以上就是一个延迟任务系统必须具备的要素了。回到redis,有什么数据结构可以既存储任务描述,又能存储任务执行时间,还能根据任务执行时间进行排序呢?想来想去,似乎只有Sorted Set了。我们可以把任务的描述序列化成字符串,放在Sorted Set的value中,然后把任务的执行时间戳作为score,利用Sorted Set天然的排序特性,执行时刻越早的会排在越前面。这样一来,我们只要开一个或多个定时线程,每隔一段时间去查一下这个Sorted Set中score小于或等于当前时间戳的元素(这可以通过zrangebyscore命令实现),然后再执行元素对应的任务即可。当然,执行完任务后,还要将元素从Sorted Set中删除,避免任务重复执行。如果是多个线程去轮询这个Sorted Set,还有考虑并发问题,假如说一个任务到期了,也被多个线程拿到了,这个时候必须保证只有一个线程能执行这个任务,这可以通过zrem命令来实现,只有删除成功了,才能执行任务,这样就能保证任务不被多个任务重复执行了。
接下来看代码。首先看下项目结构:
一共4个类:Constants类定义了Redis key相关的常量。DelayTaskConsumer是延时任务的消费者,这个类负责从Redis拉取到期的任务,并封装了任务消费的逻辑。DelayTaskProducer则是延时任务的生产者,主要用于将延时任务放到Redis中。RedisClient则是Redis客户端的工具类。
最主要的类就是DelayTaskConsumer和DelayTaskProducer了。
我们先来看下生产者DelayTaskProducer:
代码很简单,就是将任务描述(为了方便,这里只存储资讯的id)和任务执行的时间戳放到Redis的Sorted Set中。
接下来是延时任务的消费者DelayTaskConsumer:
public class DelayTaskConsumer {
private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
public void start(){
scheduledExecutorService.scheduleWithFixedDelay(new DelayTaskHandler(),1,1, TimeUnit.SECONDS);
}
public static class DelayTaskHandler implements Runnable{
@Override
public void run() {
Jedis client = RedisClient.getClient();
try {
Set ids = client.zrangeByScore(Constants.DELAY_TASK_QUEUE, 0, System.currentTimeMillis(),
0, 1);
if(ids==null||ids.isEmpty()){
return;
}
for(String id:ids){
Long count = client.zrem(Constants.DELAY_TASK_QUEUE, id);
if(count!=null&&count==1){
System.out.println(MessageFormat.format("发布资讯。id - {0} , timeStamp - {1} , " +
"threadName - {2}",id,System.currentTimeMillis(),Thread.currentThread().getName()));
}
}
}finally {
client.close();
}
}
}
}
首先看start方法。在这个方法里面我们利用Java的ScheduledExecutorService开了一个调度线程池,这个线程池会每隔1秒钟调度DelayTaskHandler中的run方法。
DelayTaskHandler类就是具体的调度逻辑了。主要有2个步骤,一个是从Redis Sorted Set中拉取到期的延时任务,另一个是执行到期的延时任务。拉取到期的延时任务是通过zrangeByScore命令实现的,处理多线程并发问题是通过zrem命令实现的。代码不复杂,这里就不多做解释了。
接下来测试一下:
我们首先生产了4个延时任务,执行时间分别是程序开始运行后的5秒、10秒、15秒、20秒,然后启动了10个消费者去消费延时任务。运行效果如下:
作者:Java填坑之路
链接:https://www.jianshu.com/p/c434fa4e5ed1
共同学习,写下你的评论
评论加载中...
作者其他优质文章