# 数据分库分表设计
分库分表,是企业里面毕竟常见的针对高并发、数据量大的场景下的一种技术优化方案,也是一个非常高频的面试题。 但是,因为很多人其实并没有非常丰富的分库分表的经验,所以能把这个问题回答的比较好的人其实还挺少的。
# 分库分表的背景
互联网业务的一个特点就是用户量巨大,BAT等头部公司都是亿级用户,产生的数据规模也飞速增长,传统的单库单表架构不足以支撑业务发展,存在下面的性能瓶颈:
- 读写的数据量限制:数据库的数据量增大会直接影响读写的性能,比如一次查询操作,扫描 5 万条数据和 500 万条数据,查询速度肯定是不同的。
- 数据库连接限制:数据库的连接是有限制的,不能无限制创建,比如 MySQL 中可以使用 max_connections 查看默认的最大连接数,当访问连接数过多时,就会导致连接失败。以电商为例,假设存储没有进行分库,用户、商品、订单和交易,所有的业务请求都访问同一个数据库,产生的连接数是非常可观的,可能导致数据库无法支持业务请求。
使用数据库连接池,可以优化连接数问题,但是更好的方式是通过分库等手段,避免数据库连接成为业务瓶颈。除了这些,如果不进行数据库拆分,大量数据访问都集中在单台机器上,对磁盘 IO、CPU 负载等都会产生很大的压力,并且直接影响业务操作的性能。
TIP
关于 MySQL 单库和单表的数据量限制,和不同的服务器配置,以及不同结构的数据存储有关,并没有一个确切的数字。这里参考阿里巴巴的《Java 开发手册》中数据库部分的建表规约:单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。 基于阿里巴巴的海量业务数据和多年实践,这一条数据库规约,可以认为是数据库应用中的一个最佳实践。也就是在新业务建表规划时,或者当前数据库单表已经超过对应的限制,可以进行分库分表,同时也要避免过度设计。因为分库分表虽然可以提高性能,但是盲目地进行分库分表只会增加系统的复杂度。
# 分库分表的原理
分库分表,顾名思义,就是将原本存储于单个数据库上的数据拆分到多个数据库,把原来存储在单张数据表的数据拆分到多张数据表中,实现数据切分,从而提升数据库操作性能。分库分表的实现可以分为两种方式:垂直切分和水平切分。
# 垂直切分
垂直拆分一般是按照业务和功能的维度进行拆分,把数据分别放到不同的数据库中。
垂直分库针对的是一个系统中对不同的业务进行拆分,根据业务维度进行数据的分离,剥离为多个数据库。比如电商网站早期,商品数据、会员数据、订单数据都是集中在一个数据库中,随着业务的发展,单库处理能力已成为瓶颈,这个时候就需要进行相关的优化,进行业务维度的拆分,分离出会员数据库、商品数据库和订单数据库等。
垂直分表是针对业务上的字段比较多的大表进行的,一般是把业务宽表中比较独立的字段,或者不常用的字段拆分到单独的数据表中。比如早期的商品表中,可能包含了商品信息、价格、库存等,可以拆分出来价格扩展表、库存扩展表等。
# 水平切分
水平拆分是把相同的表结构分散到不同的数据库和不同的数据表中,避免访问集中的单个数据库或者单张数据表,具体的分库和分表规则,一般是通过业务主键,进行哈希取模操作。
例如,电商业务中的订单信息访问频繁,可以将订单表分散到多个数据库中,实现分库;在每个数据库中,继续进行拆分到多个数据表中,实现分表。路由策略可以使用订单 ID 或者用户 ID,进行取模运算,路由到不同的数据库和数据表中。
# 分片算法实现
# 范围(RANGE)
优点:对查询友好,基本上只要加上一个时间范围的查询条件,原来该怎么查,分片之后还可以怎么查。扩容简单,提前建好库、表就好。
缺点:容易产生热点问题,大部分读和写都访会问新的数据。
# 哈希
优点:比较容易把数据和查询均匀地分布到所有分片中。
缺点:扩容麻烦、迁移数据时每次都需要重新计算 Hash 值分配到不同的库和表。
# 一致性哈希
优点:通过虚拟节点方式能保证数据较均匀的分散落在不同的库、表中,并且新增、删除节点不影响其他节点的数据,高可用、容灾性强。
RANGE 和哈希相对简单,一般可以满足大部分要求了。一致性哈希相对复杂,特殊情况下也可以使用。
# 分库分表的工具
在选定了 Sharding Key 和分片算法算法之后,如何把这些功能给实现出来,就需要用到一个分库分表的工具了。目前市面上比较不错的分库分表的开源框架主要有三个,分别是ShardingSphere、TDDL 和 Mycat。根据分库分表方案中实施切片逻辑的层次不同,分为客户端分片和代理分片,ShardingSphere 、TDDL 是客户端分片,Mycat 是代理层分片。
# ShardingSphere
ShardingSphere 的前身是当当开源的 Sharding-JDBC,目前更名为 ShardingSphere,并且已经加入 Apache 基金会。ShardingSphere 在 Sharding-JDBC 的基础上,额外提供了 Sharding-Proxy,以及正在规划中的 Sharding-Sidecar。其中 Sharding-JDBC 用来实现分库分表,另外也添加了对分布式事务等的支持。关于 ShardingSphere 的具体应用,
缺点是会有一定的代码开发工作量,对业务有一些侵入性。好处是程序员对分库分表逻辑的把控会更强,如果发生故障,排查问题会比较容易。
# Mycat
在应用层和数据库层增加一个代理层,把分片的路由规则配置在代理层,优点:对项目透明,解偶。缺点:缺点在于需要部署,自己运维一套中间件,运维成本高。
# TDDL
TDDL 是淘宝开源的一个用于访问数据库的中间件,它集成了分库分表,读写分离,权重调配,动态数据源配置等功能。
# 分库分表带来的影响
# 跨节点数据库Join关联查询
- 字段冗余:把需要关联的字段放入主表中。
- 数据抽象:通过ETL等将数据汇合聚集,生成新的表。
- 全局表:比如一些基础表可以在每个数据库中都放一份。
- 应用层组装:将基础数据查出来,通过应用程序计算组装。
# 分库操作带来的分布式事务问题
单数据库可以用本地事务搞定,多数据库就只能通过分布式事务解决了。分布式事务 (opens new window)
# 执行的SQL排序、翻页、函数计算问题
先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终得到结果。
# 数据库全局主键重复问题
单库单表可以使用 id 自增作为主键,分库分表了之后就不行了,会出现 id 重复。
# 容量规划,分库分表后二次扩容问题
平滑迁移 单库单表的系统需要在用户无感知不停机的情况下迁移到分库分表的系统,步骤如下:
- 代码准备。在服务层对订单表进行增删改的地方,要同时操作新库(分库分表后的数据库表)和老库;准备迁移程序,用于做老数据迁移;准备校验程序,用于校验新库和老库的数据是否一致。
- 开启双写,老库和新库同时写入。
- 执行迁移程序读老库数据写新库,老库读出来的数据在新库里没有或者是比新库的数据新则写入新库(根据 update_time 字段判断)。
- 第 3 步完成后,通过校验程序检验数据,看新老库数据是否一致。有问题的数据记录下来单独修复。
- 数据校验没问题后,开启双读,起初给新库放少部分流量,新库和老库同时读取。
- 然后再逐步将读流量切到新库,相当于灰度上线的过程。遇到问题可以及时把流量切回老库。
- 读流量全部切到新库后,关闭老库写入(可以在代码里加上热配置开关),只写新库。
- 迁移完成,后续可以去掉双写双读相关无用代码。
动态扩容 时间久了,分库分表后的系统可能需要进行扩容。可以采用双倍扩容策略,避免数据迁移。扩容前每个节点的数据, 有一半要迁移至一个新增节点中。假设已有 2 个节点 A、B,要双倍扩容至 A、A2、B、B2 4个节点,具体操作如下:
- 新增两个数据库 A2、B2 作为从库,设置主从同步关系为:A => A2、B => B2,直至主从数据同步完毕。
- 调整分片规则:原来是 ID%2=0 => A,ID%2=1 => B 改为 ID%4=0 => A,ID%4=2 =>A2,ID%4=1 => B,ID%4=3 => B2。
- 解除数据库实例的主从同步关系。
- 此时,四个节点的数据都已完整,只是有冗余(多存了和自己配对的节点的那部分数据),择机清除即可。
# 分库分表技术选型问题
业务中实现分库分表,需要自己去实现路由规则,实现跨库合并排序等操作,具有一定的开发成本,可以考虑使用开源的分库分表中间件。 这里比较推荐 Apache ShardingSphere,另外也可以参考淘宝的 TDDL 等。
# 有一个订单表,每天新增的订单数据是500万。
在实际的业务场景中,存在两个比较多的查询:1.根据订单d查询订单信息2.根据用户id查询订单列表
- 分库分表设计
- 防止数据库容量满了的情况
- 防止单表数据量过大的问题
如果出现了以上问题,容量扩充数据迁移多和扩客容的成本会非常高。
1天500W的增量,一年就是18亿的数据库,我们按照保留两年的热数据量,大概是接近40亿。然后我门做些空间的预留。就算50亿的数据。 可以按照32个库,每一个库32张表来设计。一共是1024张表。一个表里面存放500w数据库。这样就可以满足50亿的数据容量的规划设计。 使用orderid作为分片键。采用一致性hash算法来进行路由。
在性能层面:假设每个库正常写入的并发量是1000,那32个库可以承载32000的并发量。如果每个库的写性能优化到1500,着这个方案能支持接近5w每秒的写并发。 这样的设计已经能够满足大部分互联网公司的并发需求了。
- 业务需要根据用户id查找,而用户id是分片键导致查询的复杂。
使用基因算法来确保用户ID对应的订单ID路由到同一个库或者同一个表.在生成订单ID的时候把用户ID的基因片段拼接到订单ID中,从而保证不管是通过订单ID查询,还是根据用户ID查询都能路由到同一个表
虽然我们设计了32个库和32个表,但是也只能存诸50亿的数据,也就是差不多三年时间,那三年之后这个方案就不满足了,对于订单的类的数据我们频繁就是访问一年一年,对于一年以上的数据就不怎么关心 因此设计数据分冷热分离数据方案。热数据保持一年,一年以后的数据迁移到历史库,历史库也就就是冷库只保存2年超过这个的间的数据直接归档,面实现一个1+2的存储方案。
- 采用的HTAP的存储方案
采用TiDB,polorDB数据库来存储数据,这类数据库的好处是支持在线扩容,比如分片,迁移,这种扩展对业务完全无感知。同时支持强一致性分布事务的方案 也就是跨分片跨节点的事务一致性,可以实现跨机房部署,实现跨机房容灾。同时支持多节点写入操作。