对上述的任务,我们给一个专业的名字来形容,那就是延时任务。那么这里就会产生一个问题,这个延时任务和定时任务的区别究竟在哪里呢?一共有如下几点区别
下面,我们以判断订单是否超时为例,进行方案分析
1、JDK的延迟队列
思路
该方案是利用JDK自带的DelayQueue来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入DelayQueue中的对象,是必须实现Delayed接口的。
其中
poll():获取并移除队列的超时元素,没有则返回空
take():获取并移除队列的超时元素,如果没有则wait当前线程,直到有元素满足超时条件,返回结果。
实现
定义一个类OrderDelay实现Delayed,代码如下
package com.rjzheng.delay2;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
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+"编号的订单要删除啦。。。。");
}
}
运行的测试Demo为,我们设定延迟时间为3秒
package com.rjzheng.delay2;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;
public class DelayQueueDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
List list = new ArrayList();
list.add("00000001");
list.add("00000002");
list.add("00000003");
list.add("00000004");
list.add("00000005");
DelayQueue queue = new DelayQueue();
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) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
输出如下
00000001编号的订单要删除啦。。。。
After 3003 MilliSeconds
00000002编号的订单要删除啦。。。。
After 6006 MilliSeconds
00000003编号的订单要删除啦。。。。
After 9006 MilliSeconds
00000004编号的订单要删除啦。。。。
After 12008 MilliSeconds
00000005编号的订单要删除啦。。。。
After 15009 MilliSeconds
可以看到都是延迟3秒,订单被删除
优缺点
优点:效率高,任务触发时间延迟低。
缺点:
(1)服务器重启后,数据全部消失,怕宕机
(2)集群扩展相当麻烦
(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常
(4)代码复杂度较高
2、时间轮算法
思路
先上一张时间轮的图(这图到处都是啦)
时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)以及 timeUnit(时间单位),例如当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。
如果当前指针指在1上面,我有一个任务需要4秒以后执行,那么这个执行的线程回调或者消息将会被放在5上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到8,如果要20秒,指针需要多转2圈。位置是在2圈之后的5上面(20 % 8 + 1)
实现
我们用Netty的HashedWheelTimer来实现 给Pom加上下面的依赖
io.netty
netty-all
4.1.24.Final
测试代码HashedWheelTimerTest如下所示
package com.rjzheng.delay3;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.Timer;
import io.netty.util.TimerTask;
import java.util.concurrent.TimeUnit;
public class HashedWheelTimerTest {
static class MyTimerTask implements TimerTask{
boolean flag;
public MyTimerTask(boolean flag){
this.flag = flag;
}
public void run(Timeout timeout) throws Exception {
// TODO Auto-generated method stub
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) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(i+"秒过去了");
i++;
}
}
}
输出如下
1秒过去了
2秒过去了
3秒过去了
4秒过去了
5秒过去了
要去数据库删除订单了。。。。
6秒过去了
优缺点
优点:效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低。
缺点:
(1)服务器重启后,数据全部消失,怕宕机
(2)集群扩展相当麻烦
(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常
3、Redisson延迟队列RDelayedQueue
Redisson的集合相关文档
https://github.com/redisson/redisson/wiki/7.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%9B%86%E5%90%88
pom文件加入
org.redisson
redisson
3.8.0
package com.jeiao.redis;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RQueue;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Author:ZhuShangJin
* Date:2018/9/6
*/
public class CallCdr {
private String name;
private int age;
private String wife;
private Double salary;
private String putTime;
public CallCdr() {
}
public CallCdr(Double salary) {
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getWife() {
return wife;
}
public void setWife(String wife) {
this.wife = wife;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
public String getPutTime() {
return putTime;
}
public void setPutTime() {
this.putTime = new SimpleDateFormat("hh:mm:ss").format(new Date());
}
}
生成订单并放进延时队列的类
package com.jeiao.redis;
import org.redisson.Redisson;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RQueue;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
/**
* Author:ZhuShangJin
* Date:2018/8/31
*/
public class RedisPutInQueue {
public static void main(String args[]) {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.19.173:6379").setPassword("ps666").setDatabase(2);
RedissonClient redissonClient = Redisson.create(config);
RBlockingQueue blockingFairQueue = redissonClient.getBlockingQueue("delay_queue");
RDelayedQueue delayedQueue = redissonClient.getDelayedQueue(blockingFairQueue);
for (int i = 0; i <10 ; i++) {
try {
Thread.sleep(1*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 一分钟以后将消息发送到指定队列
//相当于1分钟后取消订单
// 延迟队列包含callCdr 1分钟,然后将其传输到blockingFairQueue中
//在1分钟后就可以在blockingFairQueue 中获取callCdr了
CallCdr callCdr = new CallCdr(30000.00);
callCdr.setPutTime();
delayedQueue.offer(callCdr, 1, TimeUnit.MINUTES);
}
// 在该对象不再需要的情况下,应该主动销毁。
// 仅在相关的Redisson对象也需要关闭的时候可以不用主动销毁。
delayedQueue.destroy();
//redissonClient.shutdown();
}
}
取消订单的操作类
package com.jeiao.redis;
import org.redisson.Redisson;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RQueue;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.logging.SimpleFormatter;
/**
* Author:ZhuShangJin
* Date:2018/8/31
*/
public class RedisOutFromQueue {
public static void main(String args[]) {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.19.173:6379").setPassword("ps666").setDatabase(2);
RedissonClient redissonClient = Redisson.create(config);
RBlockingQueue blockingFairQueue = redissonClient.getBlockingQueue("delay_queue");
RDelayedQueue delayedQueue = redissonClient.getDelayedQueue(blockingFairQueue);
while (true){
CallCdr callCdr = null;
try {
callCdr = blockingFairQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单取消时间:"+new SimpleDateFormat("hh:mm:ss").format(new Date())+"==订单生成时间"+callCdr.getPutTime());
}
}
}
打印结果如下
C:\workSoft\java\jdk8\bin\java -Didea.launcher.port=7543 "-Didea.launcher.bin.path=D:\JetBrains\IntelliJ IDEA 2016.3.4\bin" -Dfile.encoding=UTF-8 -classpath "C:\workSoft\java\jdk8\jre\lib\charsets.jar;C:\workSoft\java\jdk8\jre\lib\deploy.jar;C:\workSoft\java\jdk8\jre\lib\ext\access-bridge-64.jar;C:\workSoft\java\jdk8\jre\lib\ext\cldrdata.jar;C:\workSoft\java\jdk8\jre\lib\ext\dnsns.jar;C:\workSoft\java\jdk8\jre\lib\ext\jaccess.jar;C:\workSoft\java\jdk8\jre\lib\ext\jfxrt.jar;C:\workSoft\java\jdk8\jre\lib\ext\localedata.jar;C:\workSoft\java\jdk8\jre\lib\ext\nashorn.jar;C:\workSoft\java\jdk8\jre\lib\ext\sunec.jar;C:\workSoft\java\jdk8\jre\lib\ext\sunjce_provider.jar;C:\workSoft\java\jdk8\jre\lib\ext\sunmscapi.jar;C:\workSoft\java\jdk8\jre\lib\ext\sunpkcs11.jar;C:\workSoft\java\jdk8\jre\lib\ext\zipfs.jar;C:\workSoft\java\jdk8\jre\lib\javaws.jar;C:\workSoft\java\jdk8\jre\lib\jce.jar;C:\workSoft\java\jdk8\jre\lib\jfr.jar;C:\workSoft\java\jdk8\jre\lib\jfxswt.jar;C:\workSoft\java\jdk8\jre\lib\jsse.jar;C:\workSoft\java\jdk8\jre\lib\management-agent.jar;C:\workSoft\java\jdk8\jre\lib\plugin.jar;C:\workSoft\java\jdk8\jre\lib\resources.jar;C:\workSoft\java\jdk8\jre\lib\rt.jar;D:\ThreadUtils\target\classes;D:\.m2\repository\org\apache\commons\commons-lang3\3.4\commons-lang3-3.4.jar;D:\.m2\repository\org\redisson\redisson\3.7.5\redisson-3.7.5.jar;D:\.m2\repository\io\netty\netty-common\4.1.27.Final\netty-common-4.1.27.Final.jar;D:\.m2\repository\io\netty\netty-codec\4.1.27.Final\netty-codec-4.1.27.Final.jar;D:\.m2\repository\io\netty\netty-buffer\4.1.27.Final\netty-buffer-4.1.27.Final.jar;D:\.m2\repository\io\netty\netty-transport\4.1.27.Final\netty-transport-4.1.27.Final.jar;D:\.m2\repository\io\netty\netty-resolver\4.1.27.Final\netty-resolver-4.1.27.Final.jar;D:\.m2\repository\io\netty\netty-resolver-dns\4.1.27.Final\netty-resolver-dns-4.1.27.Final.jar;D:\.m2\repository\io\netty\netty-codec-dns\4.1.27.Final\netty-codec-dns-4.1.27.Final.jar;D:\.m2\repository\io\netty\netty-handler\4.1.27.Final\netty-handler-4.1.27.Final.jar;D:\.m2\repository\javax\cache\cache-api\1.0.0\cache-api-1.0.0.jar;D:\.m2\repository\io\projectreactor\reactor-core\3.1.7.RELEASE\reactor-core-3.1.7.RELEASE.jar;D:\.m2\repository\org\reactivestreams\reactive-streams\1.0.2\reactive-streams-1.0.2.jar;D:\.m2\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;D:\.m2\repository\com\fasterxml\jackson\dataformat\jackson-dataformat-yaml\2.7.9\jackson-dataformat-yaml-2.7.9.jar;D:\.m2\repository\org\yaml\snakeyaml\1.15\snakeyaml-1.15.jar;D:\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.7.9\jackson-core-2.7.9.jar;D:\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.7.9.2\jackson-databind-2.7.9.2.jar;D:\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.7.0\jackson-annotations-2.7.0.jar;D:\.m2\repository\net\bytebuddy\byte-buddy\1.8.11\byte-buddy-1.8.11.jar;D:\.m2\repository\org\jodd\jodd-bean\3.7.1\jodd-bean-3.7.1.jar;D:\.m2\repository\org\jodd\jodd-core\3.7.1\jodd-core-3.7.1.jar;D:\JetBrains\IntelliJ IDEA 2016.3.4\lib\idea_rt.jar" com.intellij.rt.execution.application.AppMain com.jeiao.redis.RedisOutFromQueue
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
订单取消时间:09:30:30==订单生成时间09:29:30
订单取消时间:09:30:31==订单生成时间09:29:31
订单取消时间:09:30:32==订单生成时间09:29:32
订单取消时间:09:30:33==订单生成时间09:29:33
订单取消时间:09:30:34==订单生成时间09:29:34
订单取消时间:09:30:35==订单生成时间09:29:35
订单取消时间:09:30:36==订单生成时间09:29:36
订单取消时间:09:30:37==订单生成时间09:29:37
订单取消时间:09:30:38==订单生成时间09:29:38
订单取消时间:09:30:39==订单生成时间09:29:39