范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

如何实现延迟任务,这11种方式才算优雅!

  延迟任务在我们日常生活中比较常见,比如订单支付超时取消订单功能,又比如自动确定收货的功能等等。
  所以本篇文章就来从实现到原理来盘点延迟任务的11种实现方式,这些方式并没有绝对的好坏之分,只是适用场景的不大相同。
  微信公众号:三友的java日记
  DelayQueue
  DelayQueue是JDK提供的api,是一个延迟队列
  DelayQueue泛型参数得实现Delayed接口,Delayed继承了Comparable接口。
  getDelay方法返回这个任务还剩多久时间可以执行,小于0的时候说明可以这个延迟任务到了执行的时间了。
  compareTo这个是对任务排序的,保证最先到延迟时间的任务排到队列的头。来个demo@Getter public class SanYouTask implements Delayed {      private final String taskContent;      private final Long triggerTime;      public SanYouTask(String taskContent, Long delayTime) {         this.taskContent = taskContent;         this.triggerTime = System.currentTimeMillis() + delayTime * 1000;     }      @Override     public long getDelay(TimeUnit unit) {         return unit.convert(triggerTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);     }      @Override     public int compareTo(Delayed o) {         return this.triggerTime.compareTo(((SanYouTask) o).triggerTime);     }  } 复制代码
  SanYouTask实现了Delayed接口,构造参数taskContent:延迟任务的具体的内容delayTime:延迟时间,秒为单位
  测试@Slf4j public class DelayQueueDemo {      public static void main(String[] args) {         DelayQueue sanYouTaskDelayQueue = new DelayQueue<>();          new Thread(() -> {             while (true) {                 try {                     SanYouTask sanYouTask = sanYouTaskDelayQueue.take();                     log.info("获取到延迟任务:{}", sanYouTask.getTaskContent());                 } catch (Exception e) {                 }             }         }).start();          log.info("提交延迟任务");         sanYouTaskDelayQueue.offer(new SanYouTask("三友的java日记5s", 5L));         sanYouTaskDelayQueue.offer(new SanYouTask("三友的java日记3s", 3L));         sanYouTaskDelayQueue.offer(new SanYouTask("三友的java日记8s", 8L));     } } 复制代码
  开启一个线程从DelayQueue中获取任务,然后提交了三个任务,延迟时间分为别5s,3s,8s。
  测试结果:
  成功实现了延迟任务。实现原理
  offer方法在提交任务的时候,会通过根据compareTo的实现对任务进行排序,将最先需要被执行的任务放到队列头。
  take方法获取任务的时候,会拿到队列头部的元素,也就是队列中最早需要被执行的任务,通过getDelay返回值判断任务是否需要被立刻执行,如果需要的话,就返回任务,如果不需要就会等待这个任务到延迟时间的剩余时间,当时间到了就会将任务返回。Timer
  Timer也是JDK提供的api先来demo@Slf4j public class TimerDemo {      public static void main(String[] args) {         Timer timer = new Timer();                  log.info("提交延迟任务");         timer.schedule(new TimerTask() {             @Override             public void run() {                 log.info("执行延迟任务");             }         }, 5000);     }  } 复制代码
  通过schedule提交一个延迟时间为5s的延迟任务
  实现原理
  提交的任务是一个TimerTaskpublic abstract class TimerTask implements Runnable {     //忽略其它属性          long nextExecutionTime; } 复制代码
  TimerTask内部有一个nextExecutionTime属性,代表下一次任务执行的时间,在提交任务的时候会计算出nextExecutionTime值。
  Timer内部有一个TaskQueue对象,用来保存TimerTask任务的,会根据nextExecutionTime来排序,保证能够快速获取到最早需要被执行的延迟任务。
  在Timer内部还有一个执行任务的线程TimerThread,这个线程就跟DelayQueue demo中开启的线程作用是一样的,用来执行到了延迟时间的任务。
  所以总的来看,Timer有点像整体封装了DelayQueue demo中的所有东西,让用起来简单点。
  虽然Timer用起来比较简单,但是在阿里规范中是不推荐使用的,主要是有以下几点原因:Timer使用单线程来处理任务,长时间运行的任务会导致其他任务的延时处理Timer没有对运行时异常进行处理,一旦某个任务触发运行时异常,会导致整个Timer崩溃,不安全ScheduledThreadPoolExecutor
  由于Timer在使用上有一定的问题,所以在JDK1.5版本的时候提供了ScheduledThreadPoolExecutor,这个跟Timer的作用差不多,并且他们的方法的命名都是差不多的,但是ScheduledThreadPoolExecutor解决了单线程和异常崩溃等问题。来个demo@Slf4j public class ScheduledThreadPoolExecutorDemo {      public static void main(String[] args) {         ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2, new ThreadPoolExecutor.CallerRunsPolicy());          log.info("提交延迟任务");         executor.schedule(() -> log.info("执行延迟任务"), 5, TimeUnit.SECONDS);     }  } 复制代码
  结果
  实现原理
  ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,也就是继承了线程池,所以可以有很多个线程来执行任务。
  ScheduledThreadPoolExecutor在构造的时候会传入一个DelayedWorkQueue阻塞队列,所以线程池内部的阻塞队列是DelayedWorkQueue。
  在提交延迟任务的时候,任务会被封装一个任务会被封装成ScheduledFutureTask对象,然后放到DelayedWorkQueue阻塞队列中。
  ScheduledFutureTask
  ScheduledFutureTask实现了前面提到的Delayed接口,所以其实可以猜到DelayedWorkQueue会根据ScheduledFutureTask对于Delayed接口的实现来排序,所以线程能够获取到最早到延迟时间的任务。
  当线程从DelayedWorkQueue中获取到需要执行的任务之后就会执行任务。RocketMQ
  RocketMQ是阿里开源的一款消息中间件,实现了延迟消息的功能,如果有对RocketMQ不熟悉的小伙伴可以看一下我之前写的RocketMQ保姆级教程和RocketMQ消息短暂而又精彩的一生 这两篇文章。
  RocketMQ延迟消息的延迟时间默认有18个等级。
  当发送消息的时候只需要指定延迟等级即可。如果这18个等级的延迟时间不符和你的要求,可以修改RocketMQ服务端的配置文件。来个demo
  依赖     org.apache.rocketmq     rocketmq-spring-boot-starter     2.2.1          org.springframework.boot     spring-boot-starter-web     2.2.5.RELEASE  复制代码
  配置文件rocketmq:   name-server: 192.168.200.144:9876 #服务器ip:nameServer端口   producer:     group: sanyouProducer 复制代码
  controller类,通过DefaultMQProducer发送延迟消息到sanyouDelayTaskTopic这个topic,延迟等级为2,也就是延迟时间为5s的意思。@RestController @Slf4j public class RocketMQDelayTaskController {      @Resource     private DefaultMQProducer producer;      @GetMapping("/rocketmq/add")     public void addTask(@RequestParam("task") String task) throws Exception {         Message msg = new Message("sanyouDelayTaskTopic", "TagA", task.getBytes(RemotingHelper.DEFAULT_CHARSET));         msg.setDelayTimeLevel(2);         // 发送消息并得到消息的发送结果,然后打印         log.info("提交延迟任务");         producer.send(msg);     }  } 复制代码
  创建一个消费者,监听sanyouDelayTaskTopic的消息。@Component @RocketMQMessageListener(consumerGroup = "sanyouConsumer", topic = "sanyouDelayTaskTopic") @Slf4j public class SanYouDelayTaskTopicListener implements RocketMQListener {      @Override     public void onMessage(String msg) {         log.info("获取到延迟任务:{}", msg);     }  } 复制代码
  启动应用,浏览器输入以下链接添加任务
  http://localhost:8080/rocketmq/add?task=sanyou
  测试结果:
  实现原理
  生产者发送延迟消息之后,RocketMQ服务端在接收到消息之后,会去根据延迟级别是否大于0来判断是否是延迟消息如果不大于0,说明不是延迟消息,那就会将消息保存到指定的topic中如果大于0,说明是延迟消息,此时RocketMQ会进行一波偷梁换柱的操作,将消息的topic改成SCHEDULE_TOPIC_XXXX中,XXXX不是占位符,然后存储。
  在BocketMQ内部有一个延迟任务,相当于是一个定时任务,这个任务就会获取SCHEDULE_TOPIC_XXXX中的消息,判断消息是否到了延迟时间,如果到了,那么就会将消息的topic存储到原来真正的topic(拿我们的例子来说就是sanyouDelayTaskTopic)中,之后消费者就可以从真正的topic中获取到消息了。
  定时任务
  RocketMQ这种实现方式相比于前面提到的三种更加可靠,因为前面提到的三种任务内容都是存在内存的,服务器重启任务就丢了,如果要实现任务不丢还得自己实现逻辑,但是RocketMQ消息有持久化机制,能够保证任务不丢失。RabbitMQ
  RabbitMQ也是一款消息中间件,通过RabbitMQ的死信队列也可以是先延迟任务的功能。demo
  引入RabbitMQ的依赖     org.springframework.boot     spring-boot-starter-amqp     2.2.5.RELEASE  复制代码
  配置文件spring:   rabbitmq:     host: 192.168.200.144 #服务器ip     port: 5672     virtual-host: / 复制代码
  RabbitMQ死信队列的配置类,后面说原理的时候会介绍干啥的@Configuration public class RabbitMQConfiguration {          @Bean     public DirectExchange sanyouDirectExchangee() {         return new DirectExchange("sanyouDirectExchangee");     }      @Bean     public Queue sanyouQueue() {         return QueueBuilder                 //指定队列名称,并持久化                 .durable("sanyouQueue")                 //设置队列的超时时间为5秒,也就是延迟任务的时间                 .ttl(5000)                 //指定死信交换机                 .deadLetterExchange("sanyouDelayTaskExchangee")                 .build();     }      @Bean     public Binding sanyouQueueBinding() {         return BindingBuilder.bind(sanyouQueue()).to(sanyouDirectExchangee()).with("");     }      @Bean     public DirectExchange sanyouDelayTaskExchange() {         return new DirectExchange("sanyouDelayTaskExchangee");     }      @Bean     public Queue sanyouDelayTaskQueue() {         return QueueBuilder                 //指定队列名称,并持久化                 .durable("sanyouDelayTaskQueue")                 .build();     }      @Bean     public Binding sanyouDelayTaskQueueBinding() {         return BindingBuilder.bind(sanyouDelayTaskQueue()).to(sanyouDelayTaskExchange()).with("");     }  } 复制代码
  RabbitMQDelayTaskController用来发送消息,这里没指定延迟时间,是因为在声明队列的时候指定了延迟时间为5s@RestController @Slf4j public class RabbitMQDelayTaskController {      @Resource     private RabbitTemplate rabbitTemplate;      @GetMapping("/rabbitmq/add")     public void addTask(@RequestParam("task") String task) throws Exception {         // 消息ID,需要封装到CorrelationData中         CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());         log.info("提交延迟任务");         // 发送消息         rabbitTemplate.convertAndSend("sanyouDirectExchangee", "", task, correlationData);     }  } 复制代码
  启动应用,浏览器输入以下链接添加任务
  http://localhost:8080/rabbitmq/add?task=sanyou
  测试结果,成功实现5s的延迟任务
  实现原理
  整个工作流程如下:消息发送的时候会将消息发送到sanyouDirectExchange这个交换机上由于sanyouDirectExchange绑定了sanyouQueue,所以消息会被路由到sanyouQueue这个队列上由于sanyouQueue没有消费者消费消息,并且又设置了5s的过期时间,所以当消息过期之后,消息就被放到绑定的sanyouDelayTaskExchange死信交换机中消息到达sanyouDelayTaskExchange交换机后,由于跟sanyouDelayTaskQueue进行了绑定,所以消息就被路由到sanyouDelayTaskQueue中,消费者就能从sanyouDelayTaskQueue中拿到消息了
  上面说的队列与交换机的绑定关系,就是上面的配置类所干的事。
  其实从这个单从消息流转的角度可以看出,RabbitMQ跟RocketMQ实现有相似之处。
  消息最开始都并没有放到最终消费者消费的队列中,而都是放到一个中间队列中,等消息到了过期时间或者说是延迟时间,消息就会被放到最终的队列供消费者消息。
  只不过RabbitMQ需要你显示的手动指定消息所在的中间队列,而RocketMQ是在内部已经做好了这块逻辑。
  除了基于RabbitMQ的死信队列来做,RabbitMQ官方还提供了延时插件,也可以实现延迟消息的功能,这个插件的大致原理也跟上面说的一样,延时消息会被先保存在一个中间的地方,叫做Mnesia,然后有一个定时任务去查询最近需要被投递的消息,将其投递到目标队列中。监听Redis过期key
  在Redis中,有个发布订阅的机制
  生产者在消息发送时需要到指定发送到哪个channel上,消费者订阅这个channel就能获取到消息。图中channel理解成MQ中的topic。
  并且在Redis中,有很多默认的channel,只不过向这些channel发送消息的生产者不是我们写的代码,而是Redis本身。这里面就有这么一个channel叫做__keyevent@__:expired,db是指Redis数据库的序号。
  当某个Redis的key过期之后,Redis内部会发布一个事件到__keyevent@__:expired这个channel上,只要监听这个事件,那么就可以获取到过期的key。
  所以基于监听Redis过期key实现延迟任务的原理如下:将延迟任务作为key,过期时间设置为延迟时间监听__keyevent@__:expired这个channel,那么一旦延迟任务到了过期时间(延迟时间),那么就可以获取到这个任务来个demo
  Spring已经实现了监听__keyevent@*__:expired这个channel这个功能,__keyevent@*__:expired中的*代表通配符的意思,监听所有的数据库。
  所以demo写起来就很简单了,只需4步即可
  依赖     org.springframework.boot     spring-boot-starter-data-redis     2.2.5.RELEASE  复制代码
  配置文件spring:   redis:     host: 192.168.200.144     port: 6379 复制代码
  配置类@Configuration public class RedisConfiguration {      @Bean     public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {         RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();         redisMessageListenerContainer.setConnectionFactory(connectionFactory);         return redisMessageListenerContainer;     }      @Bean     public KeyExpirationEventMessageListener redisKeyExpirationListener(RedisMessageListenerContainer redisMessageListenerContainer) {         return new KeyExpirationEventMessageListener(redisMessageListenerContainer);     }  } 复制代码
  KeyExpirationEventMessageListener实现了对__keyevent@*__:expiredchannel的监听
  当KeyExpirationEventMessageListener收到Redis发布的过期Key的消息的时候,会发布RedisKeyExpiredEvent事件
  所以我们只需要监听RedisKeyExpiredEvent事件就可以拿到过期消息的Key,也就是延迟消息。
  对RedisKeyExpiredEvent事件的监听实现MyRedisKeyExpiredEventListener@Component public class MyRedisKeyExpiredEventListener implements ApplicationListener {      @Override     public void onApplicationEvent(RedisKeyExpiredEvent event) {         byte[] body = event.getSource();         System.out.println("获取到延迟消息:" + new String(body));     }  } 复制代码
  代码写好,启动应用
  之后我直接通过Redis命令设置消息,就没通过代码发送消息了,消息的key为sanyou,值为task,值不重要,过期时间为5sset sanyou task   expire sanyou 5 复制代码
  成功获取到延迟任务
  虽然这种方式可以实现延迟任务,但是这种方式坑比较多
  任务存在延迟
  Redis过期事件的发布不是指key到了过期时间就发布,而是key到了过期时间被清除之后才会发布事件。
  而Redis过期key的两种清除策略,就是面试八股文常背的两种:惰性清除。当这个key过期之后,访问时,这个Key才会被清除定时清除。后台会定期检查一部分key,如果有key过期了,就会被清除
  所以即使key到了过期时间,Redis也不一定会发送key过期事件,这就到导致虽然延迟任务到了延迟时间也可能获取不到延迟任务。
  丢消息太频繁
  Redis实现的发布订阅模式,消息是没有持久化机制,当消息发布到某个channel之后,如果没有客户端订阅这个channel,那么这个消息就丢了,并不会像MQ一样进行持久化,等有消费者订阅的时候再给消费者消费。
  所以说,假设服务重启期间,某个生产者或者是Redis本身发布了一条消息到某个channel,由于服务重启,没有监听这个channel,那么这个消息自然就丢了。
  消息消费只有广播模式
  Redis的发布订阅模式消息消费只有广播模式一种。
  所谓的广播模式就是多个消费者订阅同一个channel,那么每个消费者都能消费到发布到这个channel的所有消息。
  如图,生产者发布了一条消息,内容为sanyou,那么两个消费者都可以同时收到sanyou这条消息。
  所以,如果通过监听channel来获取延迟任务,那么一旦服务实例有多个的话,还得保证消息不能重复处理,额外地增加了代码开发量。
  接收到所有key的某个事件
  这个不属于Redis发布订阅模式的问题,而是Redis本身事件通知的问题。
  当监听了__keyevent@__:expired的channel,那么所有的Redis的key只要发生了过期事件都会被通知给消费者,不管这个key是不是消费者想接收到的。
  所以如果你只想消费某一类消息的key,那么还得自行加一些标记,比如消息的key加个前缀,消费的时候判断一下带前缀的key就是需要消费的任务。Redisson的RDelayedQueue
  Redisson他是Redis的儿子(Redis son),基于Redis实现了非常多的功能,其中最常使用的就是Redis分布式锁的实现,但是除了实现Redis分布式锁之外,它还实现了延迟队列的功能。先来个demo
  引入pom     org.redisson     redisson     3.13.1  复制代码
  封装了一个RedissonDelayQueue类@Component @Slf4j public class RedissonDelayQueue {      private RedissonClient redissonClient;      private RDelayedQueue delayQueue;     private RBlockingQueue blockingQueue;      @PostConstruct     public void init() {         initDelayQueue();         startDelayQueueConsumer();     }      private void initDelayQueue() {         Config config = new Config();         SingleServerConfig serverConfig = config.useSingleServer();         serverConfig.setAddress("redis://localhost:6379");         redissonClient = Redisson.create(config);          blockingQueue = redissonClient.getBlockingQueue("SANYOU");         delayQueue = redissonClient.getDelayedQueue(blockingQueue);     }      private void startDelayQueueConsumer() {         new Thread(() -> {             while (true) {                 try {                     String task = blockingQueue.take();                     log.info("接收到延迟任务:{}", task);                 } catch (Exception e) {                     e.printStackTrace();                 }             }         }, "SANYOU-Consumer").start();     }      public void offerTask(String task, long seconds) {         log.info("添加延迟任务:{} 延迟时间:{}s", task, seconds);         delayQueue.offer(task, seconds, TimeUnit.SECONDS);     }  } 复制代码
  这个类在创建的时候会去初始化延迟队列,创建一个RedissonClient对象,之后通过RedissonClient对象获取到RDelayedQueue和RBlockingQueue对象,传入的队列名字叫SANYOU,这个名字无所谓。
  当延迟队列创建之后,会开启一个延迟任务的消费线程,这个线程会一直从RBlockingQueue中通过take方法阻塞获取延迟任务。
  添加任务的时候是通过RDelayedQueue的offer方法添加的。
  controller类,通过接口添加任务,延迟时间为5s@RestController public class RedissonDelayQueueController {      @Resource     private RedissonDelayQueue redissonDelayQueue;      @GetMapping("/add")     public void addTask(@RequestParam("task") String task) {         redissonDelayQueue.offerTask(task, 5);     }  } 复制代码
  启动项目,在浏览器输入如下连接,添加任务
  http://localhost:8080/add?task=sanyou
  静静等待5s,成功获取到任务。
  实现原理
  如下是Redisson延迟队列的实现原理
  SANYOU前面的前缀都是固定的,Redisson创建的时候会拼上前缀。redisson_delay_queue_timeout:SANYOU,sorted set数据类型,存放所有延迟任务,按照延迟任务的到期时间戳(提交任务时的时间戳 + 延迟时间)来排序的,所以列表的最前面的第一个元素就是整个延迟队列中最早要被执行的任务,这个概念很重要redisson_delay_queue:SANYOU,list数据类型,也是存放所有的任务,但是研究下来发现好像没什么用…SANYOU,list数据类型,被称为目标队列,这个里面存放的任务都是已经到了延迟时间的,可以被消费者获取的任务,所以上面demo中的RBlockingQueue的take方法是从这个目标队列中获取到任务的redisson_delay_queue_channel:SANYOU,是一个channel,用来通知客户端开启一个延迟任务
  任务提交的时候,Redisson会将任务放到redisson_delay_queue_timeout:SANYOU中,分数就是提交任务的时间戳+延迟时间,就是延迟任务的到期时间戳
  Redisson客户端内部通过监听redisson_delay_queue_channel:SANYOU这个channel来提交一个延迟任务,这个延迟任务能够保证将redisson_delay_queue_timeout:SANYOU中到了延迟时间的任务从redisson_delay_queue_timeout:SANYOU中移除,存到SANYOU这个目标队列中。
  于是消费者就可以从SANYOU这个目标队列获取到延迟任务了。
  所以从这可以看出,Redisson的延迟任务的实现跟前面说的MQ的实现都是殊途同归,最开始任务放到中间的一个地方,叫做redisson_delay_queue_timeout:SANYOU,然后会开启一个类似于定时任务的一个东西,去判断这个中间地方的消息是否到了延迟时间,到了再放到最终的目标的队列供消费者消费。
  Redisson的这种实现方式比监听Redis过期key的实现方式更加可靠,因为消息都存在list和sorted set数据类型中,所以消息很少丢。
  上述说的两种Redis的方案更详细的介绍,可以查看我之前写的用Redis实现延迟队列,我研究了两种方案,发现并不简单这篇文章。Netty的HashedWheelTimer先来个demo@Slf4j public class NettyHashedWheelTimerDemo {      public static void main(String[] args) {         HashedWheelTimer timer = new HashedWheelTimer(100, TimeUnit.MILLISECONDS, 8);         timer.start();          log.info("提交延迟任务");         timer.newTimeout(timeout -> log.info("执行延迟任务"), 5, TimeUnit.SECONDS);     }  } 复制代码
  测试结果
  实现原理
  如图,时间轮会被分成很多格子(上述demo中的8就代表了8个格子),一个格子代表一段时间(上述demo中的100就代表一个格子是100ms),所以上述demo中,每800ms会走一圈。
  当任务提交的之后,会根据任务的到期时间进行hash取模,计算出这个任务的执行时间所在具体的格子,然后添加到这个格子中,通过如果这个格子有多个任务,会用链表来保存。所以这个任务的添加有点像HashMap储存元素的原理。
  HashedWheelTimer内部会开启一个线程,轮询每个格子,找到到了延迟时间的任务,然后执行。
  由于HashedWheelTimer也是单线程来处理任务,所以跟Timer一样,长时间运行的任务会导致其他任务的延时处理。
  前面Redisson中提到的客户端延迟任务就是基于Netty的HashedWheelTimer实现的。Hutool的SystemTimer
  Hutool工具类也提供了延迟任务的实现SystemTimerdemo@Slf4j public class SystemTimerDemo {      public static void main(String[] args) {         SystemTimer systemTimer = new SystemTimer();         systemTimer.start();          log.info("提交延迟任务");         systemTimer.addTask(new TimerTask(() -> log.info("执行延迟任务"), 5000));     }  } 复制代码
  执行结果
  Hutool底层其实也用到了时间轮。Qurtaz
  Qurtaz是一款开源作业调度框架,基于Qurtaz提供的api也可以实现延迟任务的功能。demo
  依赖     org.quartz-scheduler     quartz     2.3.2  复制代码
  SanYouJob实现Job接口,当任务到达执行时间的时候会调用execute的实现,从context可以获取到任务的内容@Slf4j public class SanYouJob implements Job {     @Override     public void execute(JobExecutionContext context) throws JobExecutionException {         JobDetail jobDetail = context.getJobDetail();         JobDataMap jobDataMap = jobDetail.getJobDataMap();         log.info("获取到延迟任务:{}", jobDataMap.get("delayTask"));     } } 复制代码
  测试类public class QuartzDemo {      public static void main(String[] args) throws SchedulerException, InterruptedException {         // 1.创建Scheduler的工厂         SchedulerFactory sf = new StdSchedulerFactory();         // 2.从工厂中获取调度器实例         Scheduler scheduler = sf.getScheduler();          // 6.启动 调度器         scheduler.start();          // 3.创建JobDetail,Job类型就是上面说的SanYouJob         JobDetail jb = JobBuilder.newJob(SanYouJob.class)                 .usingJobData("delayTask", "这是一个延迟任务")                 .build();          // 4.创建Trigger         Trigger t = TriggerBuilder.newTrigger()                 //任务的触发时间就是延迟任务到的延迟时间                 .startAt(DateUtil.offsetSecond(new Date(), 5))                 .build();          // 5.注册任务和定时器         log.info("提交延迟任务");         scheduler.scheduleJob(jb, t);     } } 复制代码
  执行结果:
  实现原理
  核心组件Job:表示一个任务,execute方法的实现是对任务的执行逻辑JobDetail:任务的详情,可以设置任务需要的参数等信息Trigger:触发器,是用来触发业务的执行,比如说指定5s后触发任务,那么任务就会在5s后触发Scheduler:调度器,内部可以注册多个任务和对应任务的触发器,之后会调度任务的执行
  启动的时候会开启一个QuartzSchedulerThread调度线程,这个线程会去判断任务是否到了执行时间,到的话就将任务交给任务线程池去执行。无限轮询延迟任务
  无限轮询的意思就是开启一个线程不停的去轮询任务,当这些任务到达了延迟时间,那么就执行任务。demo@Slf4j public class PollingTaskDemo {      private static final List DELAY_TASK_LIST = new CopyOnWriteArrayList<>();      public static void main(String[] args) {         new Thread(() -> {             while (true) {                 try {                     for (DelayTask delayTask : DELAY_TASK_LIST) {                         if (delayTask.triggerTime <= System.currentTimeMillis()) {                             log.info("处理延迟任务:{}", delayTask.taskContent);                             DELAY_TASK_LIST.remove(delayTask);                         }                     }                     TimeUnit.MILLISECONDS.sleep(100);                 } catch (Exception e) {                 }             }         }).start();          log.info("提交延迟任务");         DELAY_TASK_LIST.add(new DelayTask("三友的java日记", 5L));     }      @Getter     @Setter     public static class DelayTask {          private final String taskContent;          private final Long triggerTime;          public DelayTask(String taskContent, Long delayTime) {             this.taskContent = taskContent;             this.triggerTime = System.currentTimeMillis() + delayTime * 1000;         }     }  } 复制代码
  任务可以存在数据库又或者是内存,看具体的需求,这里我为了简单就放在内存里了。
  执行结果:
  这种操作简单,但是就是效率低下,每次都得遍历所有的任务。

霞客读三国(第五回09)虎牢关三英战吕布虎牢关三英战吕布手机壁纸三英战吕布正议间,吕布复引兵搦战。八路诸侯齐出。公孙瓒挥槊亲战吕布。战不数合,瓒败走。吕布纵赤兔马赶来。那马日行千里,飞走如风。(霞客白马将军公孙瓒还是有两幻塔盾斧搭配选择攻略幻塔盾斧搭配有什么推荐在幻塔游戏中,盾斧拥有两种形态,可攻可守非常的好用,那么武器盾斧怎么搭配比较合适呢,不少的玩家还不是好很清楚要怎么选择盾斧搭配比较好,今天小编给大家分享的就是幻塔盾斧搭配选择攻略,梦幻西游网页版2022元旦节活动四海来朝玩法攻略新鲜出炉四海朝金殿,三界贺新春。梦幻西游网页版2022元旦节活动四海来朝正式上线。本次活动持续时间为2021年12月30日14点至2022年1月5日14点。少侠们,快和骨妹一起看看2022热血传奇之怒火合击迷失圣物玩法奖励详解哈喽大家好我是欧尼,感谢观看本期内容,喜欢的朋友记得点赞关注留言评论哦。感兴趣的想一起玩的小伙伴可以私信发送地址两个字或者关注公众号铁锤传奇获取链接领取礼包。本期给大家介绍一下怒火热血传奇之怒火合击迷失首领玩法奖励介绍哈喽大家好我是欧尼,感谢观看本期内容,喜欢的朋友记得点赞关注留言评论哦。感兴趣的想一起玩的小伙伴可以私信发送地址两个字或者关注公众号铁锤传奇获取链接领取礼包。本期我们一起来看看迷失双榜第一战士终于削弱,永久英雄9选1,23碎片兑换暃新皮肤王者荣耀云中赛年将至,新赛年的故事围绕云中蝶展开,与不夜长安一样,新赛年也会发布具有云中特色的英雄,而目前官方已接连公布了云中赛年新英雄团的悬念海报或动画,那么都有哪些彩蛋线索出现传奇世界细数传奇世界中的强力套装大家好,我是知识嗷嗷丰富,嗓音贼拉炫酷,光一个背影往那一杵,就能吸引粉丝无数的小顾。我们今天来看一下我们手游里有多少种套装,也让我们在收集装备的路上显得不那么迷茫。传奇世界里游戏装热血传奇道士没有屠龙级别的代表装备?并非如此,只是你不知道各位老板大家好啊,各位纵横传奇这么些年来,相比是听闻过甚至亲眼见到过屠龙的英姿,相信大家也都知道屠龙是战士这一职业的代表武器了,可你们知道道士的代表武器吗?相信大家也看过不少文章什元气骑士3。4。2版本速报,枭首者加强,死灵法师重回巅峰大家好!我是元气老骑士元气宅。随着元气骑士新年预告版本的发布,老宅此时的心情,真的可以用度日如年来形容。一来是期待新版本快些来临,好一睹全新玩法的风采。二来是盼望日子过得快些,好尽模玩周边EVASTORE推出福音战士新剧场版NERV专用侧背包今年迎向最终章的福音战士新剧场版终,虽说电影已上映超过半年,但周边商品可说是持续推出不间断。新世纪福音战士的官方商店EVANGELIONSTORE近日宣布将推出在福音战士新剧场版Q三国杀技能太阴间怎么办?我发动技能抽取三国杀技能太阴间怎么办?我发动技能抽取!牌佬一定都知道下面这张牌,有的肯定倍感亲切,有的看见就恶心,没错,就是这闻名天下的技能抽取,技抽面前,众生平等。正是因为游戏王环境如此,怪兽
从娃哈哈AD钙牙膏看跨界美妆的三种玩法娃哈哈的日化产品在近日悄然上线。在品牌的天猫旗舰店内,上架了AD钙儿童牙膏系列,产品为按压式设计,三种口味,单件售价34。9元,略高于青蛙王子舒客等同类型产品,同时披露的还有AD钙BeautyWonders唯芙诗,为什么补水会疼,风吹会痛,遇热会红?BeautyWonders唯芙诗,皮肤本身使一层层细胞并列组成的,并非毫无间隙,当肌肤缺水。推荐BEAUTYWONDERS玻尿酸沁润系列。细胞缩小,细胞间脂质流失,会形成间隙变大,30最大的颜值杀手,发腮竟然比发福还可怕?你有没发现,比起中年发福,好像发腮,对一个人的美貌度影响更惨烈。年轻时候,很多人都是标准的窄下颌瓜子脸,侧面看尤其明显,下颌流畅且内收,而40岁往后的照片开始往方脸靠拢,秀气和精致拥有56厘米腰围的红杏征服各种风格,彻底炫耀自己的曲线美2022年越南和平小姐公开削脸重建的美女穿着性感的衣服,彻底炫耀自己的身体。黎氏红杏是2022年越南小姐大赛中备受关注的选手,尽管最终只止步于前15名。为了拥有现在这样出色的容貌,汤唯的松弛感,到底有多可怕?最近被汤唯一张最新的广告片秒到。简简单单的黑白抹胸裙红唇黑发,随意一坐就是一整个抢眼又自然。一直以来,汤唯身上最被津津乐道的,就是看似虚无缥缈的神秘美感。这一点,简直把隔壁韩国迷得行走的衣架子!37岁童瑶上班美图曝光,黑色交叉吊带大秀迷人后背点击上方关注小丸子,获取更多有趣有料的时尚资讯和明星穿搭。在辣妹风越来越盛行的时尚圈,美丽又性感的小吊带自然有了一席之地,单单品一品slipdress这个词,想象一下两根细细的带子裤子靴子,今年秋冬最飒又时髦的穿搭嗨,各位小仙女们,大家好呀!今年秋冬,靴子可是最不可缺少的单品之一,不管是短靴,亦或者是长靴都是一样的。在选择靴子这种鞋款来穿时,该配裙子还是裤子呢?当然,无论是选择什么下装来配,姚明翻脸不认人,广东宏远未赛三连败,你们支持姚明的做法吗姚明翻脸不认人,广东宏远未赛三连败,你们支持姚明的做法吗?北京时间10月11日,CBA常规赛已经正式打响,但是对于球迷们非常关注的CBA11冠王广东宏远来说,目前正处于水深火热之中勤快妈妈做早餐,一周七天不重样,做法简单花样多,全家都爱吃由于经常晒早餐,也总能听到小伙伴称我是一个勤快妈妈,一周七天早餐不重样,看着花样多,其实做法都挺简单的。接地气的家常美食,做起来轻松,全家人也都很爱吃,每天早点起床,也不会觉得枯燥补肾,清代名方充髓汤!组方简单,一定用得到男人的大好时光是哪几年呢?这两天,粉丝问得最多的是有没有40岁左右的补方,俗话说40岁男人一枝花,更有味道,不想拖后腿,还是需要相应的补一补。那应该怎么补呢?方便理解,还是从案例说酒桌喝酒注意三个小妙招,不仅不会丢面,还会让别人印象深刻社交可以说是现在人们生活中必不可少的了,不管是工作中还是日常生活,这都是自己需要具备的能力,毕竟这个世界到处充满都是人情世故,要做好了,还会事半功倍,说实话这样的场合还是很多的。好