最近由于家里的事回去了趟,而且在忙着搞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了。
这篇就写到这里了,如果有什么不明白的请@我 ,先吃饭:)