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