场景如果你是做后端开发的,你肯定实现过列表查询接口。当然,有的查询条件很简单,一条SQL就可以搞定,但有的查询条件却极其复杂。各种不合理的设计,让查询接口变得异常难写,然后加班就更不用说了(不知道大家有没有这种感觉~)。让我们从一个例子开始,这是一个购物网站的搜索条件。如果让你实现这样一个搜索界面,你会如何实现呢?(当然你说借助搜索引擎,比如Elasticsearch就可以实现。但是我这里想说的是,如果你想自己实现怎么办?)从上图可以看出,搜索分为6个类别,每个类别又分为子类别。中间,大类的条件是各个类的交集。每个子类都有单选、多选、自定义的情况,最后输出满足条件的结果集。好了,既然需求明确了,那我们就开始实现吧。关注公众号Java技术栈回复面试,即可获得整理好的Redis系列面试题及全部答案。实现1是第一个同学A,他是写SQL的“专家”。小A自信地说:“不就是一个查询接口吗?有很多条件,但以我丰富的SQL经验,这对我来说并不难。”于是我写了如下代码(这里以MYSQL为例):select...fromtable_1leftjointable_2leftjointable_3leftjoin(select...fromtable_xwhere...)tmp_1...where...orderby...limitm,n代码运行在测试环境,和结果好像是Matched,所以准备pre-release。本次预发布,问题开始暴露。预发布是为了尽可能真实的在线上环境,所以数据量自然要比测试大很多。那么这么复杂的SQL,其执行效率可想而知。测试同学果断敲回小A的代码,实现2总结了A失败的教训,B开始优化SQL。首先,他使用explain关键字来分析SQL性能,对所有有索引的地方都加了索引。同时将一个复杂的SQL拆分成多个SQL,计算结果在程序内存中计算。这篇Explain是最全的总结,推荐阅读。伪代码如下:$result_1=query('select...fromtable_1where...');$result_2=query('select...fromtable_2where...');$result_3=query('select...fromtable_3where...');...$result=array_intersect($result_1,$result_2,$result_3,...);这个方案在性能上明显比第一个要好很多,但是在接受功能的时候,产品经理还是觉得查询速度不够快。MySQL实现了一个简单版本的搜索引擎,请参阅此推荐。小B自己也知道,每次查询都会多次查询数据库,而且有一些历史原因和一些条件不能用于单表查询,所以查询的等待时间在所难免。实现3个小C,从上面的方案可以看出优化空间。他发现小B的思维没有问题。他将复杂的条件进行拆分,计算每个子维度的结果集,最后将所有的子结果集组合起来,得到最终想要的结果。于是他突发奇想,能不能把每个子维度的结果集都提前缓存起来,这样在查询的时候就可以直接取到想要的子集,而不用每次都去查数据库计算。这里小C使用Redis来存储缓存数据。使用它的主要原因是它提供了多种数据结构,并且在Redis中进行集合的交集和并集操作非常容易。具体方案如图所示:这里,这里的每个条件都预先将计算出的结果集ID存储在对应的key中,选择的数据结构是一个集合(Set)。查询操作包括:子类单选:根据条件键直接获取对应的结果集;子类多选:根据多个条件键进行并集运算,得到对应的结果集;最终结果:将要得到的所有子类结果集合相交得到最终结果;这其实就是所谓的反向索引。在这里您会发现缺少价格条件。从需求可以看出,价格条件是一个区间,而且是无穷大。因此,无法实现上述条件穷举的key-value方法。这里我们使用Redis的另一种数据结构来实现,有序集(SortedSet):将所有商品加入到有序集中,Key为价格,value为商品ID,每个value对应的score为该商品的值物价。这样,在Redis的有序集合中,就可以通过ZRANGEBYSCORE命令,根据score(价格)区间得到对应的结果集。至此方案3的优化已经完成,通过缓存的方式实现了数据的查询和计算分离。每次查找时,只需简单地查找Redis几次即可得到结果。查询速度满足验收要求。扩展分页这里你可能发现了一个严重的功能缺陷,列表查询怎么能没有分页呢。对,马上看看Redis是如何实现分页的。关注公众号Java技术栈回复面试,即可获得整理好的Redis系列面试题及全部答案。分页主要涉及排序。为简单起见,以创建时间为例。如图:图中蓝色部分是以创建时间为分值的有序商品集合,蓝色下方的结果集是条件计算的结果。通过ZINTERSTORE命令,结果集的权重为0,商品时间结果为1,取交集得到的结果集赋值给一个新的有序的创建时间分数集。对新结果集的操作可以得到分页需要的所有数据:总页数为:ZCOUNT命令当前页内容:ZRANGE命令如果倒序排列:ZREVRANGE命令数据更新有两种方式更新索引数据。一种是通过商品数据的修改实时触发更新操作,另一种是通过定时脚本进行批量更新。这里需要注意的是,对于索引内容的更新,如果是暴力删除Key,那么就是重置Key。因为Redis中的这两个操作不会原子执行,所以中间可能会有间隙。建议只移除集合中的无效元素并添加新元素。性能优化Redis是内存级别的操作,所以单次查询会很快。但是如果我们的实现会进行多次Redis操作,Redis的多次连接次数可能是不必要的时间消耗。通过使用MULTI命令,启动一个事务,将Redis的多个操作放在一个事务中,最后通过EXEC进行原子执行(注意:这里所谓的事务只是在一个连接中执行多个操作,如果执行失败的话过程中遇到,不会回滚)。总结这只是一个使用Redis优化查询搜索的简单demo。与现有的开源搜索引擎相比,它更轻巧,学习成本也相应更低。其次,它的一些思路和开源搜索引擎有些相似。如果加上分词,还可以实现类似全文搜索的功能。
