分库分表的一些小思考

7,808 阅读8分钟

一. 什么是分库分表

分库:就是⼀个数据库分成多个数据库,部署到不同机器。

分表:就是⼀个数据库表分成多个表。

二. 为什么需要分库分表

2.1 为什么需要分库呢?

如果业务量剧增,数据库可能会出现性能瓶颈,这时候我们就需要考虑拆分数据库。从这⼏⽅⾯来看:

  • 磁盘存储

业务量剧增,MySQL单机磁盘容量会撑爆,拆成多个数据库,磁盘使⽤率⼤⼤降低。

  • 并发连接⽀撑

我们知道数据库连接是有限的。在⾼并发的场景下,⼤量请求访问数据库,MySQL单机是扛不住的!当前⾮常⽕的 微服务架构出现,就是为了应对⾼并发。它把订单、⽤户、商品等不同模块,拆分成多个应⽤,并且把单个数据库 也拆分成多个不同功能模块的数据库(订单库、⽤户库、商品库),以分担读写压⼒。

2.2 为什么需要分表?

数据量太⼤的话,SQL的查询就会变慢。如果⼀个查询SQL没命中索引,千百万数据量级别的表可能会拖垮整个数 据库。 即使SQL命中了索引,如果表的数据量超过⼀千万的话,查询也是会明显变慢的。这是因为索引⼀般是B+树结构, 数据千万级别的话,B+树的⾼度会增⾼,查询就变慢啦。

⼩伙伴们是否还记得,MySQL的B+树的⾼度怎么计算的呢? 顺便复习⼀下吧

InnoDB存储引擎最⼩储存单元是⻚,⼀⻚⼤⼩就是16k。B+树叶⼦存的是数据,内部节点存的是键值+指针。索引 组织表通过⾮叶⼦节点的⼆分查找法以及指针确定数据在哪个⻚中,进⽽再去数据⻚中找到需要的数据,B+树结构 图如下:

假设B+树的⾼度为2的话,即有⼀个根结点和若⼲个叶⼦结点。这棵B+树的存放总记录数为=根结点指针数*单个叶 ⼦节点记录⾏数。

  • 如果⼀⾏记录的数据⼤⼩为1k,那么单个叶⼦节点可以存的记录数 =16k/1k =16

  • ⾮叶⼦节点内存放多少指针呢?我们假设主键ID为bigint类型,⻓度为8字节(⾯试官问你int类型,⼀个int就是32位,4字节),⽽指针⼤⼩在InnoDB源码中设置为6字节,所以就是 8+6=14 字节, 16k/14B =16*1024B/14B = 1170

因此,⼀棵⾼度为2的B+树,能存放 1170 * 16=18720 条这样的数据记录。同理⼀棵⾼度为 3 的B+树,能存 放 1170 *1170 *16 =21902400 ,⼤概可以存放两千万左右的记录。B+树⾼度⼀般为1-3层,如果B+到了4层,查 询的时候会多查磁盘的次数,SQL就会变慢。

因此单表数据量太⼤,SQL查询会变慢,所以就需要考虑分表啦。

三. 如何分库分表

3.1 垂直拆分

3.1.1 垂直分库

在业务发展初期,业务功能模块⽐较少,为了快速上线和迭代,往往采⽤单个数据库来保存数据。数据库架构如 下:

image.png

但是随着业务蒸蒸⽇上,系统功能逐渐完善。这时候,可以按照系统中的不同业务进⾏拆分,⽐如拆分成⽤户库、 订单库、积分库、商品库,把它们部署在不同的数据库服务器,这就是垂直分库。

垂直分库,将原来⼀个单数据库的压⼒分担到不同的数据库,可以很好应对⾼并发场景。数据库垂直拆分后的架构 如下:

image.png

3.1.2 垂直分表

如果⼀个单表包含了⼏⼗列甚⾄上百列,管理起来很混乱,每次都 select * 的话,还占⽤IO资源。这时候,我们 可以将⼀些不常⽤的、数据较⼤或者⻓度较⻓的列拆分到另外⼀张表。

⽐如⼀张⽤户表,它包含 user_id、user_name、mobile_no、age、email、nickname、address、 user_desc ,如果 email、address、user_desc 等字段不常⽤,我们可以把它拆分到另外⼀张表,命名为⽤户详 细信息表。这就是垂直分表。

3.2 ⽔平拆分

⽔平分库是指,将表的数据量切分到不同的数据库服务器上,每个服务器具有相同的库和表,只是表中的数据集合 不⼀样。它可以有效的缓解单机单库的性能瓶颈和压⼒。

3.2.1 ⽔平分库

水平分库是指,将表水平切分后分到不同的数据库,使得每个库具有相同的表,表中的数据不相同,水平分库一般是伴随水平分表。

举例:如下图,order 表,水平切分后,分到 database A 和 database B 中,这样原来一个库就被拆分成 2个库。

订单库的⽔平拆分架构如下:

image.png

3.2.2 ⽔平分表

水平分表指的表结构不变,将单表数据切分成多表。切分后的结果:

  • 每个表的结构一样;
  • 每个表的数据不一样;
  • 所有表的数据并集为[全量数据]

如果⼀个表的数据量太⼤,可以按照某种规则(如 hash取模、range ),把数据切分到多张表去。 ⼀张订单表,按时间range 拆分。

切分抽象图如下:

image.png

3.3. ⽔平分库分表策略

分库分表策略⼀般有⼏种,使⽤与不同的场景:

  • range范围
  • hash取模
  • range+hash取模混合

3.3.1 range范围

range,即范围策略划分表。⽐如我们可以将表的主键,按照从 0 ~ 1000万 的划分为⼀个表, 1000~2000万 划分到 另外⼀个表。

当然,有时候我们也可以按时间范围来划分,如不同年⽉的订单放到不同的表,它也是⼀种range的划分策略。

这种⽅案的优点:

  • 这种⽅案有利于扩容,不需要数据迁移。假设数据量增加到5千万,我们只需要⽔平增加⼀张表就好啦,之前 0~4000万 的数据,不需要迁移。

缺点:

  • 这种⽅案会有热点问题,因为订单id是⼀直在增⼤的,也就是说最近⼀段时间都是汇聚在⼀张表⾥⾯的。⽐如 最近⼀个⽉的订单都在 1000万~2000 万之间,平时⽤户⼀般都查最近⼀个⽉的订单⽐较多,请求都打 到 order_1 表啦,这就导致数据热点问题。

3.3.2 hash取模

hash取模策略:指定的路由key(⼀般是user_id、订单id作为key)对分表总数进⾏取模,把数据分散到各个表 中。

⽐如原始订单表信息,我们把它分成4张分表:

  • ⽐如id=1,对4取模,就会得到1,就把它放到 t_order_1 ;
  • id=3,对4取模,就会得到3,就把它放到 t_order_3 ;

这种⽅案的优点:

  • hash取模的⽅式,不会存在明显的热点问题。

缺点:

  • 如果⼀开始按照hash取模分成4个表了,未来某个时候,表数据量⼜到瓶颈了,需要扩容,这就⽐较棘⼿了。 ⽐如你从4张表,⼜扩容成 8 张表,那之前 id=5 的数据是在( 5%4=1 ,即t_order_1),现在应该放到 ( 5%8=5 ,即t_order_5),也就是说历史数据要做迁移了。

3.3.3 range+hash取模混合

既然range存在热点数据问题,hash取模扩容迁移数据⽐较困难,我们可以综合两种⽅案⼀起嘛,取之之⻓,弃之 之短。

⽐较简单的做法就是,在拆分库的时候,我们可以先⽤range范围⽅案,⽐如订单id在04000万的区间,划分为订 单库1;id在4000万8000万的数据,划分到订单库2,将来要扩容时,id在8000万~1.2亿的数据,划分到订单库3。 然后订单库内,再⽤hash取模的策略,把不同订单划分到不同的表。

image.png

四. 什么时候开始考虑分库分表

4.1 什么时候分表?

如果你的系统处于快速发展时期,如果每天的订单流⽔都新增⼏⼗万,并且,订单表的查询效率明变慢时,就需要 规划分库分表了。⼀般B+树索引⾼度是2~3层最佳,如果数据量千万级别,可能⾼度就变4层了,数据量就会明显 变慢了。不过业界流传,⼀般500万数据就要考虑分表了。

4.2 什么时候分库

业务发展很快,还是多个服务共享⼀个单体数据库,数据库成为了性能瓶颈,就需要考虑分库了。⽐如订单、⽤户 等,都可以抽取出来,新搞个应⽤(其实就是微服务思想),并且拆分数据库(订单库、⽤户库)。

五. 分库分表会导致哪些问题

分库分表之后,也会存在⼀些问题:

  • 事务问题
  • 跨库关联
  • 排序问题
  • 分⻚问题
  • 分布式ID

5.1 事务问题

分库分表后,假设两个表在不同的数据库,那么本地事务已经⽆效啦,需要使⽤分布式事务了。

5.2 跨库关联

跨节点Join的问题:解决这⼀问题可以分两次查询实现

5.3 排序问题

跨节点的count,order by,group by以及聚合函数等问题:可以分别在各个节点上得到结果后在应⽤程序端进⾏合并。

5.4 分页问题

  • ⽅案1:在个节点查到对应结果后,在代码端汇聚再分⻚
  • ⽅案2:把分⻚交给前端,前端传来pageSize和pageNo,在各个数据库节点都执⾏分⻚,然后汇聚总数量前 端。这样缺点就是会造成空查,如果分⻚需要排序,也不好搞。

5.5 分布式ID

数据库被切分后,不能再依赖数据库⾃身的主键⽣成机制啦,最简单可以考虑UUID,或者使⽤雪花算法⽣成分布式ID。

六. 分库分表中间件简介

⽬前流⾏的分库分表中间件⽐较多:

  • cobar
  • Mycat
  • Sharding-JDBC
  • Atlas
  • TDDL(淘宝)
  • vitess