分库分表总结

什么是分库分表

从字面上简单理解,就是将原本存储在一个库的数据分块存储在多个库上,将原本存储在一个表的数据分块存储在多个表里面。

为什么要分库分表

分库分表的目的就是为了缓解数据库的压力,最大限度提高数据操作的效率。

1、IO瓶颈

第一种:磁盘读IO瓶颈,热点数据太多,数据库缓存放不下,每次查询时会产生大量的IO,降低查询速度 -> 分库和垂直分表。

第二种:网络IO瓶颈,请求的数据太多,网络带宽不够 -> 分库。

2、CPU瓶颈

第一种:SQL问题,如SQL中包含join,group by,order by,非索引字段条件查询等,增加CPU运算的操作 -> SQL优化,建立合适的索引,在业务Service层进行业务计算。

第二种:单表数据量太大,查询时扫描的行太多,SQL效率低,CPU率先出现瓶颈 -> 水平分表。

image-20200727195645646

分库分表的优缺点

水平切分的优点如下:

  • 单库单表的数据保持在一定的量级,有助于性能的提高。
  • 切分的表的结构相同,应用层改造较少,只需要增加路由规则即可。
  • 提高了系统的稳定性和负载能力。

水平切分的缺点如下:

  • 切分后,数据是分散的,很难利用数据库的Join操作,跨库Join性能较差。
  • 拆分规则难以抽象。
  • 分片事务的一致性难以解决。
  • 数据扩容的难度和维护量极大

垂直切分的优点如下:

  • 拆分后业务清晰,拆分规则明确。
  • 系统之间进行整合或扩展很容易。
  • 按照成本、应用的等级、应用的类型等将表放到不同的机器上,便于管理。
  • 便于实现动静分离、冷热分离的数据库表的设计模式。
  • 数据维护简单。

垂直切分的缺点如下:

  • 部分业务表无法关联(Join),只能通过接口方式解决,提高了系统的复杂度。
  • 受每种业务的不同限制,存在单库性能瓶颈,不易进行数据扩展和提升性能。
  • 事务处理复杂。

注意:一定要好好决定分片规则,尽量选择不会变动的字段,如果选择区域,性别,年龄等字段

综上所述,垂直切分和水平切分的共同点如下:

  • 存在分布式事务的问题。
  • 存在跨节点Join的问题。
  • 存在跨节点合并排序、分页的问题。
  • 存在多数据源管理的问题。

分库分表的方案

水平分库分表一般有两种形式:

  • 按照数值范围来分,就是每个库一段连续的数据,这个一般是按比如时间范围或者id范围来分,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。
  • 按照某个字段 hash 一下均匀分散,比如用户Id mod n,余数为0的记录放到第一个库,余数为1的放到第二个库,以此类推。这个较为常用。二次分库时,为了数据迁移方便,一般是按倍数增加,比如初始4个库,二次分裂为8个,再16个。这样对于某个库的数据,一半数据移到新库,剩余不动,对比每次只增加一个库,所有数据都要大规模变动。数据水平切分后我们希望是一劳永逸或者是易于水平扩展的,所以推荐采用mod 2^n这种一致性Hash。
  • 按照地理区域:比如按照华东,华南,华北这样来区分业务
  • 按照时间:按照时间切分,就是将6个月前,甚至一年前的数据切出去放到另外的一张表,因为随着时间流逝,这些表的数据 被查询的概率变小,所以没必要和“热数据”放在一起,这个也是“冷热数据分离”。

水平分库

image-20200727192928071

  1. 概念:以字段为依据,按照一定策略(hash、range等),将一个中的数据拆分到多个中。
  2. 结果:
    • 每个结构都一样;
    • 每个数据都不一样,没有交集;
    • 所有并集是全量数据;
  3. 场景:系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。
  4. 分析:库多了,io和cpu的压力自然可以成倍缓解。

水平分表

image-20200727193052288

  1. 概念:以字段为依据,按照一定策略(hash、range等),将一个中的数据拆分到多个中。
  2. 结果:
    • 每个结构都一样;
    • 每个数据都不一样,没有交集;
    • 所有并集是全量数据;
  3. 场景:系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。
  4. 分析:表的数据量少了,单次SQL执行效率高,自然减轻了CPU的负担。

垂直分库

image-20200727193154078

  1. 概念:以为依据,按照业务归属不同,将不同的拆分到不同的中。
  2. 结果:
    • 每个结构都不一样;
    • 每个数据也不一样,没有交集;
    • 所有并集是全量数据;
  3. 场景:系统绝对并发量上来了,并且可以抽象出单独的业务模块。
  4. 分析:到这一步,基本上就可以服务化了。例如,随着业务的发展一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化。

垂直分表

image-20200727193323376

  1. 概念:以字段为依据,按照字段的活跃性,将中字段拆到不同的(主表和扩展表)中。
  2. 结果:
    • 每个结构都不一样;
    • 每个数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据;
    • 所有并集是全量数据;
  3. 场景:系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。
  4. 分析:可以用列表页和详情页来帮助理解。垂直分表的拆分原则是将热点数据(可能会冗余经常一起查询的数据)放在一起作为主表,非热点数据放在一起作为扩展表。这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获得全部数据就需要关联两个表来取数据。但记住,千万别用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。关联数据,应该在业务Service层做文章,分别获取主表和扩展表数据然后用关联字段关联得到全部数据。

分库分表工具

  1. sharding-sphere:jar,前身是sharding-jdbc;
  2. TDDL:jar,Taobao Distribute Data Layer;
  3. Mycat:中间件。

分库分表步骤

  1. 根据容量(当前容量和增长量)评估分库或分表个数
  2. 选key(均匀)
  3. 分表规则(hash或range等)
  4. 执行(一般双写)
  5. 扩容问题(尽量减少数据的移动)

分库分表的问题

非Shard key的查询问题

  1. 除了Shard key只有一个非Shard key作为条件查询

    对于Shard key字段上的查询,我们可以直接路由到库进行查询,而对于非Shard key的查询,由于不知道数据落在哪个库上,往往需要遍历所有库,当分库数量多起来,性能会显著降低,我们该如何高效的实现查询呢?

    • 方案一:映射法

    拿用户体系为例,我们选择了user_id 做Shard key,当业务想通过user_name进行查询,先查找映射表,通过user_name查出user_id,再通过路由去查用户库。

    image-20200727193955555

    • 基因法

    image-20200727194322866

    注:写入时,基因法生成user_id,如图。关于xbit基因,例如要分8张表,23=8,故x取3,即3bit基因。根据user_id查询时可直接取模路由到对应的分库或分表。根据user_name查询时,先通过user_name_code生成函数生成user_name_code再对其取模路由到对应的分库或分表。id生成常用snowflake算法

    • 方案三:异构数据库

    对于用户模块,日常还会存在一些运营类查询,一般通过年龄、性别、登录时间、注册时间等条件进行查询。运营类查询与业务类查询相比,存在以下特点:

    • 基本上是批量分页的查询
    • 访问量低
    • 可用性要求不高
    • 一致性要求不那么严格

    这种类型的查询,我们可以通过数据冗余,将数据异构在Hive或ES内,供运营后台查询

  2. 端上除了partition key不止一个非partition key作为条件查询

    • 映射法

image-20200727194357647

  • 冗余法

image-20200727194458633

注:按照order_id或buyer_id查询时路由到db_o_buyer库中,按照seller_id查询时路由到db_o_seller库中。

  1. 后台除了partition key还有各种非partition key组合条件查询

image-20200727194632475

非Shard key跨库跨表分页查询问题

基于水平分库分表,拆分策略为常用的hash法。

注:用**NoSQL法**解决(ES等)。

扩容问题

  1. 水平扩容表(双写迁移法)

image-20200727195055817

第一步:(同步双写)修改应用配置和代码,加上双写,部署;
第二步:(同步双写)将老库中的老数据复制到新库中;
第三步:(同步双写)以老库为准校对新库中的老数据;
第四步:(同步双写)修改应用配置和代码,去掉双写,部署;

注:双写是通用方案。

扩容和部署的问题

  1. 停机部署法

大致思路就是,挂一个公告,半夜停机升级,然后半夜把服务停了,跑数据迁移程序,进行数据迁移。
步骤如下:
(1)出一个公告,比如“今晚00:00~6:00进行停机维护,暂停服务”
(2)写一个迁移程序,读db-old数据库,通过中间件写入新库db-new1db-new2,具体如下图所示

img

(3)校验迁移前后一致性,没问题就切该部分业务到新库。

ps:这里教大家一些技巧啊,如果你真的没做过分库分表,又想吹一波,涨一下工资,建议答这个方案。因为这个方案比较low,low到没什么东西可以深挖的,所以答这个方案,比较靠谱。
比如:你刚才刚好有提到分库分表的相关问题,我们当时部署的时候,先停机。然后半夜迁移数据,然后第二天将流量切到新库,这种方案太累,不知道贵公司有没有什么更好的方案?
那么这种情况下,面试官会有两种回答。第一种,面试官硬着头皮随便扯。第二种,面试官真的做过,据实回答。记住,面试官怎么回答的不重要。重点的是,你这个问题出去,会给面试官一种错觉:“这个小伙子真的做过分库分表。”

  1. 双写部署法(一)

这个就是不停机部署法,这里我需要先引进两个概念:历史数据增量数据
假设,我们是对一张叫做test_tb的表进行拆分,因为你要进行双写,系统里头和test_tb表有关的业务之前必定会加入一段双写代码,同时往老库和新库中写,然后进行部署,那么
**历史数据:**在该次部署前,数据库表test_tb的有关数据,我们称之为历史数据。
**增量数据:**在该次部署后,数据库表test_tb的新产生的数据,我们称之为增量数据。
然后迁移流程如下
(1)先计算你要迁移的那张表的max(主键)。在迁移过程中,只迁移db-oldtest_tb表里,主键小等于该max(主键)的值,也就是所谓的历史数据。
这里有特殊情况,如果你的表用的是uuid,没法求出max(主键),那就以创建时间作为划分历史数据和增量数据的依据。如果你的表用的是uuid,又没有创建时间这个字段,我相信机智的你,一定有办法区分出历史数据和增量数据。
(2)在代码中,与test_tb有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。**需要注意的是,**只发写请求的sql,只发写请求的sql,只发写请求的sql。重要的事情说三遍!
原因有二:
(1)只有写请求的sql对恢复数据才有用。
(2)系统中,绝大部分的业务需求是读请求,写请求比较少。
注意了,在这个阶段,我们不消费消息队列里的数据。我们只发写请求,消息队列的消息堆积情况不会太严重!
(3)系统上线。另外,写一段迁移程序,迁移db-oldtest_tb表里,主键小于该max(主键)的数据,也就是所谓的历史数据。
上面步骤(1)~步骤(3)的过程如下

img

等到db-old中的历史数据迁移完毕,则开始迁移增量数据,也就是在消息队列里的数据。
(4)将迁移程序下线,写一段订阅程序订阅消息队列中的数据
(5)订阅程序将订阅到到数据,通过中间件写入新库
(6)新老库一致性验证,去除代码中的双写代码,将涉及到test_tb表的读写操作,指向新库。
上面步骤(4)~步骤(6)的过程如下

img

这里大家可能会有一个问题,在步骤(1)~步骤(3),系统对历史数据进行操作,会造成不一致的问题么?
OK,不会。这里我们对delete操作和update操作做分析,因为只有这两个操作才会造成历史数据变动,insert进去的数据都是属于增量数据。
(1)对db-oldtest_tb表的历史数据发出delete操作,数据还未删除,就被迁移程序给迁走了。此时delete操作在消息队列里还有记录,后期订阅程序订阅到该delete操作,可以进行删除。
(2)对db-oldtest_tb表的历史数据发出delete操作,数据已经删除,迁移程序迁不走该行数据。此时delete操作在消息队列里还有记录,后期订阅程序订阅到该delete操作,再执行一次delete,并不会对一致性有影响。
update的操作类似,不赘述。
双写部署法(二)
上面的方法有一个硬伤,注意我有一句话
(2)在代码中,与test_tb有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。
大家想一下,这么做,是不是造成了严重的代码入侵。将非业务代码嵌入业务代码,这么做,后期删代码的时候特别累。
有没什么方法,可以避免这个问题的?
有的,订阅binlog日志。关于binlog日志,我尽量下周写一篇《研发应该掌握的binlog知识》,这边我就介绍一下作用
记录所有数据库表结构变更(例如CREATE、ALTER TABLE…)以及表数据修改(INSERT、UPDATE、DELETE…)的二进制日志。binlog不会记录SELECT和SHOW这类操作,因为这类操作对据本身并没有修改。
还记得我们在**双写部署法(一)**里介绍的,往消息队列里发的消息,都是写操作的消息。而binlog日志记录的也是写操作。所以订阅该日志,也能满足我们的需求。
于是步骤如下
(1)打开binlog日志,系统正常上线就好
(2)还是写一个迁移程序,迁移历史数据。步骤和上面类似,不啰嗦了。
步骤(1)~步骤(2)流程图如下

img

(3)写一个订阅程序,订阅binlog(mysql中有canal。至于oracle中,大家就随缘自己写吧)。然后将订阅到的数据通过中间件,写入新库。
(4)检验一致性,没问题就切库。
步骤(3)~步骤(4)流程图如下

img

怎么验数据一致性
这里大概介绍一下吧,这篇的篇幅太长了,大家心里有底就行。
(1)先验数量是否一致,因为验数量比较快。
至于验具体的字段,有两种方法:
(2.1)有一种方法是,只验关键性的几个字段是否一致。
(2.2)还有一种是 ,一次取50条(不一定50条,具体自己定,我只是举例),然后像拼字符串一样,拼在一起。用md5进行加密,得到一串数值。新库一样如法炮制,也得到一串数值,比较两串数值是否一致。如果一致,继续比较下50条数据。如果发现不一致,用二分法确定不一致的数据在0-25条,还是26条-50条。以此类推,找出不一致的数据,进行记录即可。

事务问题

方案一:使用分布式事务

  • 优点: 交由数据库管理,简单有效
  • 缺点:性能代价高,特别是shard越来越多时

方案二:由应用程序和数据库共同控制

  • 原理:将一个跨多个数据库的分布式事务分拆成多个仅处 于单个数据库上面的小事务,并通过应用程序来总控 各个小事务。
  • 优点:性能上有优势
  • 缺点:需要应用程序在事务控制上做灵活设计。如果使用 了spring的事务管理,改动起来会面临一定的困难。

跨节点Join的问题

只要是进行切分,跨节点Join的问题是不可避免的。但是良好的设计和切分却可以减少此类情况的发生。解决这一问题的普遍做法是分两次查询实现。在第一次查询的结果集中找出关联数据的id,根据这些id发起第二次请求得到关联数据。

ID的问题

一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的ID无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得ID,以便进行SQL路由.

  1. UUID
  2. Twitter的分布式自增ID算法Snowflake

跨分片的排序分页聚合的问题

这些是一类问题,因为它们都需要基于全部数据集合进行计算。多数的代理都不会自动处理合并工作。解决方案:与解决跨节点join问题的类似,分别在各个节点上得到结果后在应用程序端进行合并。和join不同的是每个结点的查询可以并行执行,因此很多时候它的速度要比单一大表快很多。但如果结果集很大,对应用程序内存的消耗是一个问题。

一般来讲,分页时需要按照指定字段进行排序。当排序字段就是分片字段的时候,我们通过分片规则可以比较容易定位到指定的分片,而当排序字段非分片字段的时候,情况就会变得比较复杂了。为了最终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。如下图所示:

img\

上面图中所描述的只是最简单的一种情况(取第一页数据),看起来对性能的影响并不大。但是,如果想取出第10页数据,情况又将变得复杂很多,如下图所示:

img

有些读者可能并不太理解,为什么不能像获取第一页数据那样简单处理(排序取出前10条再合并、排序)。其实并不难理解,因为各分片节点中的数据可能是随机的,为了排序的准确性,必须把所有分片节点的前N页数据都排序好后做合并,最后再进行整体的排序。很显然,这样的操作是比较消耗资源的,用户越往后翻页,系统性能将会越差。
那如何解决分库情况下的分页问题呢?有以下几种办法:

  • 如果是在前台应用提供分页,则限定用户只能看前面n页,这个限制在业务上也是合理的,一般看后面的分页意义不大(如果一定要看,可以要求用户缩小范围重新查询)。
  • 如果是后台批处理任务要求分批获取数据,则可以加大page size,比如每次获取5000条记录,有效减少分页数(当然离线访问一般走备库,避免冲击主库)。
  • 分库设计时,一般还有配套大数据平台汇总所有分库的记录,有些分页查询可以考虑走大数据平台。

分库的数量

分库数量首先和单库能处理的记录数有关,一般来说,Mysql 单库超过5000万条记录,Oracle单库超过1亿条记录,DB压力就很大(当然处理能力和字段数量/访问模式/记录长度有进一步关系)。
在满足上述前提下,如果分库数量少,达不到分散存储和减轻DB性能压力的目的;如果分库的数量多,好处是每个库记录少,单库访问性能好,但对于跨多个库的访问,应用程序需要访问多个库,如果是并发模式,要消耗宝贵的线程资源;如果是串行模式,执行时间会急剧增加。
最后分库数量还直接影响硬件的投入,一般每个分库跑在单独物理机上,多一个库意味多一台设备。所以具体分多少个库,要综合评估,一般初次分库建议分4-8个库。

总结

  1. 分库分表的第一原则:能不拆分尽量不拆分。分库分表在解决性能和扩展问题的同时,会为维护工作和业务逻辑带来一系列复杂性和性能损耗,尽量避免过度设计,过早优化。

  2. 选key很重要,既要考虑到拆分均匀,也要考虑到非partition key的查询。

  3. 只要能满足需求,拆分规则越简单越好。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值