第五篇,对象池的设计与实现
前面每爬取一个任务都对应一个Job任务,试想一下,当我们爬取网页越来越多,速度越来越快时,就会出现频繁的Job对象的创建和销毁,因此本片将考虑如何实现对象的复用,减少频繁的gc
设计
我们的目标是设计一个对象池,用于创建Job任务,基本要求是满足下面几点:
- 可以配置对象池的容量大小
 
- 通过对象池获取对象时,遵循一下规则:
- 对象池中有对象时,总对象池中获取
 
- 对象池中没有可用对象时,新创建对象返回(也可以采用阻塞,直到有可用对象,我们这里采用直接创建新对象方式)
 
 
- 对象用完后扔回对象池
 
实现
1. 创建对象的工厂类 ObjectFactory
对象池在初始化对象时,借用对象工厂类来创建,实现解耦
1 2 3 4 5
   | public interface ObjectFactory<T> {
      T create();
  }
  | 
 
2. 对象池中的对象接口 IPollCell
因为每个对象都拥有自己的作用域,内部包含一些成员变量,如果对象重用时,这些成员变量的值,可能会造成影响,因此我们定义 IPoolCell 接口,其中声明一个方法,用于重置所有的变量信息
1 2 3 4 5 6 7 8
   | public interface IPoolCell {
      
 
      void clear();
  }
  | 
 
3. 一个简单的对象池 SimplePool
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
   | package com.quick.hui.crawler.core.pool;
  import lombok.extern.slf4j.Slf4j;
  import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger;
 
 
 
  @Slf4j public class SimplePool<T extends IPoolCell> {
      private static SimplePool instance;
 
      public static void initInstance(SimplePool simplePool) {         instance = simplePool;     }
      public static  SimplePool getInstance() {         return instance;     }
 
      private int size;
      private BlockingQueue<T> queue;
      private String name;
      private ObjectFactory objectFactory;
 
      private AtomicInteger objCreateCount = new AtomicInteger(0);
      public SimplePool(int size, ObjectFactory objectFactory) {         this(size, "default-pool", objectFactory);     }
      public SimplePool(int size, String name, ObjectFactory objectFactory) {         this.size = size;         this.name = name;         this.objectFactory = objectFactory;
          queue = new LinkedBlockingQueue<>(size);     }
 
      
 
 
      public T get() {         T obj = queue.poll();
          if (obj != null) {             return obj;         }
          obj = (T) objectFactory.create();         int num = objCreateCount.addAndGet(1);
 
          if (log.isDebugEnabled()) {             if (objCreateCount.get() >= size) {                 log.debug("objectPoll fulled! create a new object! total create num: {}, poll size: {}", num, queue.size());             } else {                                  log.debug("objectPoll not fulled!, init object, now poll size: {}", queue.size());             }         }
          return obj;     }
 
      
 
 
 
      public void release(T obj) {         obj.clear();
                   boolean ans = queue.offer(obj);
          if (log.isDebugEnabled()) {             log.debug("return obj to pool status: {}, now size: {}", ans, queue.size());         }     }
 
      public void clear() {         queue.clear();     } }
   | 
 
上面的方法中,主要看get和release方法,简单说明
get 方法
- 首先是从队列中获取对象(非阻塞方式,获取不到时返回null而不是异常)
 
- 队列为空时,新建一个对象返回
- 未初始化队列,创建的对象表示可回收重复使用的
 
- 队列填满了,但是被其他线程获取完了,此时创建的对象理论上不需要重复使用,用完一次就丢掉
 
 
 
- release 方法
 
4. Job修改
既然要使用对象池,那么我们的IJob对象需要实现 IPoolCell接口了
将实现放在 DefaultAbstractCrawlJob 类中
1 2 3 4 5 6 7
   | @Override public void clear() {     this.depth = 0;     this.crawlMeta = null;     this.fetchQueue = null;     this.crawlResult = null; }
   | 
 
使用
上面只是实现了一个最简单的最基础的对象池,接下来就是适配我们的爬虫系统了
之前的创建Job任务是在 com.quick.hui.crawler.core.fetcher.Fetcher#start 中直接根据传入的class对象来创建对象,因此,第一步就是着手改Fetcher类
1. 初始化对象池
创建方法修改,新增对象池对象初始化:Fetcher.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
   | public <T extends DefaultAbstractCrawlJob> Fetcher(Class<T> jobClz) {     this(0, jobClz); }
 
  public <T extends DefaultAbstractCrawlJob> Fetcher(int maxDepth, Class<T> jobClz) {     this(maxDepth, () -> {         try {             return jobClz.newInstance();         } catch (Exception e) {             log.error("create job error! e: {}", e);             return null;         }     }); }
  public <T extends DefaultAbstractCrawlJob> Fetcher(int maxDepth, ObjectFactory<T> jobFactory) {     this.maxDepth = maxDepth;     fetchQueue = FetchQueue.DEFAULT_INSTANCE;     threadConf = ThreadConf.DEFAULT_CONF;     initExecutor();
      SimplePool simplePool = new SimplePool<>(ConfigWrapper.getInstance().getConfig().getFetchQueueSize(), jobFactory);     SimplePool.initInstance(simplePool); }
  | 
 
说明
为什么将创建的对象池座位 SimplePool的静态变量 ?
因为每个任务都是异步执行,在任务执行完之后扔回队列,这个过程不在 Fetcher对象中执行,为了共享对象池,采用了这种猥琐的方法
2. 启动方法修改
在创建 Fetcher 对象时,已经初始化好对象池,因此start方法不需要接收参数,直接改为
1 2 3 4 5 6 7 8 9 10 11
   | public <T extends DefaultAbstractCrawlJob> void start() throws Exception {   ....
    DefaultAbstractCrawlJob job = (DefaultAbstractCrawlJob) SimplePool.getInstance().get();   job.setDepth(this.maxDepth);   job.setCrawlMeta(crawlMeta);   job.setFetchQueue(fetchQueue);
    executor.execute(job);      ...
  | 
 
测试
测试代码与之前有一点区别,即 Fetcher 在创建时选择具体的Job对象类型,其他的没啥区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
   | public static class QueueCrawlerJob extends DefaultAbstractCrawlJob {
      public void beforeRun() {                  super.setResponseCode("gbk");     }
      @Override     protected void visit(CrawlResult crawlResult) {
      } }
 
  @Test public void testCrawel() throws Exception {     Fetcher fetcher = new Fetcher(2, QueueCrawlerJob.class);
      String url = "http://chengyu.911cha.com/zishu_4.html";     CrawlMeta crawlMeta = new CrawlMeta();     crawlMeta.setUrl(url);     crawlMeta.addPositiveRegex("http://chengyu.911cha.com/zishu_4_p[0-9]+\\.html$");
      fetcher.addFeed(crawlMeta);
 
      fetcher.start(); }
  | 
 
待改进
上面只是实现了一个最基本简单的对象池,有不少可以改进的地方
- 对象池实例的维护,上面是采用静态变量方式,局限太强,导致这个对象池无法多个共存
 
- 对象池大小没法动态配置,初始化时设置好了之后就没法改
 
- 可考虑新增阻塞方式的获取对象
 
以上坑留待后续有空进行修改
3. 源码地址
项目地址: https://github.com/liuyueyi/quick-crawler
对象池对应的tag: v0.008
相关博文
Quick-Crawel爬虫系列博文
参考
II. 其他
一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
声明
尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
扫描关注
