写点什么

分布式套路之分库分表漫谈

发布于: 2021 年 01 月 10 日
分布式套路之分库分表漫谈

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


  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.数据迁移流程以及如何保证数据一致性

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


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



先修改代码,加入双写分库分表代码;进行上线

开始进行数据双写,同步增量数据 ;双写,主要目的是追上实时数据,给全量同步数据一个 deadline,保证从这个时间之后的数据都是完整的(同时,通过异步数据完整性校验程序去校验数据完整性,但是如果我们能够保证双写可靠性,这个对比可做可不做。最好还是做一下)

全量历史数据同步,并校验数据完整性;一般全量数据同步,不用同步写的方式,原因在于同步写入一方面代码耦合度高,一方面是对系统有影响。所以我们往往通过异步方式进行写入,这个过程后文有图进行说明;

去掉双写代码,将查询分库分表的逻辑全量;通过开关切换,在全量数据同步完成之后切换到全量读写分库分表逻辑即可。此时老的逻辑已经没有请求路由过去了,我们只需要找个发版窗口把老逻辑下线就可以,此时线上已经完全迁移到分库分表的代码流程。


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


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


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


发布于: 2021 年 01 月 10 日阅读数: 40
用户头像

领取资料添加小助理vx:bjmsb2020 2020.12.19 加入

Java领域;架构知识;面试心得;互联网行业最新资讯

评论 (1 条评论)

发布
用户头像
分布式套路之分库分表漫谈
2021 年 01 月 10 日 17:59
回复
没有更多了
分布式套路之分库分表漫谈