一、基本定义
延时/定时消息是指生产者(producer)发送消息到server后,server并不将消息立即发送给消费者(consumer),而是在producer指定的时间之后送达。
二、常用的使用场景
- 任务超时处理。例如 订单系统中,如果某个用户在 30分钟内没有支付,则订单自动进行过期处理
- 任务延时处理。例如订餐系统中,下单成功60s之后 给用户发短信通知
- 定时任务。例如现在通过手机发送指令,在30分钟之后开启空调之类的。
三、实现方式
一般常用的有一下几种方式
- 定时器轮询遍历数据库记录
- JDK的DelayQueue
- JDK的ScheduledExecutorService
- Redis的ZSet实现
- RabbitMQ的延时队列
- 时间轮(netty/kafka等)
3.1 数据库轮询
比较简单常用的方式,把相对应的消息体存放在数据库中。然后启一个线程定时去扫数据库。找到超时的数据,做相对应的业务处理。
优点
- 方式很简单,不会引入其他的技术,开发周期短。
缺点
- 数据量过大时会消耗太多的IO资源,效率太低
- 而且当数量过大的时候,定时误差也较大
3.2 基于JDK的DelayQueue
Java中的DelayQueue位于java.util.concurrent包下,作为单机实现,它很好的实现了延迟一段时间后触发事件的需求。并且是线程安全,它可以有多个消费者和多个生产者,从而在某些情况下可以提升性能。
DelayQueue本质是封装了一个PriorityQueue。用户排序。这样可以保证后来插入消息的但是延时时间更短的会在前面。内部使用最小堆来实现排序队列,队首的,最先被消费者拿到的就是最小的那个。使用最小堆让队列在数据量较大的时候比较有优势。时间复杂度相对都比较好,都是O(logN)。
实现伪代码如下:
public class MyDelayed implements Delayed{
// 延迟的时间
private long delayTime;
//进入队列的时间
private long enterTime;
@Override
public int compareTo(Delayed o) {
return (int) (this.getDelay(TimeUnit.MILLISECONDS) -o.getDelay(TimeUnit.MILLISECONDS));
}
/**
* 返回只小于等于0表示当前时间已经过去
*/
@Override
public long getDelay(TimeUnit unit) {
long expire = this.enterTime + delayTime;
return unit.convert(expire - System.currentTimeMillis(),TimeUnit.MILLISECONDS);
}
// ...中间省略
//实现方式
DelayQueue<MyDelayed> delayQueue = new DelayQueue<>();
delayQueue.add(new MyDelayed(5000));
while (delayQueue.size()