(转)分布式之延时任务方案解析

转自:https://www.cnblogs.com/rjzheng/p/8972725.html

 

延时任务(eg:订单超时未支付):延时任务在某事件触发后一段时间内执行,没有执行周期

1.时间论算法

时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 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来实现

 1 public class MyTimerTaskTest {
 2     static class MyTimerTask implements TimerTask{
 3         boolean flag;
 4 
 5         public MyTimerTask(boolean flag) {
 6             this.flag = flag;
 7         }
 8 
 9         @Override
10         public void run(Timeout timeout) throws Exception {
11             System.out.println("要去数据库删除订单了。。。。。。。。。。。。");
12         }
13     }
14 
15     public static void main(String[] args) {
16         MyTimerTask timerTask = new MyTimerTask(true);
17         Timer timer = new HashedWheelTimer();
18         timer.newTimeout(timerTask, 5, TimeUnit.SECONDS);
19         int i = 1;
20         while (timerTask.flag){
21             try {
22                 Thread.sleep(1000);
23             }catch (Exception e){
24                 e.printStackTrace();
25             }
26             System.out.println(i + "秒过去了。。。。。。。。。。。");
27             i++;
28         }
29     }
30 
31 }

优缺点

优点:效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低。

缺点:

(1)服务器重启后,数据全部消失,怕宕机

(2)集群扩展相当麻烦

(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常

2.redis缓存

利用redis的zset,zset是一个有序集合,每一个元素(member)都关联了一个score,通过score排序来取集合中的值 k(score):订单超时时间戳 v(member):订单号

 1 private static JedisSentinelPool jedisPool = new JedisSentinelPool("mymaster",getSentinalSet(), "123456");
 2 
 3     public static Jedis getJedis(){
 4         return jedisPool.getResource();
 5     }
 6 
 7     //生产者 生成5个订单
 8     public void productionDelayMessage(){
 9         for (int i = 0; i < 5; i++){
10             //延迟三秒
11             Calendar call = Calendar.getInstance();
12             call.add(Calendar.SECOND, 3);
13             int second3later = (int)(call.getTimeInMillis()/1000);
14             Jedis jedis = RedisTest.getJedis();
15             jedis.zadd("order", second3later, "OID1000" + i);
16             System.out.println(System.currentTimeMillis() + "redis 生成了一个订单任务" + "OID1000" + i);
17         }
18     }
19 
20     //消费者 取订单
21     public void consumerDelayMessage(){
22         Jedis jedis = RedisTest.getJedis();
23         while (true){
24             Set<Tuple> items = jedis.zrangeWithScores("order",0,1);
25             if (items == null || items.isEmpty()) {
26                 System.out.println("当前没有等待任务");
27                 try {
28                     Thread.sleep(500);
29                 } catch (Exception e) {
30                     e.printStackTrace();
31                 }
32                 continue;
33             }
34             int score = (int)((Tuple)items.toArray()[0]).getScore();
35             Calendar cal = Calendar.getInstance();
36             int nowSecond = (int)(cal.getTimeInMillis() / 1000);
37             if (nowSecond > score){
38                 String oid = ((Tuple)items.toArray()[0]).getElement();
39                 jedis.zrem("order",oid)41                 System.out.println("消费者消费了一个订单任务 " + oid);
42             }
43         }
44     }
45 
46     public static void main(String[] args) {
47         RedisTest redisTest = new RedisTest();
48         redisTest.productionDelayMessage();
49         redisTest.consumerDelayMessage();
50     }
51 
52     private static Set<String> getSentinalSet(){
53         Set<String> set = new HashSet<>();
54         set.add("192.168.10.251:26379");
55         set.add("192.168.10.253:26379");
56         set.add("192.168.10.254:26379");
57         return set;
58     }

在高并发条件下,多消费者会取到同一个订单号

private static final int threadNum = 10;

    private static CountDownLatch cdl = new CountDownLatch(threadNum);

    static class DelayMessage implements Runnable{

        @Override
        public void run() {
            try {
                cdl.await();//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            RedisTest test = new RedisTest();
            test.consumerDelayMessage();
        }
    }

    public static void main(String[] args) {
        RedisTest test = new RedisTest();
        test.productionDelayMessage();
        for (int i = 0; i < threadNum; i++){
            new Thread(new DelayMessage()).start();
            cdl.countDown();//将count值减一
        }
    }

解决方案

*(1)用分布式锁,但是用分布式锁,性能下降了,该方案不细说。

* (2)对ZREM的返回值进行判断,只有大于0的时候,才消费数据

 Long num = jedis.zrem("order",oid);
 if (num != null && num > 0)
        System.out.println("消费者消费了一个订单任务 " + oid);

 

3.使用消息队列 我们可以采用rabbitMQ的延时队列。

RabbitMQ具有以下两个特性,可以实现延迟队列 RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter lRabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可

选)两个参数,用来控制队列内出现了deadletter,则按照这两个参数重新路由。

结合以上两个特性,就可以模拟出延迟消息的功能,具体的,我改天再写一篇文章,这里再讲下去,篇幅太长。

优缺点

优点: 高效,可以利用rabbitmq的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。

缺点:本身的易用度要依赖于rabbitMq的运维.因为要引用rabbitMq,所以复杂度和成本变高 4.JDK的延迟队列 该方案是利用JDK自带的DelayQueue来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入DelayQueue中的对

象,是必须实现Delayed接口的。

 

其中Poll():获取并移除队列的超时元素,没有则返回空

take():获取并移除队列的超时元素,如果没有则wait当前线程,直到有元素满足超时条件,返回结果。

优缺点

优点:效率高,任务触发时间延迟低。

缺点:

(1)服务器重启后,数据全部消失,怕宕机

(2)集群扩展相当麻烦

(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常

(4)代码复杂度较高

 

5.数据库轮询 通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行update或delete等操作

优缺点

优点:简单易行,支持集群操作

缺点:

(1)对服务器内存消耗大

(2)存在延迟,比如你每隔3分钟扫描一次,那最坏的延迟时间就是3分钟

(3)假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大

posted on 2018-06-27 09:16  hangzhi  阅读(205)  评论(0编辑  收藏  举报

导航