让你的实时搜索引擎远离AlreadyCloseException

最近由于家里的事回去了趟,而且在忙着搞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了。

这篇就写到这里了,如果有什么不明白的请@我 ,先吃饭:)

你可能感兴趣的:(java,Lucene)