Redisson延迟队列RDelayedQueue的使用

在开发中,往往会遇到一些关于延时任务的需求。例如

  • 生成订单30分钟未支付,则自动取消
  • 生成订单60秒后,给用户发短信

对上述的任务,我们给一个专业的名字来形容,那就是延时任务。那么这里就会产生一个问题,这个延时任务定时任务的区别究竟在哪里呢?一共有如下几点区别

  1. 定时任务有明确的触发时间,延时任务没有
  2. 定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期
  3. 定时任务一般执行的是批处理操作是多个任务,而延时任务一般是单个任务

下面,我们以判断订单是否超时为例,进行方案分析

 

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

 

你可能感兴趣的:(Java和Jvm)