zoukankan      html  css  js  c++  java
  • (转)分布式之延时任务方案解析

    转自: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)假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大

  • 相关阅读:
    广度优先搜索(一)
    快速幂
    office 2013
    最著名的十大公式
    二分查找的上下界
    双关键字快速排序
    字符串操作
    分治算法练习(二)
    P3119 [USACO15JAN]草鉴定[SCC缩点+SPFA]
    P3225 [HNOI2012]矿场搭建[割点]
  • 原文地址:https://www.cnblogs.com/hangzhi/p/9232250.html
Copyright © 2011-2022 走看看