架构之坑系列2:高并发系统设计与时间和空间的平衡

吴坚 2016-06-19 09:35:00
高可用上文我们已经讲过了,可当前互联网时代,怎么少的了高并发呢?高并发和高可用一样, 已经变成各个系统的标配了,如果你的系统QPS没有个大几千上万,都不好意思跟人打招呼,虽然可能每天的调用量不超过100。

 

高并发这个词,我个人感觉是从电商领域开始往外流传的,特别是电商领域双11那种藐视全球的流量,再把技术架构出来分享一把,现在搞得全互联网都在说高并发,而且你注意回忆一下所有你看到的高并发系统,往往都逃不开一个核心概念,那就是缓存+哈希,一切都是以这个概念和基础的,仿佛这就是高并发的核心技术了。

 

缓存+哈希=高并发?
 

 

(一)我们看到的高并发技术

 

围绕这个核心技术,通常我们看到的各种高并发的架构系统,在博客,论坛,现场分享出来的高并发系统,都跑不出以下几个方面的东西。

 

1
 
资源静态化

 

就是那种单个页面流量巨大无比,每秒的QPS几十万上百万的系统,确实并发高的系统,核心解决方案就是静态化,靠机器和带宽去抗,假如没有CDN的话,所有流量都落到同一个IP下面的话,基本上也就是用Nginx的文件静态化了,单机的承受能力主要取决于带宽和单机的性能,要再多的话,那就LVS(或者F5)+集群了,这种的典型场景就是搞活动时候的首页,活动页面了,还有就是引流搜索引擎的着陆页了,一般都是现成的图片和文字静态化,当然,这种还有很多前端的技巧和技术了,这一点我不是很了解,就不得瑟了,就中后台来说,大部分情况下直接Nginx搞定了,核心还是使用了缓存技术

 

2
 
读写分离和分库分表

 

读写分离是大家看到的第二个高并发的架构了,也很常规,因为一般情况下读比写要多得多,所以数据库的主库写,从库们提供读操作,一下就把数据库的并发性能提高了。

 

如果还不够,那么分库分表把,把数据分到各个数据库的各个机器上,进一步的减少单台机器的压力,从而达到高并发的目的。

 

如果是分库分表,有时候使用的就是哈希技术了,以某个字段哈希一下然后来分库分表,读写分离的读操作,基本也是通过哈希技术把读落到不同的机器上去减轻单机压力。

 

3
 
万能的缓存

 

说到高并发,不得不说缓存了,现在各种缓存的工具也很多也很成熟,memcache,redis之类的KV数据库作为缓存已经非常成熟了,而且基本上都可以集群化部署,操作起来也很简单,简直变成了一个高并发的代言词了,核心就是缓存技术了,而memcache和redis只是用来实现这个缓存技术的工具而已。

 

4
 
无敌的哈希

 

但凡大数据处理,高并发系统,必言哈希,随机插入,时间复杂度O(1),随便查询,时间复杂度O(1),除了耗费点空间以外,几乎没什么缺点了,在现在这个内存廉价的时代,哈希表变成了一个高并发系统的标配。

 

5
 
正面的例子

 

我们来看个例子,看看一些个大家眼中标准的高并发系统的设计,这些设计大家应该都看过,无非就是上面的几个要点,最重要的就是缓存+哈希,这两个东西的组合好像无所不能。

 

 
 
 
活动秒杀页面

 

活动秒杀页面,这是个标准的高并发吧,到了搞活动的那个时刻,单页面的访问量是天量数据了,但这种系统有个特点:逻辑简单,只要带宽和性能够,就一定能提供稳定的服务服务。

 

能迅速的返回数据即可,没有什么计算逻辑,这种高并发系统的设计基本上就是在怎么压榨机器的IO性能了,如果有CDN绝对使用CDN,能在本机读取的绝不走网络获取,能读取到内存中绝不放在硬盘上,把系统的磁盘IO和网络IO都尽可能的压榨,使用缓存+哈希技术,只要设计合理,99%的情况能搞定。活动页面的冲击感实在太强,想象一下几千万人同时访问网站还没有挂,让很多人觉得高并发应该就是这样子,这估计也是高并发这次经常在电商技术中出现的原因吧,因为搞个活动就可以搞出一个高并发事件。

 

这样的场景再扩展一点,就是凡是能提前提供数据的并发访问,就可以用缓存+哈希来搞定并发。

 

 
 
 
12306

 

接下来,我们再看看这个星球并发量最疯狂的网站,瞬间的访问量碾压其他网站的12306,这种场景下的高并发也有个特点,那就是虽然量大,但其实无法给每个用户提供服务。

 

类似的其实还有商品的抢购系统,商品和车票一共就1000张,你100万的人抢,你系统做得再好,也无法给100万人提供服务,之前12306刚刚上线的时候很多人喷,说如果让某某公司来做肯定能做好,但大家很多只看到了表面,让某很厉害的公司来做,最多也只能做到大家访问的时候不会挂掉,你买不到车票还是买不到,而且现在的12306体验也已经做得很好了,也不卡了,但是还是很多人骂,为什么?还不是因为买不到票。

 

对于这样的系统,设计关注的就不仅仅是提高性能了,因为性能瓶颈已经摆在那了,就1000张票,做得更多的是分流和限流了,除了缓存+哈希来保证用户体验以外,出现了奇葩验证码,各个站点分时间点放票。然后通过排队系统来以一种异步的方式提供最终的服务。

 

我们给这样的场景再扩展一下,凡是不能提前提供数据的,可以通过缓存+哈希来提高用户体验,然后通过异步方式来提供服务。

 

(二)高并发系统如何设计

 

如果把上面两个场景的情况合并一下,仿佛缓存+哈希变成万能的了,很多人眼中的高并发就是上面的场景的组合,认为缓存+哈希就可以解决高并发的问题,其他的场景中,加上缓存提高读写速度,在加上哈希提供分流技术,再通过一个异步提供最终服务,高并发就这么搞定了,但实际上是不是这样呢?显然没那么简单,那如何来设计一个高并发的系统呢?

 

1
 
合理的数据结构

 

举个例子来说吧,搜索提示功能大家都知道吧,就是下面这个图的东西。

 

 

如果是google,baidu这种大型搜索系统,或者京东淘宝这种电商系统,搜索提示的调用量是搜索服务本身调用量的几倍,因为你每输入一个键盘,就要调用一次搜索提示服务,这算得上是个标准的高并发系统吧?那么它是怎么实现的呢?

 

可能很多人脑子里立刻出现了缓存+哈希的系统,把搜索的搜索提示词存在redis集群中,每次来了请求直接redis集群中查找key,然后返回相应的value值就行了,完美解决,虽然耗费点内存,但是空间换时间嘛,也能接受,这么做行不行?恩,我觉得是可以的,但有人这么做吗?没有。

 

了解的人应该知道,没有人会这么来实现,这种搜索提示的功能一般用trie树来做,耗费的内存不多,查找速度为O(k),其中k为字符串的长度,虽然看上去没有哈希表的O(1)好,但是少了网络开销,节约了很多内存,并且实际查找时间还要不比缓存+哈希慢多少,一种合适当前场景的核心数据结构才是高并发系统的关键,缓存+哈希如果也看成一种数据结构,但这种数据结构并不适用于所有的高并发场景,所以高并发系统的设计,关键在合理的数据结构的设计,而不在架构的套用。

 

2
 
不断的代码性能优化

 

有了上面的数据结构,并且设计出了系统了,拿到线上一跑,效果还行,但感觉没达到极限,这时候可千万不能就直接上外部工具(比如缓存)提升性能,需要做的是不断的代码性能的优化,简单的说,就是不断的review你的代码,不断的找出可以优化的性能点,然后进行优化,因为之前设计的时候就已经通过理论大概能算出来这个系统的并发量了,比如上面那个搜索提示,如果我们假定平均每个搜索词6个字符,检索一次大约需要查询6次,需要2-3毫秒,这样的话,如果8核的机器,多线程编程方式,一秒钟最多能接受3200次请求(1000ms/2.5ms*8),如果没达到这个量级,那么肯定是代码哪里有问题。

 

这个阶段可能需要借助一些个工具了,JAVA有JAVA的性能优化工具,大家都有自己觉得好使的,我本身JAVA用得很少,也没啥可推荐的,如果是Golang的话,自带的go tool pprof就能很好的进行性能优化。

 

或者最简单的,就是把各个模块的时间打印出来,压测一遍,看看哪个模块耗时,然后再去仔细review那个模块的代码,进行算法和数据结构的优化,我个人比较推崇这个办法,虽然比较笨,但是比较实在,性能差就是差,比较直观能看出来,也能看出需要优化的点,而且比较贴近代码,少了外部工具的干扰,可能也比较装逼吧。

 

这个过程是一个长期的过程,也是《重构:改善代码的既有设计》中提到的,一个优秀的系统需要不断的进行代码级别的优化和重构,所以高并发系统的实现,就是不断的优化你代码的性能,不断逼近你设计时的理论值。

 

3
 
再考虑外部通用方法

 

以上两个都完成了,并发量也基本达到理论值了,但是还有提升的需求,这时候再来考虑外部的通用方法,比如加一个LRU缓存,把热词的查询时间变成O(1),进一步提高性能。

 

说起LRU,多说一句,这是个标准的缓存技术了,实现起来代码也不复杂,就是个哈希表+链表的数据结构,一个合格的开发人员,即便没有听说过,给定一个场景,应该也能自己设计出来,我见过很多简历都说自己有大型高并发系统的开发经验,能熟练运用各种缓存技术,也对缓存技术有深入的了解,但是一面试的时候我让他写个LRU,首先有50%的人没听说过,OK,没听过没关系,我描述一下,然后给一个场景,硬盘上有N条数据,并且有一个程序包,提供GET和SET方法,可以操作磁盘读写数据,但是速度太慢,请设计一个内存中的数据结构,也提供GET和SET方法,保存最近访问的前100条数据,这个数据结构就是一个LRU了,让面试者实现出来,如果觉得写代码麻烦,可以把数据结构设计出来描述一下就行了,就这样,还很多人不会,这怎么能说是对缓存技术有深入了解呢?就这样,怎么能说有过大型高并发系统的经验呢?这只是开源工具的使用经验罢了。

 

在没把系统的性能压榨完全之前,不要使用外部的通用方法,因为使用了以后就没有太多进一步优化空间了。

 

4
 
最后靠运维技术了

 

上面几种都已经弄完了,还需要提升性能,这时候再考虑运维的技术了,比如常规的加负载均衡,部署成集群之类的,通过运维和部署的方法提高服务的并发性。

 

高并发系统只是相对的,没有什么无上限的高并发,流量的洪流来了,再高的高并发一样挂,新浪微博的高并发应该做得很好吧?但是林心如发条微博说她和霍建华谈恋爱了,一样把微博搞挂了(非官方消息啊,我猜测的,呵呵,那天下午新浪微博正好挂了),呵呵,你说要是TFBOY明天过生日,微博是不是要连夜加几个redis集群啊?如果有微博的朋友,留个言溜溜呗:)

 

3、总结

 

罗里吧嗦说了这么多,其实我就想表达一个意思,不管是前面的高可用,还是今天的高并发。

 

代码才是关键,架构都是锦上添花的东西,既然是锦上添花的,必然坑多,没有什么捷径。

 

代码的健壮性决定了高可用,这些印度人就能做到,而高性能,高并发需要的不仅仅是代码的健壮性,还有数据结构的设计和代码的调优能力了。架构模式是大家总结出来的,和你的系统可能关系不是很大,学习太多的架构,脑袋会乱,还不如实打实的看几本书,然后对着架构多推敲练习,很多人对数据结构嗤之以鼻,觉得对于现有的开发来说,数据结构没那么重要了,但对于后端开发来说,数据结构是很重要的技能,虽然面试的时候不会让你去翻转一棵二叉树,但二叉树是什么,什么场景下用还是应该知道的吧?

 

找准合适的数据结构,不断的优化代码,这样来提升你的系统性能,这样的系统才是你可控的,才有不断优化的空间,更好的高并发,如果一开始就上外部的缓存技术,很可能如果性能达不到要求,就没有优化空间了,因为要修改外部的系统还是很困难的。

 

时间和空间的平衡
 

 

最后我们来说说架构中时间和空间的平衡吧,这里的时间指代比较广,可能是开发时间,但大部分指的是执行时间,也就是算法的时间复杂度了,而空间就是算法中经常说的空间换时间中的空间了,一个好的系统,设计出来必然是各种时间复杂度和空间复杂度平衡出来的结果,架构设计的过程,并不仅仅是模块的堆叠,在走到岔路口的时候,更多的是时间和空间平衡之后选的一个技术方案,这一篇,我会用一个搜索提示服务设计的实际例子,来说一下架构设计的过程中,时间和空间的各种矛盾,怎么分析,怎么选择,最后淌过这些时空的坑。

 

1. 搜索提示是什么

 

搜索提示是搜索引擎的重要组成部分,虽然一般是作为一个单独的服务来对外提供服务,但在一个搜索系统中,搜索提示是非常重要的组成部分,我还没看到哪个比较成熟的搜索引擎没有搜索提示功能的。首先,我们看看搜索提示是什么,大家肯定都用过,就是下面这些个东西:

 

 

2. 搜索提示的场景和目的

 

搜索提示一般情况下是为了提高用户的搜索体验,更快的选择合适的搜索词,提高检索的效率的,但是因为搜索框的流量实在是太大了,所以搜索提示也扮演着广告变现的责任,互联网嘛,有流量就有变现,比如下面这个图,明显就是一个广告啦。


 

3. 初步技术选型

 

1
 
搜索提示的需求

 

要实现一个搜索提示系统,首先需要确定的是需要提示出来什么东西,有两种提示方式。

 

  • 一种是提示出其他的搜索词,这也是大部分的搜索提示所做的,提示出其他用户的类似搜索词。

  • 还有一种是提示出现有的结果集有的东西,这种实现方式比较少见,比如一个生鲜类的电商网站,商品数量比较少,那么没必要去提示一些用户的搜索词,直接把商品名称(比如苹果,桃子,橘子)提示出来就行了,这种提示方式我们这里不讨论,因为实现起来比较简单。

 

2
 
技术栈

 

既然知道需求了,那么开始选择技术栈了。

 

  • 首先,既然有其他用户的搜索词,那么必然有一个离线的数据收集和处理的系统来完成其他用户的搜索日志处理,生成需要的数据。

  • 其次,需要一个单独的API服务,来提供搜索提示的功能,输入为不完整的搜索词,输出为根据这个搜索词提示出来的其他搜索词,检索方式的话,一般都是使用前缀匹配的方式了,这个大家都比较认可。

  • 最后,需要前端有个js代码来实时调用后台的API,这个不在我们的讨论范围内。

 

整个系统的结构图应该是下面这个样子,离线模块处理完日志数据以后,推送到API模块中,给前面的前端提供服务。

 

 

好了,框框设计好了。也就是架构图完成了哦,真是牛逼的架构啊,三个框,离线,在线,前端全齐了。接下来,我们来看看在线API部分的设计吧,我们先假设离线数据都已经准备好了,就是一堆用户的搜索词,如何快速的前缀匹配这些词就成了API设计部分的关键了,有这么几种实现方式。

 

  • 粗暴的短平快方式

 

用redis保存所有信息,每条信息类似

 

{KEY:北 VALUE:北京,北京大学,北大,北京遇上西雅图}

{KEY:北京 VALUE:北京,北京大学,北京遇上西雅图}....

 

每次来了请求的话,直接查询redis给出结果返回,就是占点空间,最好还需要一台单独的服务器

 

  • 优雅点的实现方式

 

前缀匹配嘛,最先想到的数据结构就是Trie树了,所以所有的Key可以用Trie树来保存和检索,速度也挺快的,而且空间占用比较少。

 

  • 复杂点的实现方式

既然是检索嘛,就直接用搜索引擎的倒排索引技术来实现嘛,速度也够,而且数据量也可以支持得很大。

 

4. 时间与空间的平衡一

 

实际工程应用中,这三种实现方式我都见过,而且有些实现方式是把这三种结合起来使用了,后面的文章我会说到。具体使用哪一种需要看你的实际场景,这三种实现方式差不多正好对应三种场景。

 

  • 如果你是个小型的电商或者论坛之类的,每天的搜索量也不是很大,而且在可见的未来也不会变得很大,而且也不差钱,那么直接第一种,说不定一天就能撸出来,速度还不错,但是这种有一些缺陷,首先,value值不能太复杂,影响效率,所以可扩展性不是很强,而现在的电商搜索提示中往往还有很多其他信息需要保存,redis作为缓存服务器提供高并发服务的前提是数据量比较小,最好在2K以内,这样的话用redis就有点不合适了。这种方案是个存空间的选择了,用空间换取了检索时间和开发时间,多亏有redis这种神器。

 

  • 如果是个大型的搜索引擎或者电商,搜索日志已经是巨量了,而且搜索词多种多样,那么第三种倒排索引技术为基础的实现方式可能是更好的选择,而且既然是大搜,技术都是现成的,索引分片,集群都是现成的,直接改了上就是。这种方式用长期的开发时间和检索速度上稍微的降低换取了内存空间,如果从头开始做的话,时间成本比较高。

 

  • 大部分时候,第二种实现方式是大家都采用的方式,首先没有第一种那么粗暴,并且能完成方案一的所以功能,单机就能达到较好的效果,也不用索引分片,也不用集群,所以工程复杂性不是很高,也能在较短的时间内实现出来。其次第二种方案可扩展性较强,后面挂个倒排文件就可以变成简化版的第三方案。这种方式用算法换取了内存空间,用O(n)替代了O(1),换取了内存空间,也是标准的计算机领域的时间换空间了。

 

通过一番分析下来,决定使用第二种实现方式,就是Trie树的方式了,好了,API的基本选型确定了,那么开始设计,准备写代码吧。

 

5. Trie树的多种结构

 

既然确定了Trie树的实现方式,那么首先要了解一下Trie树吧,以及Trie树的各种结构,看看具体用哪个吧。

 

1
 
基本Trie树

 

Trie树又叫字典树,本质上是一个多叉树,每一个节点就是一个多叉的结构,如果是英文的匹配,那么是一个26叉树,每个节点一个26长度的数组,每个节点的数据结构如下:

 

 

而Trie树画出来就是下面这个样子。


 

从画出来的图,很直观的可以看出来这棵树的构造方法和遍历方法,如果是纯英文的话,每个节点都有一个26长度的数组,来了一个字符,通过字符的编号直接就可以遍历到下一个节点,查找的时候复杂度就是O(K),K表示查找的字符串长度,这种数据结构简单明了,实现起来也很容易。

 

2
 
优化后的Trie树

 

基本Trie树的数据结构有个问题,就是内存使用得太多了,如果是中文查找的话,需要把所有的中国字都编号到这个数组中,内存就爆了,于是有一种优化方法,就是把数组变成变长的,这种Trie树的节点数据结构变成下面的样子了,节点查找变成一个顺序查找或者二分查找了。

 

 

3
 
双数组Trie树

 

所谓双数组Trie树,当然就是通过两个数组来实现这棵树了,这两个数组分别叫base数组和check数组,一个是基础数组,一个是检查数组。

 

Trie树实际上是一种有限状态机,通过状态转移矩阵在各个状态之间跳转,双数组Trie树极大的节省了空间,大致就是下面这个样子,我后面会有一篇专门的文章来说Trie树实现的,这里就不详细展开了,实在等不及的可以自己先搜索一下相关资料看看双数组Trie树吧。

 

6. 时间与空间的平衡二

 

OK,三种Trie树的实现方式都说了,现在要开始抉择了,我们先看看这三种数据结构的时间和空间。

 

  • 第一种空间占用大,特别是中文的情况,检索的时间效率为O(n),其中n为每次请求的字符串的长度,这种实现方式基本上属于新人练手的水平,纯粹为了了解这个数据结构或者大学生做做课程设计,工程化的可能性几乎为0。

  • 第二种空间基本不浪费,但检索的时间效率如果按照二分进行每个节点的查找的话,每个节点的查找时间变成了O(lg(n)),整体的查找时间变成K*O(log(n)),同样插入效率也变低了。

  • 第三种情况空间不浪费,时间效率也为O(n)。

 

初看,肯定选第三种了,但是!!第三种实现方式有个致命的缺陷,就是无法向下遍历(具体可以自己看看双数组的实现方式),也就是说我输入北京,找不到北京大学,北京爱上西雅图,因为它已经不是一个树型结构了,无法向下遍历了。所以如果不对第三种结构进行改造的话,是无法满足我们的功能的。要改造,最简单的办法就是在每个词后面挂一个链表,表示这个词的后继词都是什么,像下图这样。

 

 

如果按上图那么来的话,需要辅助的空间来存储后继词,那么问题又来了,又是一次时间和空间的抉择了,是选择K*O(log(n))的第二种方案,然后后继词实时遍历树来获取(又要耗费一定的时间),还是选择选择第三种方案,用空间换取时间呢?

 

好,既然这样,我们来仔细算算这个账,我们以每个节点都存一个中文来算,虽然常用的汉字大概2500个,但其中最常用的才500左右。先看第二种方案,那么我们大概估算出,每个节点的平均数组长度大概600(实际上除了第一层的节点,后面的节点数组长度完全达不到这个量级,用600属于极限估算了),600的二分查找大约需要7到8次,取个平均值4次,那么每次查询的时间就是4*K(K是字符串的长度),如果我们定好最长的提示词不超过8个字(太长也没意义),那么首先这个树的高度就是8了,如果50万的词量的话,使用多少内存大概能算出来,然后每次遍历下级节点的时间就是600^(8-K)(如果数组的每个元素都有值),我去,这么大,吓死了,好,我们即便假设每个节点的数组长度平均为60,要遍历完也要60^(8-K),也吓尿了,所以实时遍历所有子节点的方式不可取,而且后继词最多也就提示出10个,遍历出这么多词还要排序,遍历全部节点实在是没有必要,所以,第二种方案要么放弃,要么也要改造,如何改造呢?

 

因为词基本上都是离线算好的,稍微把节点的数据结构优化一下,在节点中加一个字段,表示哪个子节点有需要的数据(排序前10的词),这样往下遍历的时候就直接遍历相应的下标就可以了,就能把60^(8-K)这种遍历减少到几十次,从而找到10个提示词,我们把这个结构叫二次优化的Trie树

 

这一轮的时间和空间的比拼,第三个方案感觉就要胜利了,但第二个方案的优化版貌似也还能接受,一个耗费空间,查询速度快,一个节省空间,查询速度慢点。

 

这里多说一下,其实上面只是预估的办法比较搓,这么写是为了说预估的技能,最直接的就是拿着日志统计一遍,得到一堆不超过8位长度的搜索词,同时也能算法两个方案的内存使用规模和大概的查找效率,这样的预估办法最准确,但是在大部分时候我们并没有这么多数据,所以只能做一些基本的预估。

 

7. 离线数据处理

 

好了,我们先把检索部分放一放,来看看离线数据处理部分吧。我们先要确定一下什么东西需要在离线部分算好,什么东西需要在线处理?

 

  • 首先,日志的清洗肯定是离线部分了,我们先要把没有搜索结果的词去掉,然后去掉太长的词(假定超过8的都不要),然后保留有一定热度的词(比如每天搜索量超过10次的词),等等一些规则以后,假如剩下了50万的词,那这50万就是我们的基础数据了。

     

  • 其次,Trie树的构建是离线构建好还是实时往服务推送由服务端去构建呢?

     

  • 还有,排序的时候是离线给每个搜索词打个分,然后实时排序呢?还是离线把序都排好,服务端直接使用结果呢?

 

8. 时间与空间的平衡三

 

虽然是离线处理,但一样有时间和空间的选择。我们先来看构建部分,Trie树的构建是离线构建好还是实时往服务推送由服务端去构建,首先我们需要确定的是这个搜索提示服务需不需要实时更新,一般情况下,搜索提示没有那么强的实时性要求,一般一天或者两天更新一次体验也不会太差,所以做实时更新的搜索提示,要不就是你实在是太蛋疼了,要不就是遇到了一个特别让人蛋疼的产品经理(卧槽,黑了一下产品经理啊)。所以我们使用离线构建的方式构建好两个数组和辅助的数据结构,都存在磁盘上,服务端启动的时候读取文件就行了,这是用离线时间换取的服务端的时间,是很划得来的。

 

再来看看排序的部分,很明显,排序离线做好也比较合适,排序的位置基本不会有太大的变化,但是如果排序离线做好的话,那么辅助的数据结构就会比较大了,因为每个前缀后面跟着的10个词都要排好序放在辅助结构中,但如果我们只是把每个词打个分(比如就按热度给个分),然后用第二个方案(优化的Trie树)的存储方式,在线的时候去排序,那么辅助结构就会小很多,两种情况的结构大概就是下面这样的区别。

 

 

左边的是全排序好了的,直接使用,双数组Trie树+辅助结构方式;右边的是只是打了分的,优化的Trie树,遍历出结果以后实时排序的。离线排序的空间占用大,即便优化一下,把词都放一个地方单独存着,辅助结构中只保存词的编号,一样也比较占地方,但是查询速度快啊。在线排序的方式不怎么占地方,就是每个节点多了一个分数的字段,需要实时排序一下,虽然是实时排序,但个数就10个,不管是快排还是堆排,都很快的,所以时间效率也慢不到哪去。

 

9. 整体的时空平衡

 

综合衡量一看,我个人觉得两种方式都能接受,具体选哪一个就仁者见仁了。

 

  • 如果搜索词的量比较稳定,不会有太大的变化,那么使用双数组Trie树+辅助数据结构+离线构建Trie树+离线排序的方式更合适。

     

  • 如果搜索词虽然现在是50万,但很可能会增加得比较多,或者像下图一样,搜索提示的页面还会承载很多其他的数据的话,那么使用二次优化的Trie树+离线构建Trie树+离线打分+实时排序的实现方式更合适,因为能节省更多的内存给后续扩充词语用或者给其他数据用。

     

  • 还有如果对速度要求苛刻,那么就第一种,如果没那么苛刻,那就第二种。

 

架构设计没有好坏,只有合适不合适。

 

10. 总结

 

上面分析了这么一大堆,淌过三个的时间与空间的坑,终于基本确定了技术方案了,这其实也是系统架构设计中经常会要遇到的选择了,架构师们把这些选择做完以后,可以开始细分模块设计开发了,所以,一个小小的系统就这么多选择,各种空间和时间的平衡,你说架构师哪那么好当?呵呵,你以为就画完这篇文章的第一图就架构结束了啊。

 

这里只是用搜索提示作为一个例子来说明系统设计的时候需要时时刻刻关注时间和空间这两个因素的平衡,现在很多人设计系统的时候基本上不太关注时间,因为高配的服务器,几十上百GB的内存随便用,所以大多数都把设计往空间上去靠,用更多的空间来换取执行效率,这本身并没有什么问题,谁不希望更快啊,但是有时候预估一下,有可能虽然牺牲了一点时间效率,但是换来了不少的空间,这样的系统在数据量变大时有更多的可扩展空间,我觉得是非常值得的交换。

 

再有,对数据结构和算法的了解以及预估算能力其实是平衡时间和空间的重要技能,也是架构设计中避坑的基本技能,所以有公司的面试题会出现请你估算一下黄河出海口的面积这类估算题,因为预估算能力也是重要的架构技能吧。

 

11. 更深入一下

 

上面只是这个系统的一小部分,搜索提示需要做的远不止如此,想想下面几个场景,如果是你,你要如何设计呢?如何平衡时间和空间呢?欢迎讨论哈。

 

  • 需要拼音支持,就像这样

 

 

  • 需要拼音首字母支持

 

 

  • 某些搜索提示需要更加详细的信息

 

 

  • 需要对每个用户的搜索历史进行搜索提示【这个比较难点】

 

 

这个坑系列算是结束了,现在我正在做一些推荐广告相关的工作,后续也会分享一些相关的东西给大家,还有分词、相关搜索、分布式的东西会依次出来哦。

 

上期回顾:《架构之坑系列1:重构中的过度设计与高可用银弹》

 

经平台及作者同意授权转载

来源:西加加语言 订阅号(ID:XJJ267)

作者:吴坚

 

 
 
近期热文(点击标题可阅读全文)

 

近期活动:

DAMS第二届中国数据资产管理峰会

峰会官网:www.dams.org.cn

最新评论
访客 2023年08月20日

230721

访客 2023年08月16日

1、导入Mongo Monitor监控工具表结构(mongo_monitor…

访客 2023年08月04日

上面提到: 在问题描述的架构图中我们可以看到,Click…

访客 2023年07月19日

PMM不香吗?

访客 2023年06月20日

如今看都很棒

活动预告