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

内容提要:

---------------------目录开始--------------------

1、索引精确刷新问题

2、利用缓存提高索引批量更新拦截器的性能

3、针对不同的数据来源建立不同的索引并分域存放

4、引入xml配置文件的方式实现索引建立的动态配置

5、单值搜索、组合条件搜索等多种搜索功能的实现

6、其他以及单元测试

 ------------------------目录完-------------------

part1、索引的精确刷新以及利用缓存提高索引批量更新拦截器的性能

使用lucene建立的搜索服务,搜索结果的准确性很大程度上依赖于索引,尤其是针对于数据库数据建立的索引,一旦数据库的数据改变,而索引中的数据则成为脏数据,需要执行刷新操作以保持最新的状态。通常而言,这个过程由一个单独的任务启动一个线程来完成。而本篇要讲,即如何只去精确的刷新那些被修改过的数据,而不是全部的索引重新建立的方式去刷新。

如果要精确的只刷新那些被修改的数据,那么需要知道,两次定时任务之间,哪些数据被修改了,记录下来并在下一次的定时任务中执行刷新。通常而言,这个过程可以有多种方式,但是这里我们针对的是前面的文章中讲述的针对spring的jdbcTemplate进行的日常API的封装,

(在后续的开发过程中,所有的DAO都将继承于这个类),并且我们希望能够尽可能减少业务代码的交织,能够采用一种通用的方式,使得多个继承于abstractBaseDAO的子类的这些更新方法,只要是我们希望去获取的,都能够被自动获取,而不是每次针对不同的子类DAO编码相关的获取代码。所以很自然的,我们需要将这个获取代码往底层抽象,我们的目标就是减少业务代码的交织,提高代码的通用性,实现底层向上的服务。

所以最直接的,就是在底层操作数据库的时候,动态获取,这个获取方式,我们采用拦截器的方式。即当外部调用增加删除修改方法的时候,能够自动的拦截相关的调用并获取本次操作影响到的数据记录的ID,然后记录下来,并且不同的子类DAO可以对应不同的ID列表。

 

举个例子:我们有两个DAO,一个是商品相关的DAO,一个是相关的交易流水的DAO。

对于我们的网站搜索功能而言,我们提供的是针对于商品的搜索,而不会提供搜索交易流水。所以当商品DAO子类调用父类AbstractBaseDAO的相关的数据库修改方法的时候,需要执行拦截,记录相关的影响到的记录的ID,而交易流水DAO子类调用的时候,则不需要被拦截。

 

比如我们现在希望去拦截网站商品相关信息修改的操作,配置一个拦截子类DAO的拦截器:

 

这里遇到一个问题:如上述例子而言,我们在子类dao中会调用抽象父类abstractBaseDAO的数据库操作通用方法,而且我们希望的拦截的,也就是父类的这些操作方法,但是通常而言,拦截器涉及两个对象,即代理对象和原始对象,拦截器执行的是代理对象,子类调用父类方法,这种调用属于方法的内部调用,已经执行在原始对象中而不是代理对象中,所以这种内部调用无法被拦截。

比如类A提供了一个method1,类B继承类A,在method2方法中调用类A的method1,那么在拦截的过程中,只会拦截类B的method2,而不会去拦截父类的method1;

所以需要我们处理的是,如何去触发二次拦截的问题。

在前面的文章中,我们采用了一种AopContext.currentProxy()来获取代理对象再调用以触发拦截的方式,在本篇中,我们依然采用这种方式,但是对于代码进行了重构,我们不希望每个子类DAO都去编写这些重复的工作,所以我们采用如下的方式:

    方法内部调用,虽然通过AopContext.currentProxy()可以触发拦截,但是有一个前提条件就是,如果是面向接口的编程,即如果DAO是具有一个interface的,那么父类的这个方法,必须也存在于当前DAO接口的方法声明中。否则,应该存在于子类中。因为代理对象代理的是子类而不是父类。

这个包含的工作是一个重复性的,针对于我们封装的jdbcTemplate的AbstractBaseDAO,我们采用一个抽象父接口的形式,来降低这部分工作的重复

这样其他的子类DAO的接口只要继承于这个父接口皆可以在本接口中增加父类中相关操作的声明,以便于拦截。

同时,虽然解决了包含的工作,还有一个很重要的事情,就是在调用父类代码的时候,需要先判断上下文线程中是否有代理,如果存在代理,那就使用代理外调用,如果不存在代理,那就是用普通的内部调用。代码如下:

在AbstractBaseDAO中提供的数据库修改方法,在需要进行拦截的子类中调用的时候,都要采用如此的方式去调用,可以看其中的代码其实并不是跟业务相关的,我们采用AbsBaseDAOMethIntercepDefine接受线程上下文中的拦截对象,这个拦截对象其实是一个子类实例,由于我们的子类DAO的interface已经继承了AbsBaseDAOMethIntercepDefine,所以,这里可以直接使用AbsBaseDAOMethIntercepDefine接受,它相当于是一个处于最上层的父类AbsBaseDAOMethIntercepDefine。

所以,这部分代码我们需要抽取出来,我们采用新建一个抽象父类:

它处于AbstractBaseDAO和子类DAO之间,提供的是针对于那些需要执行拦截的子类的执行增加、删除、修改等操作的API。它的内部完成了上述的操作,这样如果子类的数据库操作需要被拦截,那么只要替换继承AbstractBaseDAO而继承于它即可。

它完整的代码:

上述解决了拦截代码的通用性问题,之后需要增加新的拦截DAO子类,只需要按照上述的步骤,来完成配置即可。剩下要做的,就是开发拦截器。

在前面文章中,我们已经完成了这个工作,这里我们进行了重构,使其可以拦截多个方法,将当前数据库操作所涉及的记录的ID获取出来并且发送给搜索引擎,由搜索引擎保存并将有定时刷新任务刷新。由于当前的系统可能有多种索引,导致拦截器拦截的DAO子类将有多个,所以,拦截器获取到ID后发送给搜索引擎的ID需要带有一个标记性的KEY,这些ID列表在搜索引擎中是使用一个map来保存的,其key值即唯一性的标记,特别注意的是,这里我们采用的是DO的名称字符串,value为对应的待更新id列表。

看看他的代码:

实时的拦截可能会导致对重复方法的频繁拦截,所以在执行完一次拦截之后,我们会将本次执行拦截的方法名称和参数都放入缓存中,有效时间为5分钟,这样保证5分钟内的重复拦截都将被过滤掉,减轻系统压力。

我们设置一个key值,其公式为:调用DAO涉及的DO的类型字符串+方法名称+(参数=值)

通常而言,理想的做法是通过DAO类型字符串加方法和参数的方式来判断,但是这里场景是,当二次拦截进入方法的时候,这里拦截类的类型是AbstractBaseDAO,而不是具体的某个子类DAO。所以,无法获取某个具体子类dao的类型。

这样,在缓存中唯一性的标记某个DAO的操作,注意我们是通过DO类型来判断,我们严格的遵守每个DAO 原子性的操作一个数据对象的原则,当然,这里也支持一个DAO操作不同的Do的操作。

缓存的执行代码很简单,前面的文章中已经有相关代码,此处省略。注意的是,它可以存放任何数据,而不仅限制于当前场景下的索引ID数据。

 

part2,引入xml配置文件的方式实现索引建立的动态配置,同时实现针对不同的数据来源建立不同的索引并分域存放

我们会在系统中建立多个索引,比如商品的索引,会员的索引,我们希望能够以一种简便的方式,来配置相应的需要建立索引的数据源和建立索引的字段以及索引存放的路径等信息。在拦截器中,使用的是一个set来保存的,这里自然的我们也可以采用一个List来保存,但是对于配置方面,我们希望能够有一种简洁而清晰的方式,而不是每次新增需要建立索引的内容的时候都去找到这个搜索bean然后修改其property的部分。

自然的,我们将采用xml配置文件的方式,类似如下:

我们配置了需要建立索引的类型名称,需要建立索引的字段,以及索引存放的位置,这样的配置清晰而易于管理,以后的扩展性也很方便。相应的,我们的搜索引擎需要修改,首先看其配置:

我们制定了配置文件的路径,那么在搜索引擎中,我们通过装配一个list,来保存当前系统中需要建立索引的配置信息。

其中涉及到一个配置对象类:

这样我们完成了配置,在后续的索引建立,搜索等过程中,我们将一直以配置来作为依据,比如索引存放位置,索引的名称等等。

看看重构后我们的搜索服务:

 

part3、搜索引擎的实现:

对于多种索引的存在,在搜索的过程中,我们需要指定使用其中的哪一种来进行搜索。索引种类的名称采用一个默认的规则:DO类型的名称,这个对应于拦截器进行设置待更新ID列表的时候,用于指定当前需要更新的列表对应于哪种索引。

 

part4、其他
这个搜索引擎模型,我们使用到了如下的几个核心类:

AbstractBaseDAO:针对于spring的jdbcTemplate封装的API,提供了30多个日常数据库操作常用的API,用于提高开发效率,降低业务代码的耦合性同时提高代码的通用性。前面已经有专门的章节讲述。

AbsBaseDAOMethIntercepDefine:定义了上述的AbstractBaseDAO需要被拦截的方法,被拦截的DAO的接口只要继承于当前的接口,就在DAO子类接口中增加上述AbstractBaseDAO需要拦截的方法声明,以便于拦截器针对于方法内部调用的二次拦截。

AbsBaseDAOMethIntercepted:抽象了通过判断上下文中是否存在代理以选择采用何种方式调用父类方法的接口,如果上下文线程中存在代理,则使用代理去执行,以触发拦截器的拦截,如果不含有,则使用普通的内部调用的方式,这个与直接调用效果相同。

LuceneMethodInterceptor:对AbstractBaseDAO中修改数据库记录的相关方法进行拦截的拦截器,拦截器拦截的是AbstractBaseDAO的子类,但是真正执行拦截过程的,是在子类通过代理去调用父类方法以再次触发的时候。

SearchConfigure:对应于搜索引擎索引xml配置文件的模型。用于保存解析后的结果。

LuceneSearcherService:搜索引擎对外服务接口。

LuceneMultiSearcher:搜索接口实现类,搜索引擎最重要的方法

LocalCacheService:本地缓存,用于缓存部分可延时的信息以减少数据库操作提升性能。比如拦截器拦截的方法执行结果,缓存5分钟以减少5分钟内的重复数据库操作。

RemoteCacheService:远程分布式缓存,memcache本地客户端

 

代码较多,单元测试昝略,请查看后续提交的工程。

---------------------华丽丽的分割

在这过程中,有很多问题是自己没有想到过的,比如如何去确定一个唯一性的key在缓存中,而且这个key还可以便于定位索引,最终确定方案为DO类的名称,但是如何去获取DO类型的名称就成为下一个难题,总之这个过程中,遇到的大部分问题都是自己在前期设计过程中没有想到的,包括后续采用xml配置方式去管理索引种类的问题,多种索引并存的问题,索引建立和搜索过程,都变得十分复杂,将单种类型的索引代码重构的过程中很折磨,这个过程对于我自己而言,是一个修炼的过程,写习惯了业务代码,这种稍微隔离具体业务较低层的代码,在方法签名设计等方面都是一个思维转换的过程,希望这个工作能够继续下去,不管是往下还是往上继续发展,收获,都是在路上……

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值