[lucene第二季]利用缓存辅助精确更新索引

news/2024/7/6 23:28:58 标签: lucene, 数据库, 搜索引擎, object, string, sql

在上一篇的lucene的入门篇中,我们编写了一个帮助系统,从数据库中将具体的信息获取出来并使用CJKAnalyzer分词后建立索引,提供针对于关键字的搜索服务,其中我们采用定时器的方式每隔10分钟更新一次索引,更新的方式为先删除所有的索引,然后重新建立索引。这样的做法效率低下,直接限制了应用场景的小数据量化,一旦数据量较大,则删除索引和建立索引所带来的系统性能的耗费与搜索本身的开销相比得不偿失。

现在我们将改进一下这个刷新机制,同时采用一些其他手段来提高搜索的效率。我们希望每次的索引的更新,只更新那些修改过的数据,对于没有修改过的数据,则不去更新。试想这样的需求,有什么方法可以完成呢?

在此提两种方案:

1、根据最后修改时间限定。每次的更新的开始,保存更新时间,然后再下一次启动的时候,根据这个索引更新时间与数据库数据的最后修改时间对比,如果修改时间大于索引更新时间,那么需要进行更新,反之则不需要更新索引。

这样做的好处是对于索引的更新而言具有精确性,可以很容易的划分出需要更新的数据群,对于索引的更新效率很有帮助。但是,这个方案的直接后果是势必需要数据库提供一个标识最后更新时间的字段支持,也就是强制用户在使用的时候必须建立这个字段,如果这个数据并非具有频繁更新的特性,那么这个字段在模型上属于冗余的,直接对模型的建立产生影响,而且如果一旦用户遗漏掉这个字段,则会造成索引无法更新,搜索无法提供正确的服务。

这种对于用户有强制性要求的方案是不友好的,不建议这样做。

2、在每次数据库的更新、增加、删除等发生的时候,将受到影响的数据库记录的ID保存下来,然后每次更新根据这个ID去更新,同样的可以采用先删除再获取最新数据建立的方式。这样做的好处是用户不用去关注更新的问题,系统自动完成。

所以这里我们采用第2种方案来完成我们的实践。

 

分析下我们目前遇到的问题:

1、我们希望在每次的更新删除等操作时,保存具体的数据库记录的ID,那么这个ID如何获取?

2、获取到ID之后,我们如何传递给搜索引擎来实现定时刷新而不是频繁的实时刷新?

总的来说,这两个问题是我们确定了方案后很明显的显露出来的,其他的问题或许在开发过程中会出现,暂且不谈。

针对第一个问题,我们很容易的想到:使用方法拦截器。在执行某个方法之前或者之后,进行拦截并添加额外的逻辑处理。那么拦截的方法都有哪些呢?我们针对于不同的逻辑处理方法,分别进行怎样的拦截逻辑?

在上一篇中我们确定了一个很重要的基调,那就是我们将在之前封装的jdbcTemplate的抽象类之上进行业务逻辑开发,所以这里我们需要针对于所提供的抽象类AbstractBaseDAO进行处理,其中提供了一些删除、增加、修改相关的API,我们都需要去分别处理。

如果业务逻辑使用的不是我们提供的封装抽象类,那么只能自己去编写更新代码,这里我们默认使用自己封装好API的抽象dao类来实现逻辑。

所以,首先配置一个方法拦截器,并且设置拦截的对象(其实说白了就是一个动态代理):

 

 

我们配置了针对于上述的norQuickNewsDAO进行拦截,它继承了我们封装的抽象类AbsctactBaseDAO,所以在拦截器中,我们只需要参考抽象类中的增加、删除、修改相关的method的名称来识别并拦截处理即可。

这里说明一点的是,我们拦截的虽然是norQuickNewsDAO,但是拦截逻辑却并非只针对于当前的拦截bean,而是一个通用的,只要是继承了我们的 封装抽象类的DAO类,我们都是通用的拦截处理逻辑。这也是将具体业务逻辑抽离使代码通用的基调。

所以在拦截方法中,如果我们需要操作数据库获取数据,我们不能出现某个AbstractBaseDAO的子类,而是直接使用AbstractBaseDAO。而这个类是抽象,那么怎么办呢?很简单,使用一个匿名内部类来完成。

 

这样在操作数据库的时候,我们可以直接使用。这样解决了拦截器中需要操作数据库的问题。

我们看看抽象类中,需要拦截的方法有哪些:

我们采用一个枚举来保存需要拦截的方法,这些都是作为默认的拦截方法必须要处理的,其中第一个参数是拦截方法的名字,第二个参数为拦截方法的参数个数,我们用来进行一些安全性的检查。

 

为了操作方便,我们采用一个set来保存,简单转换一下:

 

这样在拦截器捕获到被拦截bean的一个方法的时候,只需要contain一下是否包含在需要拦截的方法列表中即可得知是否要进行处理,如果需要处理,只需要遍历枚举并定位以定位不同的拦截处理逻辑。诸如如下:

 

准备工作已经完成,现在我们来想想具体的拦截处理逻辑:

我们的目的是找出当前的方法处理完之后受到影响的记录的ID,这样的第一个想法是是否可以通过拦截方法的返回值或者参数来获取到呢?

1、对于根据ID修改、根据ID删除之类的方法,我们可以直接从参数中获取;

2、对于批量修改,批量删除,根据条件删除,自己编写sql语句操作等情况,我们无法通过返回值或者参数来获取;

3、对于增加,返回值是新增记录的ID,我们可以直接获取到,但是对于批量增加,我们无法获取;

 

看看我们的封装抽象类的API类型:

1、自动装配sql类;

2、外部传入sql类;

我们既然无法从返回值或者参数中获取,那就只能靠自己去想办法解析sql,来组装相同条件下的查询id的sql来获取当前的sql的影响面,这样就可以得到被影响的记录的ID。这个方法是一个增加了系统性能开销的方案,在数据量不大的情况下,是可以被接受的。因我们的搜索服务提供的就是非巨量数据的搜索服务。

剩下的事情很明显了,我们需要针对于不同的拦截方法,或者组装sql自动查询,或者解析sql再组装sql查询,而操作数据库所需要的bean,正可以由上面我们初始化的匿名内部类来完成,这也体现了我们jdbcTemplate封装抽象类抽离具体业务通用的优势。

我们通过拦截器获取到了影响到的ID,然后设置到搜索引擎中,由其定时任务来更新索引即可。10分钟一次的更新,将更新在这十分钟内发生改变的数据记录,这个数据可能也是较大的,比如上万条,自然的也是需要我们有一个性能的考虑,在获取到待更新列表后,我们的线程将以200个每次的速度去从数据库获取最新的数据并建立索引。同时对于更新的线程,我们需要控制对于待更新列表的同步,避免重复的更新操作耗费多余的系统性能。也就是一次只有一个线程在执行更新。如果第一个线程的执行时间超过了10分钟的定时,第二个线程启动的时候将因为无法使用待更新列表而等待,或者因为超时而取消。这都是在接受范围之内的,我们的目的就是避免出现多个线程同时更新的情况。

 

现在看看我们的搜索引擎的情况:系统启动的时候,将建立索引,建立索引是通过手动触发的,比如在后台增加一个建立索引和删除全部索引的控制入口,然后每隔10分钟,索引将重新刷新一次,这个时间根据系统的规模动态调整。需要考虑的一点是,刷新时间间隔越久,需要处理的数据就可能越多,当然可以用来处理的时间也就越长,数据的准确性也就越低(从一定程度上的误差)。最理想的情况是一个中间点时间,能够正好处理完所有更新的数据。这个需要尝试并且与机器性能、运行环境有关。

为了提升搜索的性能,在分布式的环境下,由于lucene是非分布式的,所以我们可以通过memcache来保存特定搜索条件的搜索结果,以实现分布式的共享。同时需要注意的是,引入分布式缓存后,相应的系统复杂度也提升了,比如多台lucene更新,n台memcahce服务器的问题,memcache的有效时间和lucene自动刷新时间的时间差问题等。这里我们先不引入memcache,重点先解决单台lucene本身的精确刷新的问题。

 

新的问题来了:对于一个系统,增加和删除修改操作是非常频繁的,在上述方案中,我们采用的是一旦发生更新,就立即从数据库搜索一遍影响面的ID,然后通知搜索引擎。这样带来的是非常频繁的数据库操作的开销,一次的更新操作将变成一次更新一次查询,数据库的消耗翻倍增加,而且大多数的更新操作其更新条件其实具有重复性,比如根据状态更新等。我们希望能够降低这个频率,既然搜索引擎具有时间差,这里也就没必要采用实时,只要保持在可接受的范围内的误差就可以。所以,我们需要一种手段,过滤掉一段时间内的重复操作带来的数据库开销。

我们引入一个缓存机制,这个缓存是本地的,而不是分布式的。

 

我们开发一个缓存服务(代码折叠着,可以点击打开折叠看全部代码)

我们保存的是:

key = 执行方法+执行参数

value = 执行方法的影响到的id

同时我们设置缓存的有效期为5分钟。这样就变成:当拦截到方法,首先从缓存中获取影响到的记录的ID列表,如果获取到并且尚未过期,则直接使用并传递给搜索引擎,如果没有获取到,那么从数据库中获取,同时设置到搜索引擎和缓存中。

这样可以过滤掉一段时间的重复请求,由于缓存中的ID肯定是已经存在于搜索引擎中了,所以搜索结果的时间差不变。

 

拦截器、搜索服务、本地缓存、封装抽象类、分页代码下载:

http://download.csdn.net/source/2610165

 

 


http://www.niftyadmin.cn/n/1639124.html

相关文章

[lucene]倒排笔记

lucene的倒排算法相关笔记: 计算文章中关键字出现的位置以及出现频率,以便于精准定位。 百度的定义:用记录的非主属性查找记录而组织的文件,叫倒排文件,或者 倒排索引,次索引 lucene不使用B树&#xff0…

[ElasticSearch]Java API 之 滚动搜索(Scroll API)

一般搜索请求都是返回一"页"数据,无论数据量多大都一起返回给用户,Scroll API可以允许我们检索大量数据(甚至全部数据)。Scroll API允许我们做一个初始阶段搜索并且持续批量从Elasticsearch里拉取结果直到没有结果剩下。…

mysql数据实时同步到Elasticsearch

业务需要把mysql的数据实时同步到ES,实现低延迟的检索到ES中的数据或者进行其它数据分析处理。本文给出以同步mysql binlog的方式实时同步数据到ES的思路, 实践并验证该方式的可行性,以供参考。 mysql binlog日志 mysql的binlog日志主要用于数据库的主…

[拦截器]关于拦截方法调用其他内部方法无法被拦截问题的解决

拦截器的实现原理很简单,就是动态代理,实现AOP机制。当外部调用被拦截bean的拦截方法时,可以选择在拦截之前或者之后等条件执行拦截方法之外的逻辑,比如特殊权限验证,参数修正等操作。但是如果现在一个需求是&#xff…

使用Logstash来实时同步MySQL数据到ES

本篇我们来实战从MYSQL里直接同步数据 一、首先下载和你的ES对应的logstash版本,本篇我们使用的都是6.1.1 下载后使用logstash-plugin install logstash-input-jdbc 命令安装jdbc的数据连接插件 二、新增mysqltoes.conf文件,配置Input和output参数如下&…

[lucene第三季]Lucene那点事儿-总结篇

前面两篇文章,简单尝试了lucene的一些应用,还是再回头想想我们的需求吧,我们希望能够开发一个淘宝一样的针对商品的搜索服务,提供多种条件的组合搜索,并且对于性能提出了一定的要求。同时我们希望这个小型的搜索引擎具…

CentOS7下安装部署ES及head插件安装

1.新建一个用户elasticsearch,当然也可以不创建用户,直接用系统用户来安装和运行elasticserach [rootlocalhost ~]#useradd elasticsearch 接下来修改系统配置,这里不修改的话es运行会报错: max file descriptors [4096] for elasticsearch process is too low,…

[lucene那点事儿]想说爱你很容易

内容提要: ---------------------目录开始-------------------- 1、索引精确刷新问题 2、利用缓存提高索引批量更新拦截器的性能 3、针对不同的数据来源建立不同的索引并分域存放 4、引入xml配置文件的方式实现索引建立的动态配置 5、单值搜索、组合条件搜索等…