一、应用场景在需求开发的过程中,我们经常会遇到类似如下的一些场景:外卖订单超过15分钟未支付,自动取消b.使用抢票软件订票后,1小时内未付款,将自动取消c.待审申请逾期1天,通知审核人经理,超时后2天通知审核人主管d.自动发布那么这类场景的需求应该如何实现呢?我们首先想到的一般是启动一个定时任务扫描数据库中符合条件的数据并进行更新。一般来说,spring-quartz和elasticjob都可以实现,甚至可以自己写一个Timer。但是这种方式有一个缺点,就是需要不断地扫描数据库。如果数据量比较大,任务执行的间隔比较短,会对数据库造成一定的压力。另外,定时任务执行间隔的粒度也不好设置。如果设置的很长,会影响时效性。如果设置太短,会增加服务压力。让我们看看是否有更好的方法来做到这一点。java全套视频学习资料:http://www.atguigu.com/download.shtml2.JDK延迟队列实现DelayQueue是JDK中java.util.concurrent包下的一个无界阻塞队列。底层是优先级队列。对于放入队列的任务,可以按照过期时间排序,只需要取出过期的元素进行处理即可。具体步骤是,要放入队列的元素需要实现Delayed接口,实现getDelay方法计算过期时间,compare方法比较过期时间进行排序。使用DelayQueue,只需要一个线程就可以不断的从队列中获取数据。它的优点是不需要引入第三方依赖,实现起来非常简单。缺点也很明显。是内存存储,对分布式支持不友好。如果单点故障可能会导致数据丢失,无界队列也有OOM的风险。3.时间轮算法的实现1996年,GeorgeVarghese和TonyLauck的论文《Hashed and Hierarchical Timing Wheels: Data Structures for the Efficient Implementation of a Timer Facility》提出了一种时间轮管理Timeout事件的方法。它的设计非常巧妙,类似于时钟的运行。下图中的原始时间轮有8个格子。假设指针经过每一个格子需要1个时间单位,当前指针指向0。17个时间单位后超时的任务需要运行2次再经过一个格子才能执行,而放置在同一个网格中的任务将形成一个链表。与DelayQueue的数据结构相比,时间轮在算法复杂度上有一定的优势,但是使用时间轮实现延时任务也无法避免单点故障。4、RedisZSet实现Redis中有5种数据结构,最常用的是String和Hash,而ZSet是一种支持按分数排序的数据结构。每个元素都将与双精度类型的分数相关联。Redis使用评分来对集合中的成员进行从小到大的排序。有了这个特性,我们就可以使用超时时间作为评分来对任务进行排序。使用zaddkeyscoremember命令将任务放入redis,使用超时时间作为score,任务ID作为member,使用zrangekeystartstopwithscores命令从redis中读取任务,使用zremkeymember命令删除来自redis的任务。与前面两种实现方式相比,使用Redis可以将数据持久化到磁盘,避免数据丢失的风险,并且支持分布式,避免单点故障。5、MQ延迟队列的实现以RabbitMQ为例,它并不直接支持延迟队列的功能,但是通过一些特性,我们可以达到实现延迟队列的效果。RabbitMQ可以为Queue设置TTL,到过期时间还没有被消费的消息会变成死信——DeadLetter。我们也可以为Queue设置死信转发x-dead-letter-exchange,过期的消息可以路由到另一个Exchange。下图说明了这个过程。生产者通过不同的RoutingKey发送不同过期时间的消息。多个队列分别消费,产生死信,再路由到exe-dead-exchange。有些队列绑定了这个exchange,以便消费不同的业务逻辑。使用MQ实现的方法支持分布式和持久化消息,在业界应用广泛。它的缺点是需要为每个间隔时间场景单独建立队列。关键词:java培训
