看源码学编程系列之Deque在Kafka的应用(二)
通过看kafka 源码发现,在生产段发送消息的时候,使用了Deque 双队列这种数据结构作为消息队列。双队列(Deque)与我们平时所理解的队列(Queue)有什么区别呢?
我们都知道队列(Queue)是一种新进先出的数据结构,我们就假设是从尾部进去,然后头部出去。双队列(Deque)就比较灵活了,头部和尾部都可以进去或者出去。额,这种
双队列(Deque)有什么好处呢?拿我们公司来说吧,我们公司是一家大数据公司,每天都会产生很多数据进入到Kafka 来排队等待处理,有些数据甚至要排队好几天才轮到。好了
等了好几天消息终于可以处理了,这个时候万一客户端那边消费不成功,那这条数据怎么办?重新排队么,又要等好几天才处理?不现实的!所以只能重新插入到头部来重新消费。
这个时候有人也许会有疑问了,这个我用普通的队列(Queue)也一样处理,每次我都从队列的头部 get 一条数据,进行不断的处理等最终成功后再remove 这条数据,这种方案
好像也行但是不够给力,假如这条消息要处理得比较久,会不会影响接下来的第二、第三条数据处理效率呢,如果我们一开始就用poll 的方式,取出第一条进行处理,另外的线程接着
poll 第二、第三条数据进行处理,如果第一条数据处理不成功,就重新放回队列的头部进行重新处理,这种的效率会不会更高呢?
队列的get 与 poll 有什么区别呢?最大的区别就是poll 的时候,该数据是真的从队列移除了,这样别的线程才能取接下来的数据,get 则不同,get只是单纯取出数据,不改变
原有的数据集合,也就是别的线程无法获取接下来的任务。关于Deque 双队列请看如下代码:
1 public static void showDequeData() { 2 Deque<String> deque =new ArrayDeque<>(); 3 4 // 以下从头部添加 5 deque.offerFirst("111");//此时队列[111],如果头部数据插入成功则返回true,否则抛出异常 6 deque.addFirst("222");//此时队列[222,111],,如果头部数据插入失败则抛出异常 7 8 // 以下从尾部插入 9 deque.offerLast("999");//此时队列[222,111,999] 10 deque.addLast("888");//此时队列[222,111,999,888] 11 deque.add("333");//此时队列[222,111,999,888,333],实际实现还是调用 addLast() 12 deque.offer("777");//此时队列[222,111,999,888,333,777],实际实现还是调用 addLast() 13 14 // 头部取出 15 deque.pollFirst();////从头部移除第一个元素222,此时队列为[111, 999, 888, 333, 777] 16 deque.removeFirst();//从头部移除第一个元素111,此时队列为[ 999, 888, 333, 777],该函数实际调用了pollFirst() 17 deque.peekFirst();// 从头部取出第一个元素 999,此时队列为[999, 888, 333, 777] 18 deque.getFirst();// 从头部取出第一个元素 999,此时队列为[999, 888, 333, 777] 19 20 // 尾部取出 21 deque.pollLast();// 从尾部移除第一个元素777,此时队列为[999, 888, 333] 22 deque.removeLast();// 从尾部移除第一个元素333,此时队列为[999, 888],实际调用了pollLast 23 deque.peekLast();// 从尾部取出第一个元素888,此时队列为[999, 888] 24 deque.getLast();// 从尾部取出第一个元素888,此时队列为[999, 888] 25 System.out.println(deque.toString()); 26 }
其实Deque 这个类,只是定义一个接口规范,真正实现比较典型的有ArrayDeque,ConcurrentLinkedDeque,其中ArrayDeque 是非线程安全, ConcurrentLinkedDeque 则是线程安全。
ConcurrentLinkedDeque 线程安全实现方式,其实是一种叫做CAS 的实现方式,以下就是具体实现的一个片段。从compareAndSwapObject 方法名称中就可以窥探出,其实所谓的CAS就是
一种乐观锁的实现哲学,也就是在修改之前先看一下原值是否有改动过,如果没有改动过就更新成功,否则就不成功。
private boolean casHead(Node<E> paramNode1, Node<E> paramNode2) { return UNSAFE.compareAndSwapObject(this, headOffset, paramNode1, paramNode2);
以下代码就是参照Kafka 使用Deque 来实现消息发送,逻辑步骤如下:
第一、从尾部生成需要发送的消息数据
第二、从开头开始发送数据,如果失败则重新插回队列头部。
以下是模拟的代码程序:
1 public static void retrySendMessage() { 2 Deque<String> dequeMsg =new ArrayDeque<>(); 3 4 // 从尾部开始插入10条消息,进行后续的发送 5 for(int i=0;i<10;i++) { 6 dequeMsg.addLast("Msg="+i); 7 } 8 9 while(true) { 10 if(dequeMsg.size()>0) { 11 String msg = dequeMsg.pollFirst();// 从消息队列中取出第一条进行发送操作 12 System.out.println("开始发送消息:"+msg); 13 14 if(Utils.getRandomNum() >6) {// 使用随机数判断消息是否发送成功,如果大于6 则代表发送成功,否则发送失败 15 System.out.println(msg+" 已经发送成功!"); 16 }else { 17 System.out.println(msg+" 发送失败!,需要从头部重新进入队列"); 18 dequeMsg.addFirst(msg);//重新把数据返回队列头部 19 } 20 }else { 21 System.out.println("消息发送完成,退出程序"); 22 break; 23 } 24 } 25 26 }
以上实现可能也许比较简单,实际Kafka实现的时候,是使用多个线程来实现的,最起码一个线程用来生成数据,另一个线程用来发送数据,但中心思想是一样的。
浙公网安备 33010602011771号