很早就想嘗試一下爬蟲,相關的博文已經很多,這里記下幾個困擾了我挺久的問題。
既然說的是死鎖,我們來復習一下死鎖的四個條件:
- 循環(huán)等待
- 占有且請求(請求與持有)
- 互斥(資源有限,每次只能被一個或一類線程使用)
- 不可搶占(不可剝奪,無優(yōu)先級)
四個條件中不可被破壞的是互斥條件,即多進程同時訪問會有數(shù)據(jù)的不一致性。
言歸正傳,首先在我的實現(xiàn)中自定義了線程池:
public ThreadPoolExecutor getFixedThreadPool(int corePoolSize,int maxPoolSize,int waitingQueuesize) {
return new ThreadPoolExecutor(corePoolSize, maxPoolSize, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(waitingQueuesize),new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable run, ThreadPoolExecutor executor) {
if(!executor.isTerminated())
try {
executor.getQueue().put(run);
} catch (Exception e) {
LOG.error("Inner Exception in workQueue, putting task error",e);
}
}
});
//return Executors.newFixedThreadPool(2*nThread);
}
這是一個類newFixedThreadPool的操作,自定義了等待隊列的大小,同時隊列滿時阻塞了入隊操作,避免Int.maxValue造成的溢出或是過多的任務堆積,兼具fixedThreadPool和cachedThreadPool的優(yōu)點,可這里阻塞的拒絕策略讓我在后續(xù)的實現(xiàn)中飽受折磨...
然后為了避免任務都被阻塞在線程池,我又額外開了一個阻塞隊列存儲爬出來的待爬URL,然后用一個監(jiān)控線程監(jiān)控這個隊列,爬蟲任務相當于生產者生產待爬URL,監(jiān)控線程相當于消費者消費產生的URL,而這個隊列就是倉庫了,完美的設計。。。
public class WatchTask implements Runnable{
@Override
public void run() {
while(isCrawl) {
try{
String url=urlQueue.poll(1000, TimeUnit.MILLISECONDS);
while(StringUtils.isNotBlank(url) && isCrawled(url)) url=urlQueue.poll(100, TimeUnit.MILLISECONDS);
if(StringUtils.isNotBlank(url)) executor.execute(new WorkTask(url));
}catch(Exception e) {
LOG.error("Taking url from blocking queue error, urlQueue size:"+urlQueue.size(),e);
}
lastCur=System.currentTimeMillis();
LOG.info("WatchTask running, urlQueue:"+urlQueue.size());
}
}
}
public class WorkTask implements Runnable{
private String seedUrl=null;
public WorkTask(String seedUrl) {
this.seedUrl=seedUrl;
}
@Override
public void run() {
List<String> urls;
try {
urls=crawler.doCrawl(seedUrl);
if(urls==null || urls.size()==0) return;
for(String url:urls) {
if(StringUtils.isNotBlank(url) && !isCrawled(url)) {
urlQueue.put(url);
}
}
}catch(Exception e) {
LOG.error("Puting url to blocking queue error, size:"+urlQueue.size(),e);
}
}
}
在程序中URL隊列的大小要遠大于線程池等待隊列,明眼的朋友到這里應該看出我的操作問題在哪里了:

于是,將額外的阻塞隊列和監(jiān)控任務去掉,工作線程改成這樣,頗有種自給自足的感覺:
public class WorkTask implements Runnable{
private String seedUrl=null;
public WorkTask(String seedUrl) {
this.seedUrl=seedUrl;
}
@Override
public void run() {
List<String> urls;
try {
urls=crawler.doCrawl(seedUrl);
if(urls==null || urls.size()==0) return;
for(String url:urls) {
if(StringUtils.isNotBlank(url) && !isCrawled(url)) {
executor.execute(new WorkTask(url));
}
}
}catch(Exception e) {
LOG.error("Puting url to blocking queue error, size:"+urlQueue.size(),e);
}
}
}
然而,實際運行中發(fā)現(xiàn)能爬取的數(shù)據(jù)條數(shù)在線程池最大線程數(shù)左右,往后程序就像掛掉一樣雖然在跑但什么輸出都沒有,肯定又是阻塞了!
經過一番分析,發(fā)現(xiàn)問題回到了線程池本身的等待隊列,圓圈代表線程池,黑點表示線程非空閑:

就這樣,又一個死鎖創(chuàng)造出來了,其原因歸根到底還是一個種子url能爬取出來的子URL太多了——幾百甚至幾千上萬個(沒錯我在爬某網(wǎng)用戶信息,子url是用戶的粉絲或其關注的人,因為一些需求不能進行部分舍棄),既然如此那就把等待隊列設至大一點,對子url太多的,全部舍棄,至于何為多大家自有判斷,我用子url數(shù)和等待隊列大小關系來決定,當?shù)却犃兄衭rl數(shù)量超過等待隊列容量的一半,或子url數(shù)量超過隊列數(shù)量一半退出:
public class WorkTask implements Runnable{
private String seedUrl=null;
public WorkTask(String seedUrl) {
this.seedUrl=seedUrl;
}
@Override
public void run() {
List<String> urls;
try {
urls=crawler.doCrawl(seedUrl);
int size=tpe.getQueue().size();
//無子url,或隊列中任務數(shù)量超過容量一半,或url數(shù)量超過隊列數(shù)量一半,避免崩掉故退出
if(urls==null || urls.size()==0 || size>halfQueueSize || urls.size()>halfQueueSize) return;
System.out.println("****************add to queue with size"+urls.size());
for(String url:urls) {
if(StringUtils.isNotBlank(url) && !isCrawled(url)) {
executor.execute(new WorkTask(url));
}
}
}catch(Exception e) {
LOG.error("Puting url to blocking queue error, size:"+urlQueue.size(),e);
}
}
}
至此,終于把死鎖的問題解決了,但是爬蟲跑了一會ip就被封了,下一步是使用代理。
本文為本人解決實際問題的記錄,有任何高見歡迎留言。