最近由于家里的事回去了趟,而且在忙着搞openfire和spark的二次开发,搜索这块的博客更新就慢下来了,本来打算今天更新搜索流程第四章的,但是想到一个由于近实时搜索造成的AlreadyCloseException给我造成蛮大的困扰,在网上找了这块很少的资料,我就想把我的方法贡献出来,下面我们一起来解析下方法。
起因:
根据Lucene的打开IndexReader只是建立了一个snapshot的原理,如果我们想要实时的更新索引,只能用IndexReader的isCurrent方法在每次搜索前检查索引是否是最新的,如果不是最新的,则需要调用reopen来重新打开索引,但是不能调用open方法了,因为性能损耗太大了,问题由此产生先看如下代码
package com.tianwen.eeducation.server.searchengine.core.query;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.MultiReader;
import org.apache.lucene.search.IndexSearcher;
/**
*
* 索引搜索器管理器
*
* @author 曾杰
* @version [版本号, 2012-5-3]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
class SearcherManager
{
private static final ServerLogger LOGGER = new ServerLogger(SearcherManager.class);
private static Set<ReaderWrapper> grabageSet;
private static Thread checkThread;
static
{
// 检查线程只需要一个,所以是静态的,在类加载的时候就启动了
grabageSet = new ConcurrentSkipListSet<SearcherManager.ReaderWrapper>();
checkThread = new Thread(new CheckRunner());
checkThread.setDaemon(true);
checkThread.setName("Reader-Collector");
checkThread.start();
}
private DirectoryBean directory;//用来保存一个索引的目录信息,一个索引只可能实例化一个
private ReaderWrapper ramWrapper;
private ReaderWrapper fsWrapper;
private IndexSearcher searcher;
SearcherManager(DirectoryBean directory)
throws CorruptIndexException, IOException
{
this.directory = directory;
this.ramWrapper = new ReaderWrapper();
this.ramWrapper.setTarget(IndexReader.open(directory.getRamDirectory()));
this.fsWrapper = new ReaderWrapper();
this.fsWrapper.setTarget(IndexReader.open(directory.getFsDirectory()));
searcher = new IndexSearcher(new MultiReader(fsWrapper.getTarget(), ramWrapper.getTarget()));
searcher.setSimilarity(new ClientSimilarity());
}
/**
* 获取IndexSearcher执行对应的操作
*
* @param work
* @throws QueryException [参数说明]
*
* @return void [返回类型说明]
* @exception throws [违例类型] [违例说明]
* @see [类、类#方法、类#成员]
*/
public void doWork(SearcherWork work)
throws QueryException
{
try
{
checkIndexChange();
ramWrapper.counter.incrementAndGet();
fsWrapper.counter.incrementAndGet();
work.doSearch(searcher);
}
catch (QueryException e)
{
throw e;
}
catch (Exception e)
{
throw new QueryException(e);
}
finally
{
ramWrapper.counter.decrementAndGet();
fsWrapper.counter.decrementAndGet();
}
}
public void destroy()
{
synchronized (checkThread)
{
collectGrabage();
}
IndexUtil.closeReader(fsWrapper.getTarget());
IndexUtil.closeReader(ramWrapper.getTarget());
}
/**
* 清除垃圾Reader
*
* @return void [返回类型说明]
* @exception throws [违例类型] [违例说明]
* @see [类、类#方法、类#成员]
*/
private static void collectGrabage()
{
// SearcherManager.LOGGER.debug("Collector Start Check Grabage...");
if (grabageSet == null)
{
return;
}
if (grabageSet.size() == 0)
{
return;
}
// 转换成数组防止 ConcurrentModificationException
ReaderWrapper[] wrappers = grabageSet.toArray(new ReaderWrapper[grabageSet.size()]);
for (ReaderWrapper readerWrapper : wrappers)
{
if (readerWrapper.counter.intValue() == 0)
{
IndexUtil.closeReader(readerWrapper.getTarget());
grabageSet.remove(readerWrapper);
}
}
}
/**
* 查询前检测索引是否改变,如果改变则重新加载
*
* @throws IOException [参数说明]
*
* @return void [返回类型说明]
* @exception throws [违例类型] [违例说明]
* @see [类、类#方法、类#成员]
*/
private void checkIndexChange()
throws IOException
{
boolean fsChanged = !fsWrapper.getTarget().isCurrent();
boolean ramChanged = !ramWrapper.getTarget().isCurrent();
if (fsChanged || ramChanged)
{
synchronized (directory)
{
fsChanged = !fsWrapper.getTarget().isCurrent();
ramChanged = !ramWrapper.getTarget().isCurrent();
if (!fsChanged && !ramChanged)
{
return;
}
IndexReader newReader = null;
if (ramChanged)
{
newReader = IndexReader.openIfChanged(ramWrapper.getTarget());
grabageSet.add(ramWrapper);
ramWrapper = new ReaderWrapper();
ramWrapper.setTarget(newReader);
}
if (fsChanged)
{
newReader = IndexReader.openIfChanged(fsWrapper.getTarget());
grabageSet.add(fsWrapper);
fsWrapper = new ReaderWrapper();
fsWrapper.setTarget(newReader);
}
searcher = new IndexSearcher(new MultiReader(fsWrapper.getTarget(), ramWrapper.getTarget()));
searcher.setSimilarity(new ClientSimilarity());
}
}
}
private class ReaderWrapper implements Comparable<ReaderWrapper>
{
private AtomicInteger counter = new AtomicInteger(0);
private IndexReader target;
public IndexReader getTarget()
{
return target;
}
public void setTarget(IndexReader target)
{
this.target = target;
}
public int compareTo(ReaderWrapper other)
{
return this.hashCode() - (other == null ? 0 : other.hashCode());
}
}
/**
*
* 表示一个搜索工作
*
* @author zengj
* @version [版本号, 2012-5-3]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
public static interface SearcherWork
{
/**
* 提供一个IndexSearcher对象供使用
*
* @param searcher
* @throws QueryException [参数说明]
*
* @return void [返回类型说明]
* @exception throws [违例类型] [违例说明]
* @see [类、类#方法、类#成员]
*/
public void doSearch(IndexSearcher searcher)
throws QueryException;
}
/**
*
* 用来检测关闭已经无用的IndexReader对象的线程
*
* @author zengj
* @version [版本号, 2012-5-3]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
private static class CheckRunner implements Runnable
{
public void run()
{
while (true)
{
synchronized (this)
{
// 每隔一秒自动检测下
try
{
this.wait(500);
}
catch (InterruptedException e)
{
}
if (grabageSet == null)
{
return;
}
collectGrabage();
}
}
}
}
}
大家需要看的是哪个doWork方法和checkIndexChange方法,我们的所有的搜索操作都是通过doWork调用一个回调完成的,检查并且加载最新索引就是通过checkIndexChange做到
假设有A和B两个线程,A线程运行到了checkIndexChange方法处,通过isCurrent返回了false得知了索引有所改变了,这时候进入同步块(synchronzied),重新打开新的IndexReader,然后将原来旧的IndexReader加入grabgeSet等待CheckRunner这个守护线程关闭,如果这个时候恰好CheckRunner也关闭这个Reader,这个时候B线程也进来doWork,进入checkIndexChange,这个时候就要调用isCurrent,而调用这个方法是要ensureOpen(确定indexreader是否打开)的,简单来说就是Lucene会判断这个Reader的引用计数器,如果这个时候Reader被关闭了,引用计数器就会为0,然后B线程上就会抛出AlreaderCloseException了,但是我们又不能给doWork方法加同步块,这样的并发性能可想而知。
解决:
看来问题还是在读索引和重新加载索引之间无法权衡啊,面对这种问题我们就可以请上我们的帮手了:ReentrentReadWriteLock
这个类位于java.util.concurrent.locks包下面,是一个读写锁的实现,关于他的具体功能和原理我这里不多讲,因为网上有很多关于它的文章,简单来说就是这个东西它有两个锁,一个读锁和一个写锁,他的特性是:多个线程可同时获取读锁,但是只有在写锁没有被获取的情况下,一旦一个线程获取了写锁,其他线程即不能获取到写锁,当然也不能获取到读锁,一个线程如果有了读锁就不能获取写锁了,但是一个线程如果有写锁却可以再获取读锁。这些东西说起来挺麻烦的,让我们直接来看代码
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();//这就是我们的锁
/**
* 获取IndexSearcher执行对应的操作
*
* @param work
* @throws QueryException [参数说明]
*
* @return void [返回类型说明]
* @exception throws [违例类型] [违例说明]
* @see [类、类#方法、类#成员]
*/
public void doWork(SearcherWork work)
throws QueryException
{
try
{
lock.readLock().lock();//先获取读锁
checkIndexChange();
ramWrapper.counter.incrementAndGet();
fsWrapper.counter.incrementAndGet();
work.doSearch(searcher);
}
catch (QueryException e)
{
throw e;
}
catch (Exception e)
{
throw new QueryException(e);
}
finally
{
lock.readLock().unlock();//最后流程完成,整个读锁也释放掉
ramWrapper.counter.decrementAndGet();
fsWrapper.counter.decrementAndGet();
}
}
/**
* 查询前检测索引是否改变,如果改变则重新加载
*
* @throws IOException [参数说明]
*
* @return void [返回类型说明]
* @exception throws [违例类型] [违例说明]
* @see [类、类#方法、类#成员]
*/
private void checkIndexChange()
throws IOException
{
boolean fsChanged = !fsWrapper.getTarget().isCurrent();
boolean ramChanged = !ramWrapper.getTarget().isCurrent();
if (fsChanged || ramChanged)
{
lock.readLock().unlock();//必须要先释放读锁,否则写锁无法获取到
lock.writeLock().lock();//获取到写锁,上面就无法获取到了读锁,就会等待
try//下面执行重新加载操作
{
fsChanged = !fsWrapper.getTarget().isCurrent();
ramChanged = !ramWrapper.getTarget().isCurrent();
if (!fsChanged && !ramChanged)
{
return;
}
IndexReader newReader = null;
if (ramChanged)
{
newReader = IndexReader.openIfChanged(ramWrapper.getTarget());
grabageSet.add(ramWrapper);
ramWrapper = new ReaderWrapper();
ramWrapper.setTarget(newReader);
}
if (fsChanged)
{
newReader = IndexReader.openIfChanged(fsWrapper.getTarget());
grabageSet.add(fsWrapper);
fsWrapper = new ReaderWrapper();
fsWrapper.setTarget(newReader);
}
searcher = new IndexSearcher(new MultiReader(fsWrapper.getTarget(), ramWrapper.getTarget()));
searcher.setSimilarity(new ClientSimilarity());
}
finally
{
lock.readLock().lock();//继续获取读锁,因为后面我们还要进行搜索操作
lock.writeLock().unlock();//然后释放写锁
}
}
}
看了上面的注释详细大家都看的明白了,还是A和B,如果A进入了checkIndexChange并检测到了索引改变,就会先等待所有的读操作完成,然后获取到写锁,这时候B线程如果doWork,因为写锁被A获取到了,表示正在写,B就会阻塞在checkIndexChange之前,直到A重新加载索引操作的完成,然后释放写锁,B才可以继续进行下一步操作,这样就彻底避免了AlreadyCloseException了。
这篇就写到这里了,如果有什么不明白的请@我 ,先吃饭:)