延时/定时消息是指生产者(producer)发送消息到server后,server并不将消息立即发送给消费者(consumer),而是在producer指定的时间之后送达。
一般常用的有一下几种方式
比较简单常用的方式,把相对应的消息体存放在数据库中。然后启一个线程定时去扫数据库。找到超时的数据,做相对应的业务处理。
优点
缺点
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() > 0){
// 没有满足延时的元素 用poll返回 null
// 没有满足延时的元素 用take会阻塞
MyDelayed take = delayQueue.take();
//todo 业务处理
}
}
**需要注意: ** 如果业务处理太慢,会影响定时任务执行,所以任务要尽量快
JDK自带的一种线程池,它能调度一些命令在一段时间之后执行,或者周期性的执行。ScheduledExecutorService的实现类ScheduledThreadPoolExecutor提供了一种并行处理的模型,简化了线程的调度。DelayedWorkQueue是类似DelayQueue的实现,也是基于最小堆的、线程安全的数据结构,所以会有上例排序后输出的结果。
实现伪代码如下:
//创建任务线程池
ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(10,Executors.defaultThreadFactory());
executor.schedule(() -> System.out.println(Thread.currentThread().getName()+"延迟8s执行"),8,TimeUnit.SECONDS);
executor.schedule(() -> System.out.println(Thread.currentThread().getName()+"延迟1s执行"),1,TimeUnit.SECONDS);
**需要注意: ** 因为使用了线程池,当在同一个线程中,当上任务的执行时间过长会影响下一个任务的定时任务调用。
Redis中的ZSet是一个有序的Set,内部使用HashMap和跳表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
在用作延迟任务的时候,可以在添加数据的时候,使用zadd把score写成未来某个时刻的unix时间戳。消费者使用zrangeWithScores获取优先级最高的(最早开始的的)任务。注意,zrangeWithScores并不是取出来,只是看一下并不删除,类似于Queue的peek方法。程序对最早的这个消息进行验证,是否到达要运行的时间,如果是则执行,然后删除zset中的数据
如果在分布式环境下,可能会有并发的问题。即两个线程或者进程拿到了同一样的数据,然后都重复执行了任务并删除,这里可以使用分布式事务或者分布式锁来完成。
jedisCluster.zadd("key_1",System.currentTimeMillis()+10000,"uuid10");
jedisCluster.zadd("key_1",System.currentTimeMillis()+1000,"uuid1");
jedisCluster.zadd("key_1",System.currentTimeMillis()+2000,"uuid2");
while (true){
//获取已经过失的操作
Set<String> strings = jedisCluster.zrangeByScore("key_1", 0, System.currentTimeMillis());
//对每一个members 进行分布式锁
//进行业务的处理
}
优点
参考相关资料
RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间。如果消息超时未读,那么消息会进入到死信队列。那么我们只需要固定消费死信队列就可以实现消息的延时消费了。
相关配置代码如下:
/**
* 延时交换机
*/
public static final String DELAY_EXCHANGE = "delay.exchange";
/**
* 延时队列
*/
public static final String DELAY_QUEUE = "delay.queue";
/**
* 延时路由
*/
public static final String DELAY_QUEUEA_ROUTING_KEY = "delay.queue.routingkey";
/**
* 死信交换机
*/
public static final String DEAD_LETTER_EXCHANGE = "dead.letter.exchange";
/**
* 死信队列
*/
public static final String DEAD_LETTER_QUEUE = "dead.letter.queue";
/**
* 死信路由
*/
public static final String DEAD_LETTER_QUEUEA_ROUTING_KEY = "dead.letter.queue.routingkey";
/**
* @return 声明延时交互机
*/
@Bean(DELAY_EXCHANGE)
public DirectExchange delayExchange() {
return new DirectExchange(DELAY_EXCHANGE);
}
/**
* @return 声明延时队列
*/
@Bean(DELAY_QUEUE)
public Queue delayQueue() {
Map<String, Object> args = new HashMap<>(2);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 这里声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEA_ROUTING_KEY);
// x-message-ttl 声明队列的TTL
args.put("x-message-ttl", 9000);
return QueueBuilder.durable(DELAY_QUEUE).withArguments(args).build();
}
/**
* 绑定 延时交互机 和 延时队列
*/
@Bean
public Binding delayBinding(@Qualifier(DELAY_QUEUE) Queue queue,
@Qualifier(DELAY_EXCHANGE) DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DELAY_QUEUEA_ROUTING_KEY);
}
/**
* @return 声明死信交互机
*/
@Bean(DEAD_LETTER_EXCHANGE)
public DirectExchange deadLetterExchange() {
return new DirectExchange(DEAD_LETTER_EXCHANGE);
}
/**
* @return 声明死信队列
*/
@Bean(DEAD_LETTER_QUEUE)
public Queue deadLetterQueue() {
return new Queue(DEAD_LETTER_QUEUE);
}
/**
* 绑定 死信交互机 和 死信队列
*/
@Bean
public Binding deadLetterBindingA(@Qualifier(DEAD_LETTER_QUEUE) Queue queue,
@Qualifier(DEAD_LETTER_EXCHANGE) DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEA_ROUTING_KEY);
}
关于什么是时间轮,可以参考
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
HashedWheelTimer hashedWheelTimer = new HashedWheelTimer(100, TimeUnit.MILLISECONDS);
hashedWheelTimer.newTimeout(timeout -> System.out.println("task1:" + LocalDateTime.now().format(
formatter)), 1, TimeUnit.SECONDS);
hashedWheelTimer.newTimeout(timeout -> System.out.println("task2:" + LocalDateTime.now().format(
formatter)), 4, TimeUnit.SECONDS);
参考