共计 2513 个字符,预计需要花费 7 分钟才能阅读完成。
场景
大家如果是做后端开发的,想必都实现过列表查问的接口,当然有的查问条件很简略,一条 SQL 就搞定了,但有的查问条件极其简单,再加上库表中设计的各种不合理,导致查问接口特地难写,而后加班什么的就不用说了(不知各位有没有这种感触呢~)。
上面以一个例子开始,这是某购物网站的搜寻条件,如果让你实现这样的一个搜寻接口,你会如何实现?(当然你说借助搜索引擎,像 Elasticsearch 之类的,你齐全能够实现。但我这里想说的是,如果要你本人实现呢?)
从上图中能够看出,搜寻总共分为 6 大类,每大类中又分了各个子类。这两头,各大类条件之间是取的交加,各子类中有单选、多选、以及自定义的状况,最终输入符合条件的后果集。
好了,既然需要很明确了,咱们就开始来实现。
实现 1
率先退场是小 A 同学,他是写 SQL 方面的“专家”。小 A 信念满满的说:“不就是一个查问接口吗?看着条件很多,但凭着我丰盛的 SQL 教训,这点还是难不倒我的。”
于是乎就写出了上面这段代码(这里以 MYSQL 为例):
select ... from table_1
left join table_2
left join table_3
left join (select ... from table_x where ...) tmp_1
...
where ...
order by ...
limit m,n
代码在测试环境跑了一把,后果如同都匹配上了,于是筹备上预发。这一上预发,问题就开始裸露进去。预发为了尽可能的真切线上环境,所以数据量自然而然要比测试大的多。所以这么一个简单的 SQL,它的执行效率可想而知。测试同学果决把小 A 的代码给打了回来。
实现 2
总结了小 A 失败的教训,小 B 开始对 SQL 进行了优化,先是通过了 explain 关键字进行 SQL 性能剖析,对该加索引的中央都加上了索引。同时将一条简单 SQL 拆分成了多条 SQL,计算结果在程序内存中进行计算。
伪代码如下:
$result_1 = query('select ... from table_1 where ...');
$result_2 = query('select ... from table_2 where ...');
$result_3 = query('select ... from table_3 where ...');
...
$result = array_intersect($result_1, $result_2, $result_3, ...);
这种计划从性能上显著比第一种要好很多,可是在性能验收的时候,产品经理还是感觉查问速度不够快。小 B 本人也晓得,每次查问都会向数据库查问屡次,而且有些历史起因,局部条件是做不到单表查问的,所以查问期待的工夫是防止不了的。
实现 3
小 C 从下面的计划中看到了优化的空间。他发现小 B 在思路上是没问题的,将简单条件拆分,计算各个子维度的后果集,最初将所有的子后果集进行一个汇总合并,失去最终想要的后果。
于是他突发奇想,是否当时将各个子维度的后果集给缓存起来,这要查问的时候间接去取想要的子集,而不必每次去查库计算。
这里小 C 采纳 Redis 来存储缓存数据,用它的次要起因是,它提供了多种数据结构,并且在 Redis 中进行汇合的交并集操作是一件很容易的事件。
具体计划,如图所示:
这里每个条件都当时将计算好的后果集 ID 存入对应的 key 中,选用的数据结构是汇合(Set)。查问操作包含:
- 子类单选:间接依据条件 key,获取对应后果集;
- 子类多选:依据多个条件 Key,进行并集操作,获取对应后果集;
- 最终后果:将获取的所有子类后果集进行交加操作,失去最终后果;
这其实就是所谓的反向索引。
这里会发现,漏了一个价格的条件。从需要中可知,价格条件是个区间,并且是无穷举的。所以上述的这种穷举条件的 Key-Value 形式是做不到的。这里咱们采纳 Redis 的另一种数据结构进行实现,有序汇合(Sorted Set):
将所有商品退出 Key 为价格的有序汇合中,值为商品 ID,每个值对应的分数为商品价格的数值。这样在 Redis 的有序汇合中就能够通过 ZRANGEBYSCORE 命令,依据分数(价格)区间,获取相应后果集。
至此,计划三的优化已全副完结,将数据的查问与计算通过缓存的伎俩,进行了拆散。在每次查找时,只须要简略的查找 Redis 几次就能得出后果。查问速度上合乎了验收的要求。
扩大
分页
这里你或者发现了一个重大的性能缺点,列表查问怎么能没有分页。是的,咱们马上来看 Redis 是如何实现分页的。
分页次要波及排序,这里简略起见,就以创立工夫为例。
如图所示:
图中蓝色局部是以创立工夫为分值的商品有序汇合,蓝色下方的后果集即为条件计算而得的后果,通过 ZINTERSTORE 命令,赋后果集权重为 0,商品工夫后果为 1,取交加而得的后果集赋予创立工夫分值的新有序汇合。对新后果集的操作即能失去分页所需的各个数据:
- 页面总数为:ZCOUNT 命令
- 当前页内容:ZRANGE 命令
- 若以倒序排列:ZREVRANGE 命令
数据更新
对于索引数据更新的问题,有两种形式来进行。一种是通过商品数据的批改,来即时触发更新操作,一种是通过定时脚本来进行批量更新。这里要留神的是,对于索引内容的更新,如果暴力的删除 Key,再从新设置 Key。因为 Redis 中两个操作不会是原子性进行的,所以两头可能存在空白间隙,倡议采纳仅移除汇合中生效元素,增加新元素的形式进行。
性能优化
Redis 是内存级操作,所以单次的查问会很快。然而如果咱们的实现中会进行屡次的 Redis 操作,Redis 的屡次连接时间可能是不必要工夫耗费。通过应用 MULTI 命令,开启一个事务,将 Redis 的屡次操作放在一个事务中,最初通过 EXEC 来进行原子性执行(留神:这里所谓的事务,只是将多个操作在一次连贯中执行,如果执行过程中遇到失败,是不会回滚的)。
总结
这里只是一个采纳 Redis 优化查问搜寻的一个简略 Demo,和现有的开源搜索引擎相比,它更轻量,学习老本页相应低些。其次,它的一些思维与开源搜索引擎是相似的,如果再加上词语解析,也能够实现相似全文检索的性能。
起源:github.com/jasonGeng88/blog/blob/master/201706/redis-search.md