作者:jasonGeng88

出处:https://github.com/jasonGeng88/blog

大家如果是做后端开发的,想必都实现过列表查询的接口,当然有的查询条件很简单,一条 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,和现有的开源搜索引擎相比,它更轻量,学习成本页相应低些。

其次,它的一些思想与开源搜索引擎是类似的,如果再加上词语解析,也可以实现类似全文检索的功能。

特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:

长按订阅更多精彩▼如有收获,点个在看,诚挚感谢

我用Redis实现了一个轻量级的搜索引擎!相关推荐

  1. 使用 Redis 实现一个轻量级的搜索引擎,牛逼啊 !

    来源 | http://github.com/jasonGeng88/blog/blob/master/201706/redis-search.md 场景 大家如果是做后端开发的,想必都实现过列表查询 ...

  2. 使用 Redis 实现一个轻量级的搜索引擎

    今日推荐 后端接口如何提高性能? 16 个写代码的好习惯 为什么不推荐使用BeanUtils属性转换工具 盘点阿里巴巴 34 个牛逼 GitHub 项目 常见代码重构技巧(非常实用) 作者:jason ...

  3. mysql 队列存储_GitHub - hongliangbest/QueueTask: 一个轻量级可拓展的队列任务、暂时支持mysql、redis等存储方式...

    QueueTask 一个轻量级可拓展的队列任务.暂时支持mysql.redis等存储方式 目录结构 |--Config/ 配置文件目录 |--Config.php 配置文件 |--Connection ...

  4. hiredis(一个轻量级redis的c客户端)

    1.简单介绍 hiredis是一个轻量级的访问redis数据库的c客户端. 它是轻量级的不仅仅是因为它仅仅提供对协议的最小支持,而且它使用了一个高级别的极度类似于printf的api使它的级别远高于其 ...

  5. php快速开发框架津县,BetePHP:一个轻量级快速开发框架

    BetePHP 关于 BetePHP是一个轻量级快速开发框架.框架提供了一致的API接口,使得使用接口尽可能流畅:由于框架本身不依赖其他库,这使得框架本身运行速度非常快:框架希望使用最简单的方式mak ...

  6. Task一个轻量级分布式任务计算系统

    Task系统设计与使用 Task是一个轻量级的分布式任务计算系统,他可以帮助你快速编写一个可以在集群环境下运行的分布式方法,而这只需要你使用一行代码就可以在你原有的方法上做到. 一个简单例子: pub ...

  7. libgo高性能网络服务器,【开源】gnet: 一个轻量级且高性能的 Golang 网络库

    ![](https://ask.qcloudimg.com/http-save/1303222/sipe2g9n9h.png) # Github 主页 [https://github.com/panj ...

  8. 【开源推荐】gnet: 一个轻量级且高性能的 Go 网络库

    Github 主页 https://github.com/panjf2000/gnet 欢迎大家围观~~,目前还在持续更新,感兴趣的话可以 star 一下暗中观察哦. 简介 gnet 是一个基于事件驱 ...

  9. 组件分享之后端组件——Redis数据库的一个Go客户端redigo

    组件分享之后端组件--Redis数据库的一个Go客户端redigo 背景 近期正在探索前端.后端.系统端各类常用组件与工具,对其一些常见的组件进行再次整理一下,形成标准化组件专题,后续该专题将包含各类 ...

最新文章

  1. mysql libs 冲突_mysql-libs的冲突
  2. SHELL中的三神器:管道、重定向、backtick
  3. 一本算法刷题必读配套书(附链接)
  4. Xilinx IP核专题之PLL IP核介绍(Spartan-6)
  5. php记录用户搜索历史记录,PHPCookei记录用户历史浏览信息的代码
  6. Centos7.6安装redis
  7. Linux常用实用命令
  8. origin设置不同区域的颜色_[测试狗]Origin入门教程(二十四):效率翻倍小技巧——修改默认字体...
  9. linux先安装svn server
  10. Testing - Codereview Checklist
  11. python赋值运算符难理解_零基础学 Python(8)运算符 — 算术、比较、赋值、逻辑...
  12. 关于事件模型,js事件绑定和解除的学习
  13. Open***+ldap配置过程
  14. Adapter 模式
  15. matlab遗传算法 系统辨识,基于遗传算法的控制系统的系统辨识
  16. vrep小车寻迹代码
  17. 函数型计算机有储存功能吗,无存储功能的计算器指的是什么
  18. 佛祖,你为什么不帮我
  19. Python 每日一记217根据词频生成词云图
  20. IP反查网站,ip反查接口,旁站查询接口大全,通过IP查域名汇总:

热门文章

  1. JSP 客户端请求概述
  2. 用 Qt 给嵌入式Linux加个桌面
  3. python selenium 浏览器设置_python+selenium webdriver.firefox()方式配置浏览器设置
  4. python实现socket编程,服务端开启多线程(和多个客户端聊天)
  5. 关于学习Python的一点学习总结(6->元组)
  6. 习题10-6 递归求Fabonacci数列 (10 分)
  7. PTA基础编程题目集-6-6 求单链表结点的阶乘和
  8. python页面调用接口_python调用接口——requests模块
  9. 【学习笔记】超简单的多项式牛顿迭代(含泰勒展开式、牛顿迭代全套证明)
  10. CF660C Hard Process(尺取法)