既然是“漫谈分库分表”,那么我们需要确定我们要谈什么,不谈什么。

  1. 首先,我们不讨论具体的分库分表框架的实现和源码,这不是我们讨论的范围。
  2. 我们讨论的是思路,主要讨论如何分库分表的套路,有什么坑,有什么心得,不针对具体的细节进行展开式讨论。当然我自己的能力有限,只是希望能够抛砖引玉。
  3. 我们要明确,分库分表,并不是一个银弹,它只是我们针对MySQL单机性能不够的情况下,想要节约成本的一种方式。对于boss来说,既想要想要节约成本,又想要支撑业务,提供稳定持久度性能。

程序员发挥出聪明才智,绞尽脑汁,日复一日的努力与实践,最终产生出主要的两种方式:

  1. agent嵌入式模式,用一个jar包,集成到我们的代码里,在代码里通过路由规则,分片键方式进行分库分表,属于嵌入式方式。
  2. cs模式(客户端-服务端模式),提供一个三方组件, 如: mycat,sharding-sphere中的proxy方式,类似于mycat;存在中心化,需要保证三方组件的高可用。

如果有更好的技术选型,我们宁愿不用分库分表,因为它本身就是一个复杂的解决方案。只是一种折中,更合适的是NewSQL、商业化数据库(比方说,Oracle,在大部分场景下,性能足够用,但是费用高昂)。

如果真的有一天,出现了一个优秀的、经济的newSQL, 比方说 oceanbase,tidb,那么我们基本上可以告别分库分表。

我们之所以选择使用分库分表策略,根本上还是因为,一方面是因为我们的使用成本不能太高;一方面,单机DB数据库性能不够了;一方面,newSQL当前还不成熟,太贵,不敢用。

分库分表用的厂家挺多的,有丰富的开源框架,有社区,还有成熟的案例,所以我们采用,

直接原因在于,阿里站台了,我们国内的风气是,阿里用啥我用啥,阿里怎么做我这么 跟风严重。我的想法是,我们还是有自己的技术前瞻性一些看法,最好不要唯阿里,唯技术。

说了这么多,我们回归正题,开始看问题。

1. 只做分表可以吗?还是必须要分表又分库,如果是分库的话 库是在多个服务器上吗?这个怎么来考虑

我想说,还是要看业务规模,既看当前的业务规模,也看未来3-5年的业务发展趋势。

涉及到技术选型,我们的宗旨是,永远选择最适合当下业务的、成本最低的,收益最高的,合适的就是最好的。

我们选择的方案最好是技术团队刚刚好能够hold住的选型。如果选型已经不适合当前的业务发展,那么大可以换套更适合的。这个本来就是事物发展的必然规律。

要么,业务还没发展到一个更高层次,就已经GG了,那么刚刚好,不用浪费钱买更好的设施,刚好止损;

要么,就是当前的方案确实不够用了,我们换了一套更牛X的,虽然说这样会花更多的钱,请更多的人,但是,这不正合我们的心意么,我们的目的本身就是通过合适的技术架构,更加优秀的代码,支持业务发展。

一句话总结就是,既然不得不花钱,那该花就花吧。

分场景讨论

一图胜千言,我们分别看看这两种场景。

对于 离线数据分析场景

只分表是够的,因为你主要用来分析数据,分析数据完成之后的数据就可以删掉了。异步任务删掉若干月/天的数据。

 

对于 实时业务系统

如果是一个分布式的业务系统 2C,需要承载巨量的流量的,建议 分库分表 同时考虑。

 

分库分库的前提,预估业务量

分库分表的前提,是预估业务量,我们提供一个经验值,不代表最合适的,只是一个定性的分析:

QPS 500-1000以下,   那么采用主从读写分离,基本上足够支撑业务了;

QPS 1000-10000,考虑分布分表是一个比较合适的事情 

12000TPS 30000QPS 32库 1024表 
1000多万 16000QPS 16库512表

本质上来说:分库分表是一锤子买卖,前期的设计很重要,决定了后期扩容以及数据迁移的难度。在前期设计的时候,大概率我们需要做好未来3-5年的规划,短的话需要做1-2年的规划,根据规划来确定是不是要分库分表,以及分多少库、多少表。

回到问题本身,这个主要取决于当前的业务量,以及业务量的增速。

我们根据这几个维度,给出一组公式:

某年数据增量M = (1 + 数据年增速K)^ n  * 初始数据量 N

第一年增量 M1 = (1+k)   * N 
第二年增量 M2 = (1+K)^2 * N
第三年增量 M3 = (1+K)^3 * N

三年数据总量 M' = N + m1 + m2 + m3

我们就以单表承载1000万数据来算,一共要有几张表,当前不一定是1000w,2000w-5000w都可以,这个首先是一个经验值,其次还需要定量分析。

定量的分析,就需要进行压测。我们需要针对你的线上的配置,用一个库的实例去压测,压出你的这个配置下,在不影响系统吞吐量的前提下,单表的最大容量,压测是一个稳妥的环节,能够在前期很好的指导我们进行设计。

我们接着讨论,什么时候需要分库,必须要保证每个数据库都是一个独立的实例么?

并不是,我们还是要具体问题具体分析。

如果是开发环境,也就是研发RD自己写代码用的库,那么多套库在一台机器上也可以,毕竟开发环境没有并发量,最多拿来开发,只要不用来压测就没啥问题。

如果是线上环境,除了要将库部署到多台机器,还得考虑读写分离,以及库的高可用。线上线下的主要区别在于,线上有高可用的要求,而线下不需要

思考一下,两者区别是什么,区别就在于成本的控制

我们给出结论,具体什么时候要把数据库部署到一台机器实例,还是要看场景,看成本,看自己需不需要。具体问题具体分析。

2. 路由键怎么生成?用雪花算法可以吗?如果原来的数据库主键是自增的,没有业务唯一约束,如果迁移之后,原先的数据怎么在分库分表中进行路由

好问题。

首先说,路由键怎么生成?

本质上,这是一个如何实现一个可靠的分布式发号器的问题。我们只说思路,因为展开说都能但单独说好半天了。

思路:

对于某些框架而言,他们有自己的主键生成器,比如说shardingSphere/ShardingJDBC 类SnowFlake算法;

  1. UUID:字符串形式,确实是唯一,但是可读性差,不好做数学计算,不直观,比较长,占用空间大
  2. SNOWFLAKE:可以用,也可以用改进leaf,leaf本身就是一套完善的分布式发号器,自己也有高可用保障。

当然还有别的方式:

因为既然已经做了分库分表,大概率你的系统也是分布式的吧,那么用进程内的发号不是一个理想的方式。

如果要简单实现一种分布式发号服务,我们可以利用 redis increment 实现一套发号器,也可以借助数据库的自增唯一id来做,但是我们还是需要自己进行开发,实现一个发号系统。

简单的上一个图,表达一下思路,这块儿内容之后会单独写文章来讲。

 

总结一下就是,本质上,这是一个如何实现一个可靠的分布式发号器的问题。

所以得依赖某个具体的分布式发号机制 这个问题不用纠结,关注一下最终的选型就好,多进行权衡。

3. 如果本身是一个单库,并且没有路由键,完全拿主键当唯一标识了,我分库分表怎么玩?

很简单,你原来的唯一标识是什么,分库分表之后还用这个就行了。

但是,因为本身没有一个业务属性的键,所以建议在进行数据迁移之后,加入一个业务属性的自然主键,并且大概率你需要配置一下新的路由规则。

具体的过程为:

  1. 迁移数据
  2. 更改路由配置 指定一个新的查询规则,分库分表的路由规则
  3. 改代码,把代码中涉及到C R U D 的代码,比如说DAO、repository中包含的代码,代码都加上路由规则,简单的说你还是可以用原来的id去执行查询 、插入 、删除的,但是主要的改动点就在于你需要有一个路由规则。

我们说,数据库迁移到分不分表的核心:是保证数据的完整性,代码该重构就重构,很难有一个全面的不需要改代码的方案,我们只能折中权衡,降低复杂度。

原先的主键id,迁移到分库分表新库中,已经不是连续的了,但是还需要保证unique,新的数据库表中的自增主键还需要有,但是没有业务属性了,之所以分库分表之后还需要有自增主键,主要在于提升插入效率,查询效率。通过主键索引树,进行回表操作。

相当于你原先用了自增id是有业务属性的,这里说句题外话,请尽量不要使用自增主键代表业务含义

3. 分片键怎么选择

我们的答案依旧不能给出一个准确的说法,我只能说,要根据业务场景的要求去选择。

这么说太笼统了,我们通过几个例子来表达一下。

对于用户表,使用用户唯一标识, 如:userId作为分片键;
对于账户表,使用账户唯一标识,如:accountId作为分片键;
对于订单表,使用订单唯一标识, 如:orderId作为分片键;
对于商家相关信息表,使用商家唯一标识, 如:merchantId作为分片键;
......

如果我们要查一个用户的订单,那么我们应该用userId去路由表,插入订单到订单表,保证一个用户的所有订单都能够分布在一个表分片上。这样做能够很好的避免引入分布式事务。

如果说,维度不是用户,而是其他维度,比方说,我们想查询某个商家的所有用户的订单

那么我们就应该用商家的merchantId也去存一份数据,路由的时候用商家id去路由,只要是这个商家的用户订单,我们写入到商家的订单表里,那么对于商家所属的订单,我们就可以从某个分片上获取到。

用一个图表达,能够很明确地体现上述的说明内容:

对于用户而言,分片键作用方式如下图:
usertable.png

对于商家而言,分片键作用方式如下图:
merchanttable.png

所以我们的结论就是:要根据业务场景的要求去选择,具体问题具体分析,尽量保证不引入分布式事务,提升查询效率。

补充一句,对于主流的做法,如果需要有复杂查询,要么依据不同维度去进行双写,要么直接通过引入异构的方式去查询,比方说使用elastic search,或者使用hive等方式。

4.批量插入数据的时候,会往各个分库去插,在实际业务中是否要做分布式事务

第三个问题或多或少也提到了这个问题的答案。

我们在落地分库分表的过程中,要尽量避免引入分布式事务

因为从上面第三个问题,你会发现,如果我们有路由键,问题就简单的多了,我们大部分情况下不需要引入分布式事务,但是如果没有就很痛苦。

对于乱序插入且需要保证插入事务性的场景,就需要分布式事务。但是这样做效率太低,也不合适。

首先乱序插入的场景并不多,其次如果引入分布式事务,那么事务的力度也不小,而且对于插入的性能有着显著的影响。不是最佳的方式。

我的建议就是,还是基于最终一致性去做,否则引入分布式事务,太影响效率了,而且也会增加系统的复杂度,我觉得我们设计系统的宗旨就是,能不用复杂的方案就不用,有时间喝喝茶,干点别的何乐而不为呢。

所以这个问题的结论就是:尽量避免分布式事务,如果不得不引入,需要尽量缩小事务的范围和力度。通过折中,多去考虑一下方案的可行性,
性能很重要,没有分布式事务也能做,怎么做,就是通过最终一致性。

但是,如果你说 “我就是避免不了分布式事务啊,那咋办嘛”。那就用吧,若无必要,勿增实体。不得不用,就用,没什么好说的。

5.如果一个库有很多张表,对一张表进行分库分表了,此时不分库不分表的表怎么放置, 是否指定到分库里某一个库里面?

本质上:这是非分库分表的数据与分库分表数据的分布的一个问题。

实际上,分库分表中间件往往都有对应功能,这个功能往往叫做默认路由规则,怎么理解呢?

就是说,对于没有分库分表的这些表,走默认路由规则 就行了,这样的话始终会路由到default DataSource上去。

相当于是一个白名单。找一下中间件的文档,看看默认路由规则怎么配,基本上中间件都考虑这个问题了,对于ShardingSphere而言,一个配置样例如下:

CustomerNoShardingDBAlgorithm
    default-table-strategy: (缺省表分区策略)
        complex:
        sharding-columns: db_sharding_id
        algorithm-class-name: com.xxx.XxxClass
    tables:
        ops_account_info: (必须要配置这个,才能使用缺省分表策略)
        actual-data-nodes: db-001.ops_account_info

详细的举个例子,比方说:

一个服务在原有的数据库进行分库(比如user库分为了user01,user02)的时候,是把不分表的表强制路由走某一个数据库吗(比如把不分表的表都路由到user01)?

这里说到的本质就是: 默认路由规则,我们只需要配置某些表走默认路由规则就行了,比方说,我们现在有user 表 order表,config表,其中user表、 order分库分表,而config没有分库分表。

那么我们只需要把config表放在user库的0库,1库,2库,随便某个位置,

放好之后,我们只需要在分库分表中间件的配置文件中配置默认路由规则,把config表特殊配置一下,只要查config表,就走到这个指定的库上去。

其他的也类似 ,只要有这种需求,就增加对应的配置。

一定要显式告诉中间件,哪些表不走路由规则,并且要告诉它,这些表具体放在哪儿,
最好是放在请求量不大的库里,或者说单独搞一个库也可以,这个库放的都是不进行分库分表的表,
并配置不走路由规则就完事儿了,其实还是默认路由规则。

为什么这么做呢?有什么意图呢?

我的理解就是:之所以我们分库分表的原因,就是因为请求很大需要降低并发度;而对于请求频率小的表,我们可以不分库分表还是通过单表方式使用,那么就可以配置为默认路由规则就好。

8.数据迁移流程以及如何保证数据一致性

简单的概括,数据迁移依赖于数据的双写;数据一致性,依赖于数据完整性校验。

对于迁移而言,我们有以下步骤:

 

  1. 先修改代码,加入双写分库分表代码;进行上线
  2. 开始进行数据双写,同步增量数据 ;双写,主要目的是追上实时数据,给全量同步数据一个deadline,保证从这个时间之后的数据都是完整的(同时,通过异步数据完整性校验程序去校验数据完整性,但是如果我们能够保证双写可靠性,这个对比可做可不做。最好还是做一下)
  3. 全量历史数据同步,并校验数据完整性;一般全量数据同步,不用同步写的方式,原因在于同步写入一方面代码耦合度高,一方面是对系统有影响。所以我们往往通过异步方式进行写入,这个过程后文有图进行说明;
  4. 去掉双写代码,将查询分库分表的逻辑全量;
    通过开关切换,在全量数据同步完成之后切换到全量读写分库分表逻辑即可。此时老的逻辑已经没有请求路由过去了,我们只需要找个发版窗口把老逻辑下线就可以,此时线上已经完全迁移到分库分表的代码流程。

最后我想说,一定要回归,一定要回归,一定要回归!!!

原文链接:http://wuwenliang.net/2021/01/09/分布式套路之分库分表漫谈/

如果觉得本文对你有帮助,可以关注一下我公众号,回复关键字【面试】即可得到一份Java核心知识点整理与一份面试大礼包!另有更多技术干货文章以及相关资料共享,大家一起学习进步!