1. 应用场景
1、订单成功后,在30分钟内没有支付,自动取消订单
2、外卖平台发送订餐通知,下单成功后60s给用户推送短信。
3、如果订单一直处于某一个未完结状态时,及时处理关单,并退还库存
2. 什么是延时队列
当接收到消息之后,我需要隔一段时间进行处理(相对于立马处理,它隔了一段时间,所以他叫延迟消息)。
2.1 原理
DelayQueue中的所有元素必须实现Delayed接口
/**
* 一种混合风格的接口,用来标记那些应该在给定延迟时间之后执行的对象。
* <p>
* 此接口的实现必须定义一个 compareTo 方法,该方法提供与此接口的 getDelay 方法一致的排序。
*/
public interface Delayed extends Comparable<Delayed> {
/**
* 返回与此对象相关的剩余有效时间,以给定的时间单位表示.
*/
long getDelay(TimeUnit unit);
}
可以看到,Delayed接口除了自身的getDelay方法外,还实现了Comparable接口。getDelay方法用于返回对象的剩余有效时间,实现Comparable接口则是为了能够比较两个对象,以便排序。
也就是说,如果一个类实现了Delayed接口,当创建该类的对象并添加到DelayQueue中后,只有当该对象的getDalay方法返回的剩余时间≤0时才会出队。
另外,由于DelayQueue内部委托了PriorityBlockingQueue对象来实现所有方法,所以能以堆的结构维护元素顺序,这样剩余时间最小的元素就在堆顶,每次出队其实就是删除剩余时间≤0的最小元素。
无法使用take或poll移除未到期的元素,也不会将这些元素作为正常元素对待。例如,size方法同时返回到期和未到期元素的计数。此队列不允许使null元素。
3. 延时队列的实现一 - DelayQueue 延时队列
DelayQueue 是一个 BlockingQueue(无界阻塞)队列,它本质就是封装了一个 PriorityQueue(优先队列),PriorityQueue 内部使用完全二叉堆来实现队列元素排序,当向 DelayQueue 队列中添加元素时,会给元素一个 Delay(延迟时间)作为排序条件,队列中最小的元素会优先放在队首。队列中的元素只有到了 Delay 时间才允许从队列中取出。队列中可以放基本数据类型或自定义实体类,在存放基本数据类型时,优先队列中元素默认升序排列,自定义实体类就需要根据类属性值比较计算了。
3.1 demo
先简单实现一下看看效果,添加三个 order 入队 DelayQueue,分别设置订单在当前时间的5秒、10秒、15秒后取消。
要实现 DelayQueue 延时队列,队中元素实现 Delayed 接口,接口里只有一个 getDelay 方法,用于设置延期时间。Order 类中 compareTo() 负责对队列中的元素进行排序。
public class Order implements Delayed {
/**
* 到期时间
*/
@JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private long time;
String name;
public Order(String name, long time, TimeUnit unit) {
this.name = name;
this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);
}
@Override
public long getDelay(TimeUnit unit) {
// 剩余时间= 到期时间-当前系统时间,系统一般是纳秒级的,所以这里做一次转换
long d = unit.convert(time-System.currentTimeMillis(), TimeUnit.NANOSECONDS);
return d;
}
@Override
public int compareTo(Delayed o) {
// 订单剩余时间-当前传入的时间= 实际剩余时间(单位纳秒)
long d = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
// 根据剩余时间判断等于0 返回1 不等于0
// 有可能大于0 有可能小于0 大于0返回1 小于返回-1
return (d == 0) ? 0 : ((d > 0) ? 1 : -1);
}
}
DelayQueue 的 put 方法是线程安全的,因为 put 方法内部使用了 ReentrantLock 进行线程同步。DelayQueue 还提供了两种出队的方法 poll() 和 take()。poll() 为非阻塞获取,没有到期的元素直接返回 null;take() 为阻塞方式获取,没有到期的元素线程将会等待。
public class DelayQueueDemo {
public static void main(String[] args) throws InterruptedException {
Order Order1 = new Order("Order1", 5, TimeUnit.SECONDS);
Order Order2 = new Order("Order2", 10, TimeUnit.SECONDS);
Order Order3 = new Order("Order3", 15, TimeUnit.SECONDS);
DelayQueue<Order> delayQueue = new DelayQueue<>();
delayQueue.put(Order1);
delayQueue.put(Order2);
delayQueue.put(Order3);
System.out.println("订单延迟队列开始时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
while (delayQueue.size() != 0) {
//取队列头部元素是否过期
Order task = delayQueue.poll();
if (task != null) {
System.out.format("订单:{%s}被取消, 取消时间:{%s}\n", task.name, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
Thread.sleep(1000);
}
}
}
上边只是简单的实现入队与出队的操作,实际开发中会有专门的线程,负责消息的入队与消费。执行后看到结果如下,Order1、Order2、Order3 分别在 5秒、10秒、15秒后被执行,至此就用 DelayQueue 实现了延时队列。
订单延迟队列开始时间:2020-05-06 14:59:09
订单:{Order1}被取消, 取消时间:{2020-05-06 14:59:14}
订单:{Order2}被取消, 取消时间:{2020-05-06 14:59:19}
订单:{Order3}被取消, 取消时间:{2020-05-06 14:59:24}
4. 延时队列的实现二 - Redis + 定时
Redis 有一个有序集合的数据结构 ZSet,ZSet 中每个元素都有一个对应 Score,ZSet 中所有元素是按照其 Score 进行排序的。Redis 的 ZSet 实现延迟队列逻辑如下:
- 入队操作:zadd key timestamp task将需要处理的任务,按其需要延迟处理时间作为 Score 加入到 ZSet 中。Redis 的 zadd 的时间复杂度是 O(logN),能相对比较高效的进行入队操作。
- 定时(比如每隔一秒)通过 zrangebyscore(返回指定分数范围的升序元素)方法查询 ZSet 中 Score 最小的元素,具体操作为:zrangebyscore KEY -inf +inf limit 0 1 WITHSCORES。查询结果有两种情况:
a.查询出的分数小于等于当前时间戳,说明到这个任务需要执行的时间了,则去异步处理该任务。
b.查询出的分数大于当前时间戳,由于刚刚的查询操作取出来的是分数最小的元素,所以说明 ZSet 中所有的任务都还没有到需要执行的时间,则休眠一秒后继续查询。
来源: https://www.jianshu.com/p/977466020144
原理:https://blog.csdn.net/qq_35029061/article/details/86741925