Disruptor替代queue并解决queue的一些问题

一,遇到的问题。

在工作项目中,数据处理完成后合并数据放入一个LinkedTransferQueue中,会有一些长线程来不断地从队列中拿写到数据库。

由此产生以下问题:

1)因为LinkedTransferQueue是无界的,同时使用了transfer + take解决了一部分问题,但是程内部运行的数据处理线程数量多的时候,只有没有限制,会产生Full GC。

2)LinkedTransferQueue是无锁的,但是内部数据结构使用LinkedList,不断添加,删除元素会导致重复gc

 

基于以上问题想到两个方式:

1)实现一个LinkedTransferQueue的子类,在外层协助控制。(想法来自Motan,Tomcat的线程池设计)

附一份Motan线程池中对LinkedTransferQueue的改造。

Motan线程池中queue参考折叠原码  

 

class ExecutorQueue extends LinkedTransferQueue<Runnable> {
   private static final long serialVersionUID = -265236426751004839L;
   StandardThreadExecutor threadPoolExecutor;
 
   public ExecutorQueue() {
      super();
   }
 
   public void setStandardThreadExecutor(StandardThreadExecutor threadPoolExecutor) {
      this.threadPoolExecutor = threadPoolExecutor;
   }
 
   // 注:代码来源于 tomcat
   public boolean force(Runnable o) {
      if (threadPoolExecutor.isShutdown()) {
         throw new RejectedExecutionException("Executor not running, can't force a command into the queue");
      }
      // forces the item onto the queue, to be used if the task is rejected
      return super.offer(o);
   }
 
   // 注:tomcat的代码进行一些小变更
   public boolean offer(Runnable o) {
      int poolSize = threadPoolExecutor.getPoolSize();
 
      // we are maxed out on threads, simply queue the object
      if (poolSize == threadPoolExecutor.getMaximumPoolSize()) {
         return super.offer(o);
      }
      // we have idle threads, just add it to the queue
      // note that we don't use getActiveCount(), see BZ 49730
      if (threadPoolExecutor.getSubmittedTasksCount() <= poolSize) {
         return super.offer(o);
      }
      // if we have less threads than maximum force creation of a new
      // thread
      if (poolSize < threadPoolExecutor.getMaximumPoolSize()) {
         return false;
      }
      // if we reached here, we need to add it to the queue
      return super.offer(o);
   }
}

 


2)干扰器。

      1,Disruptor采用无锁设计。速度加快

           每个生产者或消费者线程,会先申请可以操作的元素在分段中的位置,申请到之后,直接在该位置写入或读取数据。

      2,Disruptor采用环状结构。避免gc,缓存机制使速度改变。通过设置RingBuffer来控制缓存大小,避免了无界的一系列问题。

           避免,垃圾回收,采用双重而非链表。同时,对处理器的缓存机制更加友好。

      3,元素位置定位。

           索引长度为2,则长度为2 ^ n,通过位运算,加快定位的速度。下标采用增量的形式。不用担心索引重叠的问题。。

 

二,Disruptor使用。

1,示范

1)定义要传递的消息类。

消息类折叠原码  

 

public class QueueMessage implements Serializable {
 
    public QueueMessage(){}
 
    private List<String> messageList;
 
    public void setMessageList(List<String> messageList){
        this.messageList = messageList;
    }
 
    public List<String> getMessageList(){
        return this.messageList;
    }
}

 

2)生产类

生产类折叠原码  

 

public class DisrutorProducerDemo {
 
    private final RingBuffer<QueueMessage> ringBuffer;
 
    public DisrutorProducerDemo(RingBuffer<QueueMessage> ringBuffer) {
        this.ringBuffer = ringBuffer;
    }
 
    public void onData(String orderId) {
        long sequence = ringBuffer.next();
        try {
            QueueMessage message = ringBuffer.get(sequence);
            List<String> messageList = new ArrayList<String>();
            messageList.add(orderId);
            message.setMessageList(messageList);
        finally {
            ringBuffer.publish(sequence);
        }
    }
}

 


3)消费类

消费类折叠原码  

 

public class EventHandlerDemo implements EventHandler<QueueMessage>, WorkHandler<QueueMessage> {
 
 
    public void onEvent(QueueMessage event, long sequence, boolean endOfBatch) throws Exception {
        System.out.println("data:" + event.getMessageList().get(0) + ", sequence: " + sequence + ", endOfBatch:" + endOfBatch);
    }
 
    public void onEvent(QueueMessage event) throws Exception {
        System.out.println("event:"+  event.getMessageList().get(0));
    }
}

 


4)组合使用:

Disruptor组合使用折叠原码  

 

public class DisrutorMainDemo {
 
    public static void main(String[] args) throws InterruptedException {
 
        Disruptor<QueueMessage> disruptor = new Disruptor<QueueMessage>(
                QueueMessage::new,
                1024 1024,
                Executors.defaultThreadFactory(),
                ProducerType.SINGLE,
                new YieldingWaitStrategy()
        );
        //可以传多个handler,多个handler会重复消费每条数据
        disruptor.handleEventsWith(new EventHandlerDemo());
        //一条数据只能被一个handler消费
        //disruptor.handleEventsWithWorkerPool(new EventHandlerDemo());
        disruptor.start();
        RingBuffer<QueueMessage> ringBuffer = disruptor.getRingBuffer();
        DisrutorProducerDemo eventProducer = new DisrutorProducerDemo(ringBuffer);
        for (int i = 0; i < 100; i++) {
            eventProducer.onData(UUID.randomUUID().toString());
        }
    }
}

 

 

2,Disruptor等待策略

名称
措施
适用场景
阻塞等待策略 加锁 CPU资源紧缺,骨折和延迟并不重要的场景
BusySpinWait策略 自旋 通过不断重试,减少切换线程导致的系统调用,而降低延迟。推荐在线程绑定到固定的CPU的场景下使用
分阶段回退等待策略 自旋+ yield +自定义策略 CPU资源紧缺,骨折和延迟并不重要的场景
睡眠等待策略 自旋+屈服+睡眠 性能和CPU资源之间有很好的折中。连续不均匀
超时阻止等待策略 加锁,有超时限制 CPU资源紧缺,骨折和延迟并不重要的场景
屈服等待策略 自旋+产量+自旋 性能和CPU资源之间有很好的折中。连续比较均匀

 

三,性能对比测试:

1,使用Disruptor前后性能对比结果

 
数据量
处理起始时间
处理结束时间
耗时
优化前 1000W 2020-08-25 18:57:46 2020-08-25 19:02:52 5.1分钟
优化后 1000W 2020-08-26 11:54:10 2020-08-26 11:56:58 2.8115分钟

猜你喜欢

转载自blog.csdn.net/MrBack/article/details/108224729