在移动互联网时代,海量的用户每天产生海量的数量,比如:
-
用户表;
-
订单表;
-
交易流水表.
以支付宝用户为例,8亿;微信用户更是10亿.订单表更夸张,比如美团外卖,每天都是几千万的订单.淘宝的历史订单总量应该百亿,甚至千亿级别,这些海量数据远不是一张表能Hold住的.事实上MySQL单表可以存储10亿级数据,只是这时候性能比较差,业界公认MySQL单表容量在1KW以下是最佳状态,因为这时它的BTREE索引树高在3~5之间.
既然一张表无法搞定,那么就想办法将数据放到多个地方,目前比较普遍的方案有3个:
-
1️⃣.分区;
-
2️⃣.分库分表;
-
3️⃣.NoSQL/NewSQL.
说明:
只分库,或者只分表,或者分库分表的融合方案都统一认为是分库分表方案.因为分库,或者分表只是一种特殊的分库分表而已.NoSQL比较具有代表性的是MongoDB/es;NewSQL比较具有代表性的是TiDB.
二. 数据库存在的瓶颈而当数据库中的数据量变大后,可能就会产生各种瓶颈的限制,比如IO瓶颈,CPU瓶颈等.但是无论是哪种瓶颈最终都会导致数据库的活跃连接数增加,进而逼近甚至达到数据库可承载活跃连接数的阈值.在业务Service来看就是,可用数据库连接减少甚至无连接可用,接下来就可能导致并发量、吞吐量、崩溃等各种问题的产生.
1. IO性能瓶颈磁盘读取IO时的瓶颈: 热点数据太多,数据库缓存放不下,每次查询时会产生大量的IO,降低查询速度.解决办法是可以考虑分库和垂直分表;
网络IO瓶颈: 请求的数据太多,网络带宽不够,解决办法是可以考虑分库.
2. CPU性能瓶颈SQL问题: 如SQL中包含join,group by,order by,非索引字段条件查询等,这就增加了CPU运算的操作.解决办法是对SQL优化,建立合适的索引,在业务Service层进行业务计算;
单表数据量太大: 查询时扫描的行太多,SQL效率降低,增加了CPU运算的操作.解决办法是考虑水平分表.
三. 为什么不采用分区方案? 我们再看分区表方案.了解这个方案之前,先了解它的原理:分区表是由多个相关的底层表实现,这些底层表也是由句柄对象表示,
所以我们也可以直接访问各个分区,存储引擎管理分区的各个底层表和管理普通表一样(所有的底层表都必须使用相同的存储引擎).
分区表的索引只是在各个底层表上各自加上一个相同的索引,从存储引擎的角度来看,
底层表和一个普通表没有任何不同,存储引擎也无须知道这是一个普通表还是一个分区表的一部分.
事实上,这个方案也不错,它对用户屏蔽了sharding的细节,即使查询条件没有sharding column,它也能正常工作(只是这时候性能一般).不过它的缺点很明显:很多的资源都受到单机的限制,例如连接数,网络吞吐等.虽然每个分区可以独立存储,但是分区表的总入口还是一个MySQL示例,从而导致它的并发能力非常一般,远远达不到互联网高并发的要求!
还有其他缺点比如:无法使用外键,不支持全文索引等.
所以,如果使用分区表,你的业务应该具备如下两个特点:
-
1️⃣.数据不是海量(分区数有限,存储能力就有限)的;
-
2️⃣.并发能力要求不高.
首先,为什么不选择第三种方案NoSQL/NewSQL,主要是因为RDBMS(关系型数据库有)以下几个优点:
-
RDBMS生态完善;
-
RDBMS绝对稳定;
-
RDBMS支持事务.
NoSQL/NewSQL作为新生儿,在我们把可靠性当做首要考察对象时,它是无法与RDBMS相提并论的.RDBMS发展了几十年,只要有软件的地方,它都是核心存储的首选.
目前绝大部分公司的核心数据都是以RDBMS存储为主,NoSQL/NewSQL存储为辅.互联网公司又以MySQL为主,国企&银行等不差钱的企业以Oracle/DB2为主.NoSQL/NewSQL宣传的无论多厉害,就现在各大公司对它的定位,都是RDBMS的补充,而不是取而代之.
五. 分库分表方案介绍接下来我们介绍目前互联网行业处理海量数据的通用方法: 分库分表.
1. 常见的分库分表方案虽然大家都是采用分库分表方案来处理海量核心数据,但是还没有一个一统江湖的中间件,下面列举一些有一定知名度的分库分表中间件:
-
阿里的TDDL,DRDS和cobar;
-
开源社区的sharding-jdbc(3.x已经更名为sharding-sphere);
-
民间组织的MyCAT;
-
360的Atlas;
-
美团的zebra.
其他比如网易,58,京东等公司都有自研的中间件.
这么多的分库分表中间件全部可以归结为两大类型:
-
1️⃣.CLIENT模式;
-
2️⃣.PROXY模式.
CLIENT模式代表有阿里的TDDL,开源社区的sharding-jdbc(sharding-jdbc的3.x版本即sharding-sphere已经支持了proxy模式),架构如下:
PROXY模式代表有阿里的cobar,民间组织的MyCAT,架构如下:
但是,无论是CLIENT模式,还是PROXY模式,几个核心的步骤是一样的:SQL解析-->重写-->路由-->执行-->结果归并.
六. 什么时候考虑切分下面讲述一下什么时候需要考虑做数据切分.
1. 能不切分尽量不要切分并不是所有表都需要进行切分,主要还是看数据的增长速度.切分后会在某种程度上提升了业务的复杂度,数据库除了承载数据的存储和查询外,协助业务更好的实现需求也是其重要工作之一.
不到万不得已不要轻易使用分库分表这个大招,避免"过度设计"和"过早优化".分库分表之前,不要为分而分,先尽力去做力所能及的事情,例如:升级硬件、升级网络、读写分离、索引优化等.当数据量达到单表的瓶颈时候,再考虑分库分表.
2. 数据量过大,正常运维影响业务访问这里说的运维,是指:
-
1️⃣. 对数据库备份,如果单表太大,备份时需要大量的磁盘IO和网络IO.例如1T的数据,网络传输占50MB时候,需要20000秒才能传输完毕,整个过程的风险都是比较高的;
-
2️⃣. 对一个很大的表进行DDL修改时,MySQL会锁住全表,这个时间会很长,这段时间业务不能访问此表,影响很大. 如果使用pt-online-schema-change,使用过程中会创建触发器和影子表,也需要很长的时间.在此操作过程中,都算为风险时间.将数据表拆分,总量减少,有助于降低这个风险;
-
3️⃣. 大表会经常访问与更新,就更有可能出现锁等待.将数据切分,用空间换时间,变相降低访问压力.
举个例子,假如项目一开始设计的用户表如下:
id bigint #用户的ID;
name varchar #用户的名字;
last_login_time datetime #最近登录时间;
personal_info text #私人信息;
..... #其他信息字段.
在项目初始阶段,这种设计是满足简单的业务需求的,也方便快速迭代开发.而当业务快速发展时,用户量从10w激增到10亿,用户非常的活跃,每次登录会更新 last_login_name 字段,使得 user 表被不断update,压力很大.而其他字段: id, name, personal_info 是不变的或很少更新的,此时在业务角度,就要将 last_login_time 拆分出去,新建一个 user_time 表.
personal_info 属性是更新和查询频率较低的,并且text字段占据了太多的空间.这时候就要对此垂直拆分出 user_ext 表了.
4. 数据量快速增长随着业务的快速发展,单表中的数据量会持续增长,当性能接近瓶颈时,就需要考虑水平切分,做分库分表了.此时一定要选择合适的切分规则,提前预估好数据容量.
5. 安全性和可用性鸡蛋不要放在一个篮子里.在业务层面上垂直切分,将不相关的业务的数据库分隔,因为每个业务的数据量、访问量都不同,不能因为一个业务把数据库搞挂而牵连到其他业务.利用水平切分,当一个数据库出现问题时,不会影响到100%的用户,每个库只承担业务的一部分数据,这样整体的可用性就能提高.
七. 数据切分我们知道关系型数据库本身比较容易成为系统的瓶颈,单机存储容量、连接数、处理能力都有限.当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从库、优化索引,在做很多操作时性能仍下降严重.此时就要考虑对其进行切分,切分的目的就在于减少数据库的负担,缩短查询时间.
分布式数据库的核心内容无非就是数据切分(Sharding),以及切分后对数据的定位、整合.数据切分就是将数据分散存储到多个数据库中,使得单一数据库中的数据量变小,通过扩充主机的数量来缓解单一数据库的性能问题,从而达到提升数据库操作性能的目的.
数据切分根据其切分类型,可以分为两种方式: 垂直(纵向)拆分和水平(横向)拆分.
七. 垂直拆分垂直拆分常见有垂直分库和垂直分表两种.
1. 垂直分库
垂直分库就是根据业务的耦合性,将关联度低的不同表存储在不同的数据库.做法与大系统拆分为多个小系统类似,按业务分类进行独立划分,与"微服务治理"的做法相似,每个微服务使用单独的一个数据.如图:
这样我们把表按模块划分到不同数据库中(当然原则还是不破坏第三范式),这种拆分在大型网站的演变过程中是很常见的.当一个网站还在很小的时候,只有小量的人来开发和维护,各模块和表都在一起,当网站不断丰富和壮大的时候,也会变成多个子系统来支撑,这时就有按模块和功能把表划分出来的需求.其实,相对于垂直切分更进一步的是服务化改造,说得简单就是要把原来强耦合的系统拆分成多个弱耦合的服务,通过服务间的调用来满足业务需求看,因此表拆出来后要通过服务的形式暴露出去,而不是直接调用不同模块的表,淘宝在架构不断演变过程,最重要的一环就是服务化改造,把用户、交易、店铺、宝贝这些核心的概念抽取成独立的服务,也非常有利于进行局部的优化和治理,保障核心模块的稳定性.
2. 垂直分表
垂直分表是基于数据库中的"列"进行,某个表字段较多,可以新建一张扩展表,将不经常用的或字段长度较大的字段拆分到扩展表中.在字段很多的情况下(例如一个大表有100多个字段),通过"大表拆小表",更便于开发与维护,也能避免跨页问题,MySQL底层是通过数据页存储的,一条记录占用空间过大会导致跨页,造成额外的性能开销.另外数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加载更多的数据,命中率更高,减少了磁盘IO,从而提升了数据库性能.
-
解决业务系统层面的耦合,业务清晰;
-
与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等;
-
高并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈.
-
部分表无法join,只能通过接口聚合方式解决,提高了开发的复杂度;
-
分布式事务处理复杂;
-
依然存在单表数据量过大的问题(需要水平切分).
上面谈到垂直切分只是把表按模块划分到不同数据库,但没有解决单表大数据量的问题,而水平切分就是要把一个表按照某种规则把数据划分到不同表或数据库里.
水平分表适合用户表行数很多的情况下,一般单表行数超过5000万就得分表,如果单表的数据比较复杂那可能2000万甚至1000万就得分了,所以这个得看实际情况,有些表很简单可能一亿行都不用分.也就是说当一个表行数超过千万级别的时候关注一下,如果没有性能问题就可以再等等看,不要急着分表,因为分表会是带来很多问题.
一般当一个应用难以再细粒度的垂直切分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时候就需要进行水平切分了.
水平切分可以分为库内分表和分库分表,是根据表内数据内在的逻辑关系,将同一个表按不同的条件分散到多个数据库或多个表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果.如图所示:
库内分表只解决了单一表数据量过大的问题,但没有将表分布到不同机器的库上,因此对于减轻MySQL数据库的压力来说,帮助不是很大,大家还是竞争同一个物理机的CPU、内存、网络IO,最好通过分库分表来解决.
2. 水平拆分的优点:-
不存在单库数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力;
-
应用端改造较小,不需要拆分业务模块.
-
跨分片的事务一致性难以保证;
-
跨库的join关联查询性能较差;
-
数据多次扩展难度和维护量极大.

以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中.
结果:
- 每个库的结构都一样;
- 每个库的数据都不一样,没有交集;
- 所有库的并集是全量数据.
数据库多了,io和cpu的压力可以得到成倍的缓解.
5. 水平分表
以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中.
结果:
- 每个表的结构都一样;
- 每个表的数据都不一样,没有交集;
- 所有表的并集是全量数据.
表的数据量少了,单次SQL执行效率高,减轻了CPU的负担.
6. 水平分表实现方案水平分表的问题比垂直分表更麻烦,要考虑怎么切,讲的高级点就叫路由.
1️⃣. 根据数值范围划分按照时间区间或ID区间来切分.例如:按日期将不同月甚至是日的数据分散到不同的库中;将userId为1~9999的记录分到第一个库,10000~20000的分到第二个库,以此类推.
某种意义上,某些系统中使用的"冷热数据分离",将一些使用较少的历史数据迁移到其他库中,业务功能上只提供热点数据的查询,也是类似的实践.
优点:
-
单表大小可控;
-
天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移;
-
使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题.
缺点:
热点数据成为性能瓶颈.连续分片可能存在数据热点,例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询.
一般采用hash取模mod的切分方式,例如:将 Customer 表根据 cusno 字段切分到4个库中,余数为0的放到第一个库,余数为1的放到第二个库,以此类推.这样同一个用户的数据会分散到同一个库中,如果查询条件带有cusno字段,则可明确定位到相应库去查询.
优点:
数据分片相对比较均匀,不容易出现热点和并发访问的瓶颈.
缺点:
-
后期分片集群扩容时,需要迁移旧的数据(使用一致性hash算法能较好的避免这个问题);
-
容易面临跨分片查询的复杂问题.比如上例中,如果频繁用到的查询条件中不带cusno时,将会导致无法定位数据库,从而需要同时向4个库发起查询,再在内存中合并数据,取最小集返回给应用,分库反而成为拖累.

还是拿用户表来说,就是弄一个路由表,里面存userId和表编号,表示这个userId是这张user表的的.这种方式也简单,之后又要分表了之后改改路由表,迁移一部分数据.但是这种方法导致每次查询都得查两次,并且如果路由表太大了,那路由表又成为瓶颈了.