Database笔记——分库分表策略

214 阅读15分钟

概述

为什么要分库

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

  • 磁盘储存,业务量剧增,MySQL单机磁盘容量会撑爆,拆成多个数据库,磁盘使用率大大降低。
  • 并发连接支撑,我们知道数据库连接是有限的。在高并发的场景下,大量请求访问数据库,MySQL单机是扛不住的。微服务架构的出现,就是为了应对高并发。它把订单、用户、商品等不同模块,拆分成多个应用,并且把单个数据库也拆分成多个不同功能模块的数据库(订单库、用户库、商品库),以分担读写压力。

为什么需要分表?

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

分库分表——垂直拆分

垂直拆分一般运用于不同业务纬度之间的拆分,例如将一个电商的单独库拆为用户库、订单库、商品库等。对于业务情景单一的“大库”、“大表”,一般不采用垂直拆分的概念,受制于业务条件而非技术条件。以下关于垂直拆分的详细解释引用自掘金-分库分表与数据迁移

垂直拆分,按照业务纬度分库分表

垂直拆库
将一个数据库,拆分为多个提供不同业务数据处理能力的数据库。如:将一个电商的单独库拆为用户库、订单库、商品库

垂直拆表
如果单表数据量过大,还需要对单表进行拆分。如:一个200列的订单主表,拆分为十几个子表:订单表、订单详情表、订单收件信息表等。



优点:

单库(单表)变小,便于管理
对性能和容量有提升
拆分后,系统和数据复杂度降低
可以作为微服务改造的基础

缺点:

库变多,管理变复杂
对业务系统有较强的侵入性
改造过程复杂,容易出故障
拆分到一定程度就无法继续拆分

链接:https://juejin.cn/post/7112239111032799269
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

分库分表——水平拆分

水平分表概述

其实在掘金-分库分表与数据迁移一文中也有提及水平拆分策略,但我本人在学习的过程中觉得文中所提及的过于简略,故而自己整理出了这篇文章,此处先将上文链接中对水平拆分的概述摘录如下:

水平拆分
水平拆分就是直接对数据进行分片,有分库和分表两个具体方式。不改变数据本身的结构,只是降低单个节点数据量。这样对业务系统本身的代码来说不需要做特别大的改动,甚至可以基于一些中间件做到透明。
比如把一个10亿条记录的订单的单库单表。按用户id除以32取模,将单库拆分为32个库;再按订单id除以32取模,每个库再拆为32个表。这样就是32*32=1024个表,单个表数据量就只有不到百万条了。

水平分库分表
一般来说我们我们的数据都是有创建时间的,可以按时间拆分,按照年、季度、月、天都可以。
或者根据用户拆分、甚至可以根据一些自定义的复杂的逻辑来拆分。

为什么有时候不建议分表,只建议分库?
因为分表不能解决容量问题,如果瓶颈在IO(磁盘IO、网络IO)上,分表也解决不了,因为分表还是在同一个机器,而分库可以在两个机器上。

分库还是分表,如何选择?
一般情况下,如果数据本身读写压力较大,磁盘IO已经成为瓶颈,那么分库比分表要好。而使用不同的库,可以并行提升整个集群的并行数据处理能力。
相反的情况下,可以尽量考虑分表,降低单表的数据量。



优点:

解决容量问题
比垂直拆分对系统影响小
部分提升性能和稳定性

缺点:

集群规模大,管理复杂
复杂SQL支持问题
数据迁移问题
一致性问题


链接:https://juejin.cn/post/7112239111032799269
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

上述概述解释了“为什么要水平分库分表”的问题和水平分库分表的优缺点,但没有详细说明水平分库分表的原理与技术实现。

事实上,MySQL引入了HASH、RANGE、LIST及KEY四种不同的表分区策略用于分表,当然在其它的类型数据库中,分区的实现方式略有不同,但是分区的思想原理是相同的,本文以MySQL为例介绍表分区策略。

Hash

HASH分区主要用来确保数据在预先确定数目的分区中平均分布,而在RANGE和LIST分区中,必须明确指定一个给定的列值或列值集合应该保存在哪个分区中,而在HASH分区中,MySQL自动完成这些工作,你所要做的只是基于将要被哈希的列值指定一个列值或表达式,以及指定被分区的表将要被分割成的分区数量。比如拿id来做哈希,1500取余8等于4,所以这条记录就放在user_4这个表中,2011取余8等于3,所以这条记录就放在user_3中。

这种分法好处就是分的很均匀,基本上每个表的数据都差不多,但弊端在于以后新增数据又得分表了,需要重新迁移以前的数据。我粗浅地认为一致性哈希可以解决普通哈希在上述场景的弊端,但在我查阅的文章中,一般提及一致性哈希主要在负载均衡的场景下,但显然该算法在水平分库场景下也可以避免每次添加新数据库(服务器)后需要对整个哈希环上的节点重新哈希的弊端。

一致性hash

Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据迁移的问题。

将哈希空间 [0, 2n-1] 看成一个哈希环,每个服务器节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。

一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将它前一个节点 C 上的数据重新进行分布即可,对于节点 A、B、D 都没有影响。

上面描述的一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同。数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。

解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得多,那么虚拟节点在哈希环上分布的均匀性就会比原来的真实节点好,从而使得数据分布也更加均匀。

关于一致性hash的实际部署,参考一致性Hash算法在数据库分表中的实践 - 知乎

Range

基于属于一个给定连续区间的列值,把多行分配给同一个分区,这些区间要连续且不能相互重叠。

比如我们可以将表的主键,按照从0-1000万划分为一个表,1000万-2000万划分到另外一个表。当然,有时候我们也可以按时间范围来划分,如不同年月的订单放到不同的表,它也是一种range的划分策略。

这种方案的优点:这种方案有利于扩容,不需要数据迁移。假设数据量增加到5千万,我们只需要水平增加一张表就好啦,之前0-4000万的数据,不需要迁移。

缺点:这种方案会有热点问题,因为订单id是一直在增大的,也就是说最近一段时间都是汇聚在一张表里面的。比如最近一个月的订单都在1000万-2000万之间,平时用户一般都查最近一个月的订单比较多,请求会都打到order_1表,这就导致表的数据热点问题。

我们为什么要分库分表 - 掘金一文中,提到了同时使用range和hash的混合分表策略,用于解决上述热点问题,先将该部分摘录如下:

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

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


链接:https://juejin.cn/post/7085132195190276109
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

List

类似于按RANGE分区,区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择分区的。LIST分区通过使用“PARTITION BY LIST(expr)”来实现,其中“expr” 是某列值或一个基于某个列值、并返回一个整数值的表达式,然后通过“VALUES IN (value_list)”的方式来定义每个分区,其中“value_list”是一个通过逗号分隔的整数列表。

这一分区方案的优缺点基本等同于range方法。

Key

类似于按hash分区,区别在于KEY分区只支持计算一列或多列,且MySQL 服务器提供其自身的哈希函数。必须有一列或多列包含整数值。

这一分区方案的优缺点基本等同于hash方法。

表分区的实践

关于表分区的建立实践参考MySql分区策略:HASH/RANGE/LIST/KEY_persistenceヾ(◍°∇°◍)ノ的博客

关于单测示例可参考MySQL分库分表 看完吊打面试官 - 知乎

分库分表带来的问题

事务问题

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

当更新内容同时存在于不同库找那个,不可避免会带来跨库事务问题。跨分片事务也是分布式事务,没有简单的方案,一般可使用“XA协议”和“两阶段提交”处理。

分布式事务能最大限度保证了数据库操作的原子性。但在提交事务时需要协调多个节点,推后了提交事务的时间点,延长了事务的执行时间,导致事务在访问共享资源时发生冲突或死锁的概率增高。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平扩展的枷锁。

对于那些性能要求很高,但对一致性要求不高的系统,往往不苛求系统的实时一致性,只要在允许的时间段内达到最终一致性即可,可采用事务补偿的方式。与事务在执行中发生错误立刻回滚的方式不同,事务补偿是一种事后检查补救的措施,一些常见的实现方法有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源进行同步等。

跨库关联

切分之前,系统中很多列表和详情表的数据可以通过join来完成,但是切分之后,数据可能分布在不同的节点上,此时join带来的问题就比较麻烦了,考虑到性能,尽量避免使用Join查询。解决的一些方法:

全局表,也可看做“数据字典表”,就是系统中所有模块都可能依赖的一些表,为了避免库join查询,可以将这类表在每个数据库中都保存一份。这些数据通常很少修改,所以不必担心一致性的问题。

字段冗余,一种典型的反范式设计,利用空间换时间,为了性能而避免join查询。例如,订单表在保存userId的时候,也将userName也冗余的保存一份,这样查询订单详情顺表就可以查到用户名userName,就不用查询买家user表了。但这种方法适用场景也有限,比较适用依赖字段比较少的情况,而冗余字段的一致性也较难保证。

在系统service业务层面,分两次查询,第一次查询的结果集找出关联的数据id,然后根据id发起器二次请求得到关联数据,最后将获得的结果进行字段组装。这是比较常用的方法。

ER切片,关系型数据库中,如果已经确定了表之间的关联关系(如订单表和订单详情表),并且将那些存在关联关系的表记录存放在同一个分片上,那么就能较好地避免跨分片join的问题,可以在一个分片内进行join。在1:1或1:n的情况下,通常按照主表的ID进行主键切分。

排序问题

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

跨节点多库进行查询时,会出现limit分页、order by 排序等问题。分页需要按照指定字段进行排序,当排序字段就是分页字段时,通过分片规则就比较容易定位到指定的分片;当排序字段非分片字段时,就变得比较复杂.需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户。

如果取得页数很大,情况就变得复杂的多,因为各分片节点中的数据可能是随机的,为了排序的准确性,需要将所有节点的前N页数据都排序好做合并,最后再进行整体排序,这样的操作很耗费CPU和内存资源,所以页数越大,系统性能就会越差。

在使用Max、Min、Sum、Count之类的函数进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总再次计算。

分页问题

方案1:在个节点查到对应结果后,在代码端汇聚再分页。

方案2:把分页交给前端,前端传来pageSize和pageNo,在各个数据库节点都执行分页,然后汇聚总数量前端。这样缺点就是会造成空查,如果分页需要排序,也不好搞。

分布式id

常见的方案有uuid和雪花id,这里建议采用雪花id。

关于uuid和雪花id可以参考分布式ID生成器 · 系统设计

关于其它分布式id实现方式,参考注意:雪花算法并不是ID的唯一选择! - 掘金

分库分表中间件

目前流行的分库分表中间件比较多:

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

常用分库分表中间件

参考文章

数据库怎么分库分表 - 掘金

Mysql数据库分库分表会有哪些问题? - 掘金

分库分表与数据迁移 - 掘金

我们为什么要分库分表 - 掘金

MySQL:互联网公司常用分库分表方案汇总! - 掘金

MySql分区策略:HASH/RANGE/LIST/KEY_persistenceヾ(◍°∇°◍)ノ的博客

一致性Hash算法在数据库分表中的实践 - 知乎

MySQL分库分表 看完吊打面试官 - 知乎

注意:雪花算法并不是ID的唯一选择! - 掘金

分布式ID生成器 · 系统设计