本文既非教程也不深奥,即使不懂技术的人,读一下也肯定能有收获。
在上一篇《服务器集群》中,我们通过系统分离和负载均衡,把服务器从单台变成了集群(但数据库还是一台)。
这次"双11"又临时加了好几台服务器,总算有惊无险地过去了。但令人忧心的是,数据库已经长期处于高负荷状态,马上"双12"还要加大推广,如果数据库扛不住了,其他服务器再多也没用啊。
所以我们今天就来研究一下:如何突破数据库的性能瓶颈,解决高并发的最大痛点。
先举个例子
因为担心大家对着一堆技术名词头疼,所以我们设计了一个更容易理解的例子:
- 有一位作家找了个助手,她负责记录和整理书稿,还要给客人朗读书稿。
- 助手的工作非常忙,因为作家的作品都交给她打理,简直是一刻也离不开她。
- 随着作家越来越有名,助手的工作也越来越繁重,她觉得实在忙不过来了。
其实数据库就像是这个助手,它的工作同样是“读”和“写”,同样也面临着“一个人(机器)忙不过来”的问题。
接下来我们就拿这个“作家助手”的例子,来解释相关的技术原理。
基本思路
作家发现他的助手忙不过来,该怎么办?
作家首先想到的,是给她换更好的笔墨、更好的纸张、更大的书桌,提高她的工作效率。最开始这还很管用,但是随着事情越来越多,助手很快又忙不过来了。而且这时候作家发现,虽然还有更好的纸笔,但是他已经买不起了。
同样,当数据库扛不住的时候,我们最容易想到的办法也是升级硬件。
最初这很有效,但多次升级后会发现效果越来越不明显,而且机器越来越昂贵,大大超出了我们的承受能力。
当最高配的PC服务器也撑不住的时候,再升级就只能换成小型机了。早年包括淘宝和支付宝在内,核心数据库用的也都是高性能的小型机。这种小型机价格昂贵,便宜的几十万一台,贵的上千万一台。
这张图是HP的SuperDome小型机,大概800万/台。当年某银行电商平台的数据库服务器,一次买了两台(一台是备用机)。
更麻烦的是:数据库升级硬件,通常至少也得停机维护一天,这种情况多来几次,用户就都跑光了。
作家意识到,他的助手终究不是千手观音,必须找更多的人来分担助手的工作。
同样,分散压力才是拯救数据库的正确思路,那么具体怎么做呢?
- 一是“打群架”(人多好办事)
最理想的,始终还是把数据库从一台“单点”变成多台“集群”。
虽然数据库不能直接做负载均衡(会出现“数据不一致“的致命问题),不过我们有其他变通的方法,比如分库分表和读写分离。
- 二是“找外包”(不做脏累活)
数据库忙不过来,大多是被一些重复性的工作拖累。我们可以把这些工作“外包”给其他更专业的系统,比如缓存系统和搜索引擎。
为了能够轻松应对下次双11,我们既要打群架,也要找外包。
(一)读写分离
作家发现,朗读占用了助手太多的时间,所以又招了一个工读生来分担一部分朗读工作,而书稿的管理则仍然由助手一个人负责。但是每当有新书稿时,助手都会通知工读生抄写一份,保证朗读的内容都是最新的。
这就是我们常说的数据库“读写分离”(也叫“主从集群”)。
数据库读写分离原理
具体做法就是:把一台数据库分成一模一样的两台,其中一台既能“写”也能“读”(主库),而另一台则只能“读”(从库)。程序只能向主库写入数据,所有数据以主库为准,主库会通知从库同步更新。
实现这种机制的关键,在于主库如何将数据同步更新到从库。幸运的是,我们不需要担心这个问题,常见的数据库软件比如MySQL,都自带这种主从机制,我们不必写任何代码,只需要简单的配置参数就能实现。
随着作家名气越来越大,来听作品朗读的客人也越来越多,助手和工读生两个加起来也忙不过来了。这时候作家又招了第二个工读生,之后没过多久又招了第三个...
这种情况就是数据库的“一主多从”,而且多台从库完全可以通过负载均衡组成集群。
数据库一主多从原理
为什么会需要“一主多从”?
因为互联网产品有一个普遍的特点,就是“看的人多,说的人少”。翻译过来就是数据库“读大于写”。
数据库的查询(SELECT)请求,无论数量还是并发都远远大于写入请求(INSERT/UPDATE)。根据行业经验,一个典型的网站数据库,查询请求的占比高达90%以上。
因此数据库的压力主要是高并发查询,而多台从库组成集群,无疑大大提高了并发查询能力。
(二)缓存系统
虽然作家已经招了很多工读生,但一个工读生最多只能同时给10个人朗读,这种低效的方式实在无法人满意。于是作家找来了一个出版社,把书稿整理校对之后印刷发行,这样广大读者就在第一时间读到了最新的作品。
这个出版社,就是我们的“缓存系统”了。
缓存是一种用内存来临时存取数据的软件系统,由于数据读写都在内存中完成,缓存的效率是数据库无法比拟的。
前面说过,数据库的压力主要在查询上。因此我们把查询的结果放入缓存中,下次查询的时候,程序先从缓存中读数据,读不到才去数据库里查,这样就极大地减轻了数据库的压力。
最早的缓存系统叫Memcached,后来在此基础上发展出了 Redis,它们都是开源软件,整个互联网行业几乎都在使用。Redis最大的特点就是轻量高效,一台服务器就可以轻松做到上万并发。
为了使用缓存,我们需要在程序中加入一段代码,来控制缓存的读写和更新。我们需要保持缓存数据与数据库一致,办法很简单,当数据库中的数据发生变化时,从缓存中清除旧数据就行了。
简单可靠的缓存原理
缓存可以部署在任一台服务器上(可以与应用程序共用一台机器),但最好还是独立一台服务器,一方面是集中控制的需要,另一方面也便于后续扩容。
加入缓存后的系统架构
出版社承诺发行作家的每一部作品。但作品实在太多,因此最开始只是印个样本,只有订购的人多了,才会加大印刷量,而那些无人问津的作品就不再印刷了。
同样,用于缓存的内存容量也是有限的,当内存不够用的时候,缓存系统会优先保留“热点数据”,也就是读取频次数高的数据,从而提高缓存的利用率。
80%的数据库查询请求,最终会落在20%的热点数据之上,因此使用缓存能够减轻大约80%的数据库压力。
缓存的“减压”效果非常明显,每当数据库扛不住时,程序员都会尝试通过加缓存来缓解压力点。甚至有时候在最初开发系统的时候,就已经给所有查询都加上了缓存。
(三)搜索引擎
用作家的例子来解释搜索引擎,稍微有点困难:
作家出的书已经很多了,但她的读者有个特点:如果书中提到某个角色或地名,就会很高兴地购买。但是读者不可能去翻找每一本书,所以大家都喜欢直接去问作家的助手。
不胜其扰的助手于是想了一个办法:她把作家的所有作品都读了一遍,将书里出现的所有角色和地名都记下来,并注明它们分别在哪些书中出现过,最后编成了一本索引。这样,读者只要从索引中找到自己喜欢的角色地名,就知道应该购买哪几本书,不用再来问她了。
这就是使用搜索引擎的原因,以及搜索引擎的基本原理。
我们目前使用的关系型数据库,在做关键词检索时,采用的是非常低效的全文模糊匹配方式,也就是SQL中的“LIKE”查询。

假设数据库有一千万条商品数据,用户想搜索描述中提到某个关键词的所有商品,理论上就要把一千万条数据都读取一遍,并且还要对描述内容进行全文比对,这显然极其低效。如果大家都这样搜索,很容易就把数据库拖垮了。
而搜索引擎跟数据库不一样,它采用的是更高效的爬虫和索引机制。
搜索引擎是先根据一个词典,把所有的关键词都在数据库中检索一次(爬虫),再把检索结果都保存为索引文件(一个关键词对应一个索引)。当用户搜索某个关键词时,只需要直接读那个对应的索引文件就行,这样就不用再去查询数据库,大大减轻了数据库压力。
这种搜索引擎技术,虽然看起来跟百度一样复杂,但其实已经有很多成熟的开源软件可以直接使用,例如 Lucene、Solr、Elasticsearch、Sphinx 等等,我们很容易就可以系统加上搜索引擎服务。
加入搜索引擎后的系统架构
当然,程序还是需要改一下的,开源搜索引擎都有成熟的接口,开发并不困难。
(四)分库分表
我们特意把这个放到最后来介绍,首先还是作家助手的例子:
作家把前面说的这些方法都用上了,但是他的助手还是累趴下了。原因是作家写了几本大部头长篇,整理书稿的压力太大,出版发行的工作也太重。而这些工作,又只能是助手一个人完成,没法让工读生来做。所以作家觉得,他还应该多找几个助手。
单台数据库的能力终究是有限的,特别是当数据多到一定程度(上亿条)时,读写分离和缓存也无济于事。
所以必须分成多个数据库,但是要怎么分呢?
作家现在有了两个助手,他让新来的助手A只负责工作量最大的那部作品,而其他的则继续让原来的助手B负责,两个人分工明确,既减轻了压力,也不会互相干扰。
我们知道,数据库是由多张数据表组成的,我们完全可以建立多个数据库,每个数据库里面存放不同的数据表,这就是“分库分表”中“分库”的基本原理。
而首先被分出去的,通常就是数据最多、压力最大的那张表。
分库原理
作家让助手A和B各负责一本书。但是A那本书的内容是对B那本书的注解,因此校对书稿必须同时看两本书。然而A和B又都不能看对方那本,所以只好把书稿都丢给作家,让他自己去校对。作家为此苦不堪言。
数据库“分库”也会遇到“表关联”问题,当两张表的数据之间存在关联时,分库可能会使程序变得非常复杂。
比如“列出我的所有订单和这些订单中包含的商品信息”,当订单表和商品表放在同一个数据库时,一句简单的SQL语句就可以完成,但分库之后,程序只能先读出所有订单,再针对每个订单,分别去另一个库里找商品信息,效率就变得非常低下了。
说完“分库”我们再来说“分表”。
作家有一部超级长篇的连载小说,写了几千章还在更新,但是负责整理校对的助手受不了了。于是作家把这部小说分成了很多册,每一册都不会太长,助手校对起来就轻松很多。后来作家又增加了一个助手,两个助手一人负责这部小说的前10册,另一个人负责后10册。
“分库”虽然分开了不同的数据表,但单张表的记录数太多,也会导致数据库压力过大。例如如电商中的订单数据(每天都在增加,而且需要一直保留历史数据)。
这个时候我们就需要进行“分表”:把原本存在一张表里面的数据,拆分成多份,分别存到多张表中,这些表的数据结构完全一致。分开后的表,甚至还可以分别存在不同的数据库中,也就是“分表再分库”。
分表之后再进行分库
作家在把小说分册的时候,用了两种切分方式。
第一种是按篇幅均分,比如一个章节大约3000字,一册书统一500章节,这样每一册书的篇幅(厚度)都是差不多的,整理校对的工作量也比较平均。
第二种是按故事段落拆分,比如第一册是讲主角上山学艺的,第二册讲主角下山报仇...
数据库分表的切分方式与之类似:
- 水平切分:按数据区段,比如按用户编号,每一百万个编号分为一张表。
- 垂直切分:按业务归属,比如把订单按城市切分、区分线上订单和门店订单。
理论上,只要不断对数据库进行拆分,总是能将单台数据库的压力降低到可以承受的水平。但在实践中,除非万不得已,程序员们并不喜欢分库分表。因为无论怎么分,都会提高程序的复杂度,并且增加出BUG的风险。
虽然如此,分库分表依然是应对数据库高并发的最有效手段。为了下次双11,分库分表势在必行。
小结
除了上面四种方法之外,其他常用的还有非关系型数据库(NoSQL)、分布式数据库等。灵活运用这些方法,基本就能解决“数据库扛不住”的难题。
其实不管是做集群还是拆数据,所有高并发解决方案的基本原理都是“分散压力”。我们只要牢记这一点,就很容易找到扛过双11、确保不宕机的有效方法。
当然,也不要忘了继续优化代码!特别是那些读写数据库的代码,有时候优化一下,就可以节省好多台服务器(这才是程序员高薪的底气)。
下一篇文章,计划介绍一些常见的“压力点”和“解压”思路,敬请期待!
如果你对大型网站架构有兴趣,不妨看一下这个专栏。一顿夜宵的钱,学点东西还是值得的。
相关文章链接:
双11不宕机,高并发系统的基本原理(一):服务器集群