- 什么是延时任务,什么是定时任务
- 方案一:数据库轮询
- 方案二:JDK的延迟队列
- 方案三:时间轮算法
- 方案四:redis缓存
- 方案五:消息队列
什么是延时任务,什么是定时任务
定时任务有执行周期,延时任务没有执行周期,只有在某一事件触发后的一段时间内被执行。
定时任务有明确的触发时间,延时任务没有。
定时任务一般以批处理的形式,处理多个任务,延时任务一般只是单个任务。
延时任务的例子:用户下单后30分钟未付款,则订单自动取消。
本文主要讲延时任务的处理方法
方案 | 优势 | 劣势 |
数据库轮询 | 实现简单 | 存在明显的性能瓶颈 |
JDK的延迟队列 | 实现简单 | 数据内存态,不可靠 |
时间轮算法 | 实时性高 | 实现难度大,内存消耗大 |
消息队列 | 异步交互可削峰 | 延时的时间长度不可控,如果数据需要持久化则性能会降低 |
redis缓存 | 数据持久化,性能高 | 实现难度大 |
方案一:数据库轮询
即通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行 update 或 delete 等操作
可以用 quartz 来实现
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.2</version>
</dependency>
public class MyJob implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("开始扫描");
}
public static void main(String[] args) throws Exception {
// 创建任务
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("job", "group").build();
// 创建触发器 每3秒钟执行一次
Trigger trigger = TriggerBuilder
.newTrigger()
.withIdentity("trigger", "group1")
.withSchedule(
SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(3).
repeatForever())
.build();
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
// 将任务及其触发器放入调度器
scheduler.scheduleJob(jobDetail, trigger);
// 调度器开始调度任务
scheduler.start();
}
}
方案二:JDK的延迟队列
利用 JDK 自带的 DelayQueue 来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入 DelayQueue 中的对象,是必须实现 Delayed 接口的。
public class OrderDelay implements Delayed {
private String orderId;
private long timeout;
OrderDelay(String orderId, long timeout) {
this.orderId = orderId;
this.timeout = timeout + System.nanoTime();
}
public int compareTo(Delayed other) {
if (other == this) {
return 0;
}
OrderDelay t = (OrderDelay) other;
long d = (getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS));
return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
}
// 返回距离你自定义的超时时间还有多少
public long getDelay(TimeUnit unit) {
return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);
}
void print() {
System.out.println(orderId + "编号的订单要删除了。。。。");
}
}
public class DelayQueueDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("00000001");
list.add("00000002");
list.add("00000003");
list.add("00000004");
list.add("00000005");
DelayQueue<OrderDelay> queue = newDelayQueue < OrderDelay > ();
long start = System.currentTimeMillis();
for (int i = 0; i < 5; i++) {
//延迟三秒取出
queue.put(new OrderDelay(list.get(i), TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS)));
try {
queue.take().print();
System.out.println("After " + (System.currentTimeMillis() - start) + " MilliSeconds");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
方案三:时间轮算法
时间轮算法可以类比于时钟,箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。这样可以看出定时轮由个 3 个重要的属性参数,ticksPerWheel(一轮的 tick 数),tickDuration(一个 tick 的持续时间)以及 timeUnit(时间单位),例如当 ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。
如果当前指针指在 1 上面,我有一个任务需要 4 秒以后执行,那么这个执行的线程回调或者消息将会被放在 5 上。
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.24.Final</version>
</dependency>
public class HashedWheelTimerTest {
static class MyTimerTask implements TimerTask {
boolean flag;
public MyTimerTask(boolean flag) {
this.flag = flag;
}
public void run(Timeout timeout) throws Exception {
System.out.println("要去数据库删除订单了。。。。");
this.flag = false;
}
}
public static void main(String[] argv) {
MyTimerTask timerTask = new MyTimerTask(true);
Timer timer = new HashedWheelTimer();
timer.newTimeout(timerTask, 5, TimeUnit.SECONDS);
int i = 1;
while (timerTask.flag) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i + "秒过去了");
i++;
}
}
}
方案四:redis缓存
redis 的 zset,zset 是一个有序集合,每一个元素(member)都关联了一个 score,通过 score 排序来取集合中的值。
以超时订单为例,将订单超时时间戳与订单号分别设置为 score 和 member,系统扫描第一个元素判断是否超时。
public class AppTest {
private static final String ADDR = "127.0.0.1";
private static final int PORT = 6379;
private static JedisPool jedisPool = new JedisPool(ADDR, PORT);
public static Jedis getJedis() {
return jedisPool.getResource();
}
//生产者,生成5个订单放进去
public void productionDelayMessage() {
for (int i = 0; i < 5; i++) {
//延迟3秒
Calendar cal1 = Calendar.getInstance();
cal1.add(Calendar.SECOND, 3);
int second3later = (int) (cal1.getTimeInMillis() / 1000);
AppTest.getJedis().zadd("OrderId", second3later, "OID0000001" + i);
System.out.println(System.currentTimeMillis() + "ms:redis生成了一个订单任务:订单ID为" + "OID0000001" + i);
}
}
//消费者,取订单
public void consumerDelayMessage() {
Jedis jedis = AppTest.getJedis();
while (true) {
Set<Tuple> items = jedis.zrangeWithScores("OrderId", 0, 1);
if (items == null || items.isEmpty()) {
System.out.println("当前没有等待的任务");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
int score = (int) ((Tuple) items.toArray()[0]).getScore();
Calendar cal = Calendar.getInstance();
int nowSecond = (int) (cal.getTimeInMillis() / 1000);
if (nowSecond >= score) {
String orderId = ((Tuple) items.toArray()[0]).getElement();
jedis.zrem("OrderId", orderId);
System.out.println(System.currentTimeMillis() + "ms:redis消费了一个任务:消费的订单OrderId为" + orderId);
}
}
}
public static void main(String[] args) {
AppTest appTest = new AppTest();
appTest.productionDelayMessage();
appTest.consumerDelayMessage();
}
}
以上的解决方法,存在一个问题,在高并发的情况下,一个资源会被多次处理。
解决方法:可引入分布式锁或者对ZREM的返回值进行判断
方案五:消息队列
采用 rabbitMQ 的延时队列。RabbitMQ 具有以下两个特性,可以实现延迟队列
RabbitMQ 可以针对 Queue 和 Message 设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为 dead letter
lRabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了deadletter,则按照这两个参数重新路由。