如果你是做后端开发的,一定已经实现了列表查询的接口。当然有些查询条件很简单,一条SQL就搞定了。
但是有些查询条件极其复杂,库表中不合理的设计使得查询界面特别难写,然后还要加班就不言而喻了(不知道大家有没有这种感觉~)。
先说个例子。这是一个购物网站的搜索条件。如果让你实现这样一个搜索界面,你会怎么做?
当然你说借助搜索引擎,比如Elasticsearch,完全可以实现。但是我在这里想说的是,如果你想自己实现呢?
从上图可以看出,搜索分为六个类别,每个类别又分为各个子类别。
中间每个类别的条件是交集,每个子类别中有单选、多选和自定义的情况,最后输出满足条件的结果集。
好吧,既然需求明确,那就开始实现吧。
实施1
第一个出现的是小啊,他是编写SQL的“专家”。小啊自信地说,“不就是一个查询界面吗?看了很多条件,但以我丰富的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的代码。
3实现2
总结小啊失败的教训,小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自己也知道,每次查询都会多次查询数据库,而由于一些历史原因,有些条件无法进行单表查询,因此查询等待时间是不可避免的。
4实现3
小C从上述方案中看到了优化空间。他发现小B的思路没有问题,将复杂的条件拆分,计算每个子维度的结果集,最后将所有子结果集汇总合并,得到最终想要的结果。
于是他突发奇想,是否可以提前缓存每个子维度的结果集,这就需要在查询时直接获取想要的子集,而不是每次都检查数据库。
这里,C使用Redis存储缓存数据。
这里每个条件都事先将计算好的结果集 ID 存入对应的 Key 中,选用的数据结构是集合(Set)。
查询操作包括:
- 子类单选:直接根据条件 Key,获取对应结果集。
- 子类多选:根据多个条件 Key,进行并集操作,获取对应结果集。
- 最终结果:将获取的所有子类结果集进行交集操作,得到最终结果。
这其实就是所谓的反向索引。这里会发现,漏了一个价格的条件。从需求中可知,价格条件是个区间,并且是无穷举的。
所以上述的这种穷举条件的 Key-Value 方式是做不到的。这里我们采用 Redis 的另一种数据结构进行实现,有序集合(Sorted Set):
将所有商品添加到有序集合中,以键为价格,以值为商品ID,每个值对应的得分就是商品价格的数值。
这样,在Redis的有序集合中,可以通过ZRANGEBYSCORE命令根据score(price)区间得到相应的结果集。
至此,第三种方案的优化已经全部结束,数据的查询和计算通过缓存的方式分离。
在每次搜索中,您只需简单地搜索Redis几次即可获得结果。查询速度符合受理要求。
5扩展
①分页
这里你可能会发现一个严重的功能缺陷,列表查询怎么能不分页呢?是的,让我们看看Redis是如何立即实现分页的。
分页主要涉及排序,在这里,为了简单起见,它基于创建时间。
图中蓝色部分是以创建时间为分数的有序商品集,蓝色下方的结果集是条件计算的结果。通过ZINTERSTORE命令,结果集的权重为0,产品时间结果为1,通过交集获得的结果集被赋予一个新的有序集,该有序集具有创建时间分数。
新结果集的操作可以获得分页所需的所有数据:
总页数是:ZCOUNT命令。
当前页面内容:ZRANGE命令。
按相反顺序:ZREVRANGE命令。
②数据更新
更新索引数据有两种方法。一种是通过修改商品数据立即触发更新操作,另一种是通过定时脚本批量更新。
这里需要注意的是,如果密钥被暴力删除,索引内容将再次更新。
来源:github.com/jasonGeng88/blog