现功能时的选择很重要,如果你的系统所处理的数据量不是很大,我觉得队列和缓存很适合你,这样你可以对消息的传递更加了解,但你使用MQ,kafka的中间件时,你会发现使用起来更加轻松,但对于数据量大的系统来说,中间件是最好的选择,在这个大数据的时代,高并发,多线程,分布式会越来越重要
数据量小推荐使用:DelayQueue+redis
数据量大推荐使用:RabbitMQ
以下介绍常见的几种
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<String> list = new ArrayList<String>();
list.add("00000001");
list.add("00000002");
list.add("00000003");
list.add("00000004");
list.add("00000005");
DelayQueue<OrderDelay> queue = new DelayQueue<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) {
// 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加上下面的依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.24.Final</version>
</dependency>
测试代码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文件加入
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.8.0</version>
</dependency>
队列中要存放的元素实体
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<CallCdr> blockingFairQueue = redissonClient.getBlockingQueue("delay_queue");
RDelayedQueue<CallCdr> 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<CallCdr> blockingFairQueue = redissonClient.getBlockingQueue("delay_queue");
RDelayedQueue<CallCdr> 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());
}
}
}
打印结果如下
订单取消时间: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