Java Worker 设计模式

Worker模式

想解决的问题

异步执行一些任务,有返回或无返回结果

使用动机

有些时候想执行一些异步任务,如异步网络通信、daemon任务,但又不想去管理这任务的生命周。这个时候可以使用Worker模式,它会帮您管理与执行任务,并能非常方便地获取结果

结构

很多人可能为觉得这与executor很像,但executor是多线程的,它的作用更像是一个规划中心。而Worker则只是个搬运工,它自己本身只有一个线程的。每个worker有自己的任务处理逻辑,为了实现这个目的,有两种方式

1. 建立一个抽象的AbstractWorker,不同逻辑的worker对其进行不同的实现;

2. 对worker新增一个TaskProcessor不同的任务传入不同的processor即可。

第二种方式worker的角色可以很方便地改变,而且可以随时更换processor,可以理解成可”刷机”的worker
^ ^。这里我们使用第二种方式来介绍此模式的整体结构。

针对上图,详细介绍一下几个角色:

  • ConfigurableWorker:顾名思义这个就是真正干活的worker了。要实现自我生命周期管理,需要实现Runable,这样其才能以单独的线程运行,需要注意的是work最好以daemon线程的方式运行。worker里面还包括几个其它成员:taskQueue,一个阻塞性质的queue,一般BlockingArrayList就可以了,这样任务是FIFO(先进先出)的,如果要考虑任务的优先级,则可以考虑使用PriorityBlockingQueue;listeners,根据事件进行划分的事件监听者,以便于当一个任务完成的时候进行处理,需要注意的是,为了较高效地进行listener遍历,这里我推荐使用CopyOnWriteArrayList,免得每次都复制。其对应的方法有addlistener、addTask等配套方法,这个都不多说了,更详细的可以看后面的示例代码。
  • WorkerTask:实际上这是一个抽象的工内容,其包括基本的id与,task的ID是Worker生成的,相当于递wtte后的一个执回,当数据执行完了的时候需要使用这个id来取结果。而后面真正实现的实体task则包含任务处理时需要的数据。
  • Processor:为了实现可”刷机”的worker,我们将处理逻辑与worker分开来,processor的本职工作很简单,只需要加工传入的task数据即可,加工完成后触发fireEvent(WorkerEvent.TASK_COMPLETE)事件,之后通过Future的get即可得到最终的数据。

另外再说一点,对于addTask,可以有一个overload的方法,即在输入task的同时,传入一个RejectPolice,这样可以在size过大的时候做出拒绝操作,有效避免被撑死。

适用性/问题

这种设计能自动处理任务,并能根据任务的优先级自动调节任务的执行顺序,一个完全独立的thread,你完全可以将其理解成一专门负责干某种活的”机器人”。它可以用于处理一些定时、请求量固定均匀且对实时性要求不是太高的任务,如日志记录,数据分析等。当然,如果想提高任务处理的数据,可以生成多个worker,就相当于雇佣更多的人来为你干活,非常直观的。当然这样一来,谁来维护这worker便成了一个问题,另外就目前这种设计下worker之间是没有通信与协同的,这些都是改进点。

那么对于多个worker,有什么组织方式呢?这里我介绍三种,算是抛砖引玉:

流水线式worker(assembly-line worker)

就像生产车间上的流水线工人一样,将任务切分成几个小块,每个worker负责自己的一部分,以提高整体的生产、产出效率,如下图:

假设完成任务 t 需要的时间为:W(t)=n,那么将任务分解成m份,流水线式的执行,每小份需要的时间便为 W(t/m)=n/m,那么执行1000条任务的时间,单个为1000n,流水线长度为L,则用这种方式所用的时间为(1000-1)*(m-L+1)*n/m+n
其中L<m,由此可见,流水线的worker越多、任务越细分,工作的效率将越高。这种主方式的问题在于,如果一个worker出现问题,那么整个流水线就将停止工作。而且任务的优先级不能动态调用,必须事先告知。

多级反馈队列(Multilevel Feedback Queue)

这是一个有Q1、Q2…Qn个多重流水线方式,从高到低分别代码不同的优先级,高优先级的worker要多于低优先级的,一般是2的倍数,即Q4有16个worker、Q3有8个,后面类推。任务根据预先估计好的优先级进入,如果任务在某步的执行过长,直接踢到下一级,让出最快的资源。如下图所示:

显然这种方式的好处就在于可以动态地调整任务的优级,及时做出反应。当然,为了实现更好的高度,我们可以在低级里增加一个阀值,使得放偶然放入低级的task可以有复活的机会^
^。

MapReduce式

流水线虽然有一定的并行性,但总体来说仍然是串行的,因为只要有一个节点出了问题,那都是致命的错误。MapReduce是Google率先实现的一个分布式算法,有非常好的并行执行效率。

如上图所示,只要我们将Map与Reduce都改成Worker就行了,如MapWorker与ReduceWorker。这样,可以看见,Map的过程是完全并行的,当然这样就需要在Map与Reduce上的分配与数据组合上稍稍下一点功夫了。

样例实现

这里我们实现一个PageURLMiningWorker,对给定的URL,打开页面后,采取所有的URL,并反回结果进行汇总输出。由于时间有限,这里我只实现了单worker与MapReduce worker集两种方式,有兴趣的同学可以实现其它类型,如多级反馈队列。注意!我这里只是向大家展示这种设计模式,URL
抓取的效率不在本次考虑之列。

所有的代码可以在这里获取:https://github.com/sefler1987/javaworker

单Worker实现样例

 

  1. package com.alibaba.taobao.main;
  2.  
  3. import java.util.Arrays;
  4. import java.util.List;
  5. import java.util.concurrent.ConcurrentHashMap;
  6. import java.util.concurrent.ConcurrentSkipListSet;
  7. import java.util.concurrent.TimeUnit;
  8.  
  9. import com.alibaba.taobao.worker.ConfigurableWorker;
  10. import com.alibaba.taobao.worker.SimpleURLComparator;
  11. import com.alibaba.taobao.worker.WorkerEvent;
  12. import com.alibaba.taobao.worker.WorkerListener;
  13. import com.alibaba.taobao.worker.WorkerTask;
  14. import com.alibaba.taobao.worker.linear.PageURLMiningProcessor;
  15. import com.alibaba.taobao.worker.linear.PageURLMiningTask;
  16.  
  17. /**
  18. * Linear version of page URL mining. It’s slow but simple.
  19. * Average time cost for 1000 URLs is: 3800ms
  20. *
  21. * @author xuanyin.zy E-mail:xuanyin.zy@taobao.com
  22. * @since Sep 16, 2012 5:35:40 PM
  23. */
  24. public class LinearURLMiningMain implements WorkerListener {
  25. private static final String EMPTY_STRING = ”";
  26.  
  27. private static final int URL_SIZE_TO_MINE = 10000;
  28.  
  29. private static ConcurrentHashMap<String, WorkerTask<?>> taskID2TaskMap = new ConcurrentHashMap<String, WorkerTask<?>>();
  30.  
  31. private static ConcurrentSkipListSet<String> foundURLs = new ConcurrentSkipListSet<String>(new SimpleURLComparator());
  32.  
  33. public static void main(String[] args) throws InterruptedException {
  34. long startTime = System.currentTimeMillis();
  35.  
  36. ConfigurableWorker worker = new ConfigurableWorker(“W001″);
  37. worker.setTaskProcessor(new PageURLMiningProcessor());
  38.  
  39. addTask2Worker(worker, new PageURLMiningTask(“http://www.taobao.com”));
  40. addTask2Worker(worker, new PageURLMiningTask(“http://www.xinhuanet.com”));
  41. addTask2Worker(worker, new PageURLMiningTask(“http://www.zol.com.cn”));
  42. addTask2Worker(worker, new PageURLMiningTask(“http://www.163.com”));
  43.  
  44. LinearURLMiningMain mainListener = new LinearURLMiningMain();
  45. worker.addListener(mainListener);
  46.  
  47. worker.start();
  48.  
  49. String targetURL = EMPTY_STRING;
  50. while (foundURLs.size() < URL_SIZE_TO_MINE) {
  51. targetURL = foundURLs.pollFirst();
  52.  
  53. if (targetURL == null) {
  54. TimeUnit.MILLISECONDS.sleep(50);
  55. continue;
  56. }
  57.  
  58. PageURLMiningTask task = new PageURLMiningTask(targetURL);
  59. taskID2TaskMap.putIfAbsent(worker.addTask(task), task);
  60.  
  61. TimeUnit.MILLISECONDS.sleep(100);
  62. }
  63.  
  64. worker.stop();
  65.  
  66. for (String string : foundURLs) {
  67. System.out.println(string);
  68. }
  69.  
  70. System.out.println(“Time Cost: ” + (System.currentTimeMillis() - startTime) + ”ms”);
  71. }
  72.  
  73. private static void addTask2Worker(ConfigurableWorker mapWorker_1, PageURLMiningTask task) {
  74. String taskID = mapWorker_1.addTask(task);
  75. taskID2TaskMap.put(taskID, task);
  76. }
  77.  
  78. @Override
  79. public List<WorkerEvent> intrests() {
  80. return Arrays.asList(WorkerEvent.TASK_COMPLETE, WorkerEvent.TASK_FAILED);
  81. }
  82.  
  83. @Override
  84. public void onEvent(WorkerEvent event, Object… args) {
  85. if (WorkerEvent.TASK_FAILED == event) {
  86. System.err.println(“Error while extracting URLs”);
  87. return;
  88. }
  89.  
  90. if (WorkerEvent.TASK_COMPLETE != event)
  91. return;
  92.  
  93. PageURLMiningTask task = (PageURLMiningTask) args[0];
  94. if (!taskID2TaskMap.containsKey(task.getTaskID()))
  95. return;
  96.  
  97. foundURLs.addAll(task.getMinedURLs());
  98.  
  99. System.out.println(“Found URL size: ” + foundURLs.size());
  100.  
  101. taskID2TaskMap.remove(task.getTaskID());
  102. }
  103. }

 

MapReduce实现样例

 

  1. package com.alibaba.taobao.main;
  2.  
  3. import java.util.ArrayList;
  4. import java.util.Arrays;
  5. import java.util.List;
  6. import java.util.concurrent.ConcurrentHashMap;
  7. import java.util.concurrent.ConcurrentSkipListSet;
  8. import java.util.concurrent.TimeUnit;
  9.  
  10. import com.alibaba.taobao.worker.ConfigurableWorker;
  11. import com.alibaba.taobao.worker.SimpleURLComparator;
  12. import com.alibaba.taobao.worker.WorkerEvent;
  13. import com.alibaba.taobao.worker.WorkerListener;
  14. import com.alibaba.taobao.worker.WorkerTask;
  15. import com.alibaba.taobao.worker.mapreduce.Map2ReduceConnector;
  16. import com.alibaba.taobao.worker.mapreduce.MapReducePageURLMiningTask;
  17. import com.alibaba.taobao.worker.mapreduce.PageContentFetchProcessor;
  18. import com.alibaba.taobao.worker.mapreduce.URLMatchingProcessor;
  19.  
  20. /**
  21. * MapReduce version of page URL mining. It’s very powerful.
  22. *
  23. * @author xuanyin.zy E-mail:xuanyin.zy@taobao.com
  24. * @since Sep 16, 2012 5:35:40 PM
  25. */
  26. public class MapReduceURLMiningMain implements WorkerListener {
  27. private static final String EMPTY_STRING = ”";
  28.  
  29. private static final int URL_SIZE_TO_MINE = 10000;
  30.  
  31. private static ConcurrentHashMap<String, WorkerTask<?>> taskID2TaskMap = new ConcurrentHashMap<String, WorkerTask<?>>();
  32.  
  33. private static ConcurrentSkipListSet<String> foundURLs = new ConcurrentSkipListSet<String>(new SimpleURLComparator());
  34.  
  35. public static void main(String[] args) throws InterruptedException {
  36. long startTime = System.currentTimeMillis();
  37.  
  38. // four mapers
  39. List<ConfigurableWorker> mappers = new ArrayList<ConfigurableWorker>(4);
  40.  
  41. ConfigurableWorker mapWorker_1 = new ConfigurableWorker(“W_M1″);
  42. ConfigurableWorker mapWorker_2 = new ConfigurableWorker(“W_M2″);
  43. ConfigurableWorker mapWorker_3 = new ConfigurableWorker(“W_M3″);
  44. ConfigurableWorker mapWorker_4 = new ConfigurableWorker(“W_M4″);
  45. mapWorker_1.setTaskProcessor(new PageContentFetchProcessor());
  46. mapWorker_2.setTaskProcessor(new PageContentFetchProcessor());
  47. mapWorker_3.setTaskProcessor(new PageContentFetchProcessor());
  48. mapWorker_4.setTaskProcessor(new PageContentFetchProcessor());
  49.  
  50. mappers.add(mapWorker_1);
  51. mappers.add(mapWorker_2);
  52. mappers.add(mapWorker_3);
  53. mappers.add(mapWorker_4);
  54.  
  55. // one reducer
  56. ConfigurableWorker reduceWorker_1 = new ConfigurableWorker(“W_R1″);
  57. reduceWorker_1.setTaskProcessor(new URLMatchingProcessor());
  58.  
  59. // bind reducer to final result class
  60. MapReduceURLMiningMain main = new MapReduceURLMiningMain();
  61. reduceWorker_1.addListener(main);
  62.  
  63. // initiate tasks
  64. addTask2Worker(mapWorker_1, new MapReducePageURLMiningTask(“http://www.taobao.com”));
  65. addTask2Worker(mapWorker_2, new MapReducePageURLMiningTask(“http://www.xinhuanet.com”));
  66. addTask2Worker(mapWorker_3, new MapReducePageURLMiningTask(“http://www.zol.com.cn”));
  67. addTask2Worker(mapWorker_4, new MapReducePageURLMiningTask(“http://www.sina.com.cn/”));
  68.  
  69. // bind mapper to reduer
  70. Map2ReduceConnector connector = new Map2ReduceConnector(Arrays.asList(reduceWorker_1));
  71. mapWorker_1.addListener(connector);
  72. mapWorker_2.addListener(connector);
  73. mapWorker_3.addListener(connector);
  74. mapWorker_4.addListener(connector);
  75.  
  76. // start all
  77. mapWorker_1.start();
  78. mapWorker_2.start();
  79. mapWorker_3.start();
  80. mapWorker_4.start();
  81. reduceWorker_1.start();
  82.  
  83. String targetURL = EMPTY_STRING;
  84. int lastIndex = 0;
  85. while (foundURLs.size() < URL_SIZE_TO_MINE) {
  86. targetURL = foundURLs.pollFirst();
  87.  
  88. if (targetURL == null) {
  89. TimeUnit.MILLISECONDS.sleep(50);
  90. continue;
  91. }
  92.  
  93. lastIndex = ++lastIndex % mappers.size();
  94. MapReducePageURLMiningTask task = new MapReducePageURLMiningTask(targetURL);
  95. taskID2TaskMap.putIfAbsent(mappers.get(lastIndex).addTask(task), task);
  96.  
  97. TimeUnit.MILLISECONDS.sleep(100);
  98. }
  99.  
  100. // stop all
  101. mapWorker_1.stop();
  102. mapWorker_2.stop();
  103. mapWorker_3.stop();
  104. mapWorker_4.stop();
  105. reduceWorker_1.stop();
  106.  
  107. for (String string : foundURLs) {
  108. System.out.println(string);
  109. }
  110.  
  111. System.out.println(“Time Cost: ” + (System.currentTimeMillis() - startTime) + ”ms”);
  112. }
  113.  
  114. private static void addTask2Worker(ConfigurableWorker mapWorker_1, MapReducePageURLMiningTask task) {
  115. String taskID = mapWorker_1.addTask(task);
  116. taskID2TaskMap.put(taskID, task);
  117. }
  118.  
  119. @Override
  120. public List<WorkerEvent> intrests() {
  121. return Arrays.asList(WorkerEvent.TASK_COMPLETE, WorkerEvent.TASK_FAILED);
  122. }
  123.  
  124. @Override
  125. public void onEvent(WorkerEvent event, Object… args) {
  126. if (WorkerEvent.TASK_FAILED == event) {
  127. System.err.println(“Error while extracting URLs”);
  128. return;
  129. }
  130.  
  131. if (WorkerEvent.TASK_COMPLETE != event)
  132. return;
  133.  
  134. MapReducePageURLMiningTask task = (MapReducePageURLMiningTask) args[0];
  135. if (!taskID2TaskMap.containsKey(task.getTaskID()))
  136. return;
  137.  
  138. foundURLs.addAll(task.getMinedURLs());
  139.  
  140. System.out.println(“Found URL size: ” + foundURLs.size());
  141.  
  142. taskID2TaskMap.remove(task.getTaskID());
  143. }
  144. }

 

结果对比

Y轴为抓取X轴URL个数所用的时间

总结

我们可以看到,worker模式组合是非常灵活的,它真的就像一个活生生的工人,任你调配。使用worker,我们可以更方便地实现更复杂的结构。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值