数据库轮询
不是很推荐的一种方式,需要定时扫描数据库,借助定时任务工具,如果是多服务部署,那么还需要考虑分布式调度,推荐xxL-job。
缺点在于数据库压力大,而且会有延迟性。比如每隔3分钟进行扫描一次,最坏的情况就是那个取消支付要延迟3分钟行。
mysql定时器是系统给提供了event,下面创建表:
create table mytable (
id int auto_increment not null,
name varchar(100) not null default '',
introduce text not null,
createtime timestamp not null,
constraint pk_mytable primary key(id)
)
创建存储过程,这里的存储过程主要提供给mysql的定时器event来调用去执行:
create procedure mypro()
BEGIN
insert into mytable (name,introduce,createtime) values ('1111','inner mongolia',now());
end;
这里只是简单的写了一下,只是为了说明例子。
紧接着创建mysql的定时器event:
create event if not exists eventJob
on schedule every 1 second
on completion PRESERVE
do call mypro();
这里设置为每一秒执行一次
至此所有的准备工作已经写完了,做完这些,mysql要想利用定时器必须的做准备工作,就是把mysql的定时器给开启了:
SET GLOBAL event_scheduler = 1; -- 启动定时器
SET GLOBAL event_scheduler = 0; -- 停止定时器
紧接着还要开启事件:
ALTER EVENT eventJob ON COMPLETION PRESERVE ENABLE; -- 开启事件
ALTER EVENT eventJob ON COMPLETION PRESERVE DISABLE; -- 关闭事件
SHOW VARIABLES LIKE '%sche%'; -- 查看定时器状态
https://www.cnblogs.com/mr-wuxiansheng/p/5962454.html
xxl-job中心式的调度平台轻量级,开箱即用,操作简易,上手快,与SpringBoot有非常好的集成,而且监控界面就集成在调度中心,界面又简洁,对于企业维护起来成本不高,还有失败的邮件告警等等。
xxl-job是通过一个中心式的调度平台,调度多个执行器执行任务,调度中心通过DB锁保证集群分布式调度的一致性,这样扩展执行器会增大DB的压力,但是如果实际上这里数据库只是负责任务的调度执行。但是如果没有大量的执行器的话和任务的情况,是不会造成数据库压力的。实际上大部分公司任务数,执行器并不多(虽然面试经常会问一些高并发的问题)。
JDK的延迟队列
利用JDK自带的无界阻塞队列DelayQueue来实现,该队列只有在延迟期满的时候才能从中获取元素,放入DelayQueue中的对象,是必须实现Delayed接口的。这种方式优点在于延时低,缺点就是一旦服务重启,之前放入队列的任务全部丢失。如果有大量的任务,可能会造成oom。代码复杂度也比较高。
package com.fchan.mq.jdkDelay;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class MyDelay implements Delayed {
/**
* 延迟时间
*/
@JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Long time;
String name;
public MyDelay(String name, Long time, TimeUnit unit) {
this.name = name;
this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);
}
public Long getTime() {
return time;
}
public String getName() {
return name;
}
//获取延时时间
@Override
public long getDelay(TimeUnit unit) {
return time - System.currentTimeMillis();
}
//对延时队列中的元素进行排序
@Override
public int compareTo(Delayed o) {
MyDelay myDelay = ((MyDelay) o);
return this.time.compareTo(myDelay.getTime());
}
}
package com.fchan.mq.jdkDelay;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;
public class MyDelayDemo {
public static void main(String[] args) throws InterruptedException {
MyDelay myDelay1 = new MyDelay("MyDelay1", 5L, TimeUnit.SECONDS);
MyDelay myDelay2 = new MyDelay("MyDelay2", 10L, TimeUnit.SECONDS);
MyDelay myDelay3 = new MyDelay("MyDelay3", 15L, TimeUnit.SECONDS);
DelayQueue<MyDelay> delayQueue = new DelayQueue<MyDelay> ();
//add 和 put 后面都是调用的offer方法,内部使用了ReentrantLock
//delayQueue.put();
delayQueue.add(myDelay1);
delayQueue.add(myDelay2);
delayQueue.add(myDelay3);
System.out.println("订单延迟队列开始时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
while (delayQueue.size() != 0) {
/**
* 取队列头部元素是否过期
*/
//DelayQueue的put/add方法是线程安全的,因为put/add方法内部使用了ReentrantLock锁进行线程同步。
// DelayQueue还提供了两种出队的方法 poll() 和 take() ,
// poll() 为非阻塞获取,没有到期的元素直接返回null;
// take() 阻塞方式获取,没有到期的元素线程将会等待。
MyDelay 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);
}
}
}
Quartz
Quartz一款非常经典任务调度框架,在Redis、RabbitMQ还未广泛应用时,超时未支付取消订单功能都是由定时任务实现的。
Quartz缺点没有自带的管理界面;调度逻辑和执行任务耦合在一起;维护需要重启服务
时间轮算法
时间轮算法可以类比于时钟,如下图箭头(指针)按某一个方间按固定频率轮动,每一次跳动称为一个tick。这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(
轮的tick数),tickDuration(一个tick的持续时间)从及timeUnit(时间单位),例如当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的利针走动完全类似了。可从用Netty的HashedWheelTimer来进行使用,缺点和延识队列一样
使用消息队列
采用rabbitMQ的延时队列。RabbitMQ具有从下两个特性,RabbitMQ可以针对Queue和Message设置x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter。
RabbitMQ的Queue可从配置x-dead-letter-exchange和x-
dead-letter-routing-key(可选)两个参数,用来控制队列内出现了deadletter,则按照这两个参数重新路由。结合以上两个特性,就可以模拟出延迟消息的功能。
优点就是高效,可以利用rabbitmq的分布式特性轻易的进行横句扩展,消息支持持久化增加了可靠性
public void send(String delayTimes) {
amqpTemplate.convertAndSend("order.pay.exchange", "order.pay.queue","大家好我是延迟数据", message -> {
// 设置延迟毫秒值
message.getMessageProperties().setExpiration(String.valueOf(delayTimes));
return message;
});
}
}
设置转发规则
/**
* 延时队列
*/
@Bean(name = "order.delay.queue")
public Queue getMessageQueue() {
return QueueBuilder
.durable(RabbitConstant.DEAD_LETTER_QUEUE)
// 配置到期后转发的交换
.withArgument("x-dead-letter-exchange", "order.close.exchange")
// 配置到期后转发的路由键
.withArgument("x-dead-letter-routing-key", "order.close.queue")
.build();
}
除此之外 监听RedisKey过期时间进行定时任务并不可信
redis的过期监听并不可靠, 尤其是大量KEY的时候.很可能 会延迟推送.
redis官方链接
In order to obtain a correct behavior without sacrificing consistency, when a key expires, a DEL operation is synthesized in both the AOF file and gains all the attached replicas nodes. This way the expiration process is centralized in the master instance, and there is no chance of consistency errors.
However while the replicas connected to a master will not expire keys independently (but will wait for the DEL coming from the master), they’ll still take the full state of the expires existing in the dataset, so when a replica is elected to master it will be able to expire the keys independently, fully acting as a master.
大致意思是会生成del追加到aof中,副本没有自己的过期key,他会等待删除命令,
数据分析请勿过度依赖Redis的过期监听