一、分库分表背景
在如今互联网的海量数据场景下,传统的将数据集中存储在单一数据节点,在性能、高可用性和运维成本上会出现瓶颈和内耗。随着业务数据量的不断增加,当数据容量过大、QPS/TPS接近或超过单个数据库实例的处理极限时,往往是采用垂直和水平结合的数据拆分方法,把数据服务和数据存储分布到多台数据库服务器上,并将大数据量表根据量级进行拆分。
1.容量瓶颈
我们都知道数据库性能瓶颈,主要取决于CPU、内存、文件IO、网络IO等,抛开硬件基础设施不论,减少单节点的IO次数是提升性能瓶颈的重要因素,常规的关系型数据库,以B+Tree类型索引为例,IO次数取决于b+Tree树的索引高度,而数据量超过一定大小,B+Tree 索引的高度就会增加,而每增加一层高度,整个索引扫描就会多一次 IO,因而当某张表的数据量达到容量瓶颈(行业内差不多1000万条数据)时,我们会将它进行分表 。
索引高度这里解释一下:b+Tree树索引高度的计算公式为:设高度为h,数据表的数据量为N,每个磁盘块的数据项的数量是m,则有:h=㏒(m+1)N ,而m = 磁盘块的大小 / 数据项的大小,通常磁盘块的大小也就是一个数据页的大小,是固定的,数据项所占空间取决于索引字段所占字节,索引字段的长度一般都是表设计就定义好了,所以从公式上可以得出影响索引高度的变量因素就是表中的数据量大小。
2.吞吐量瓶颈
⾼并发访问请求也使得集中式数据库成为系统的最⼤瓶颈,数据分片是最优的解决方式,数据分片也称为分库分表,分库能够⽤于有效的分散对数据库单点的访问量,分表能够⽤于有效的数据量超过可承受阈值而产⽣的查询瓶颈, 解决MySQL 单表性能问题,使⽤多主多从的分⽚⽅式,可以有效的避免数据单点,从而提升数据架构的高可⽤性。通过分库和分表进⾏数据的拆分来使得各个表的数据量保持在阈值以下,以及对流量进⾏疏导应对⾼访问量,是应对⾼并发和海量数据系统的有效⼿段。
二、技术选型
目前常用的就是Cobar(MyCat)与Sharding-JDBC两种,是具有代表性的中间层(proxy)和客户端直连(client)的方案。
proxy
- mycat或者cobar,shading-proxy,atlas等都属于中间层方案。
- 缺点是需要独立部署,单机模式无法保证可靠性,一旦宕机则服务就变得不可用。
- 优势是中间层更便于实现监控、数据迁移、连接管理等功能。
client
- shading-jdbc
- 对应用透明,由应用业务方配置分库分表策略
- client模式,它架构简单,性能损耗也比较小,运维成本低
选型分析:Sharding-JDBC轻量级客户端直连库的方式做分库分表,改造成本较小,功能丰富。
优点如下:
Sharding-JDBC定位为轻量Java框架,使用客户端直连数据库,以jar包形式提供服务,无proxy代理层,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式,适用于中小企业。
Sharding-JDBC分片策略灵活,可支持等号、between、in等多维度分片,也可支持多分片键。
SQL解析功能完善,支持聚合、分组、排序、limit、or等查询,并支持Binding Table以及笛卡尔积表查询。
三、数据分片
1.分片拆分方式
垂直拆分
- 按业务模块拆分,不同业务模块独立为单独的库
- 拆分多列表为多表,减少表的列数
水平拆分
- 把大数据量单表拆分成多表,例如表数据为1个亿,预估年增长量一千多万,那可以按单表1000万,并预留5-10年的增长空间,合理拆分出50-100张表左右。
- 这里每个表也可以拆分到不同的数据库
2.分片的实现
首先会按照分片规则把数据分到若干个shard、partition当中,当执行一条SQL时,会通过 路由策略 , 将数据route(路由) 到不同的分片内。因而我们需要考虑一系列问题:
- 选择合理的分片算法
- 分片键的选定
- 分片路由
这里介绍几个核心概念:
分片键:⽤于分⽚的字段,是将数据库(表)⽔平拆分的关键字段。可以用主键或唯一标识ID。
数据节点:数据节点是分库分表中一个不可再分的最小数据单元(表),它由数据源名称和数据表组成。
逻辑表:逻辑表是指一组具有相同逻辑和数据结构表的总称。比如我们将用户表 t_user 拆分成 t_user_0 ··· t_user_9 等 10 张表。此时我们会发现分库分表以后数据库中已不在有 t_user 这张表,取而代之的是 t_user_n,但我们在代码中写 SQL 依然按 t_user 来写。而 t_user 就是这些拆分表的逻辑表。
真实表:真实表也就是上边提到的 t_user_n 数据库中真实存在的物理表。
绑定表:指分片规则一致的主表和子表。例如:t_order 表和 t_order_item 表,均按照 order_id 分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。\
广播表:指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
2.1 分片算法
比较常用的分片算法有range区间分片、分片键hash分片等,可以根据实际情况选用。
粗略介绍一下这些常用的分片算法原理:
range区间分片:就是每个片,一段连续的数据,这个一般是按比如时间范围/数据范围来的,但是这种一般较少用,因为很容易发生数据倾斜,大量的流量都打在最新的数据上了。
hash取模分片:对表中的分片键hash运算之后,与数据节点数量进行取余运算,根据余数不同保存在不同的节点上,缺点是当数据节点扩容时原节点中的数据会进行迁移操作,数据大批量迁移是不安全的,所以一般会按5年预增的量来分配数据节点。
不得不进行数据节点扩容的情况,建议进行倍数扩容,比如原来5个,扩容就需要选5的倍数,这样可以控制数据迁移最多也才50%的量。
一致性hash分片:对每一个分片键key进行hash运算,被哈希后的结果在哪个token的范围内,则按顺时针去找最近的节点,这个key将会被保存在这个节点上。一致性哈希在大批量的数据场景下负载更加均衡,但是在数据规模小的场景下,会出现单位时间内某个节点完全空闲的情况出现。
虚拟槽分片:虚拟槽分片是Redis Cluster采用的分片方式,可以理解为范围分片的变种, hash取模分片+range范围分片, 把hash值取余数分为n段,一个段给一个节点负责。
- 比如在 Redis Cluster 中一共有 0~16383 个虚拟槽, 我们可以把这些槽分配给对应的分片服务器
- 在存储数据的时候通过
slot = CRC16(key) & 16383计算出对应数据键的槽值- 然后将该值保存到对应槽的分片服务器上
从执行 SQL 的角度来看,分库分表可以看作是一种路由机制,把 SQL 语句路由到我们期望的数据库或数据表中并获取数据,分片算法可以理解成一种路由规则。因而sharding-jdbc抽象出了一批sql角度路由的分片算法,便于开发者结合实际场景进行分片实现:
Sharding-Jdbc抽象分片算法类: ShardingAlgorithm,根据类型又分为:精确分片算法、区间分片算法、复合分片算法以及Hint分片算法;
-
精确分片算法:对应
PreciseShardingAlgorithm类,主要用于处理=和IN的分片; -
区间分片算法:对应
RangeShardingAlgorithm类,主要用于处理BETWEEN AND,>,<,>=,<=分片; -
复合分片算法:对应
ComplexKeysShardingAlgorithm类,用于处理使用多键作为分片键进行分片的场景; -
Hint分片算法:对应
HintShardingAlgorithm类,用于处理使用Hint行分片的场景;
2.2 ShardingJdbc的分片策略
ShardingJDBC 分库分表的核心就是在于 ****配置 分片策略+分片算法 **** 。分片策略是一种抽象的概念,实际分片操作的是由分片算法和分片健来完成的,也就是说算法是工具,而分片策略是决定分片的准则。
- 标准分片策略:在实际使用过程中,常用的分片策略就是单分片键,此策略支持 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 两个分片算法。其中 PreciseShardingAlgorithm 是必选的,用于处理 = 和 IN 的分片。RangeShardingAlgorithm 用于处理BETWEEN AND, >, <,>=,<= 条件分片,RangeShardingAlgorithm 是可选的, 如果不配置RangeShardingAlgorithm,SQL中的条件等将按照全库路由处理。
-
复合分片策略: 同样支持对 SQL语句中的 =,>, <, >=, <=,IN和 BETWEEN AND 的分片操作。不同的是它支持多分片键,具体分配片细节完全由应用开发者实现。ComplexShardingStrategy ⽀持多分⽚键,由于多分⽚键之间的关系复杂,因此并未进⾏过多的封装,而是直接将分⽚键值组合以及分⽚操作符透传⾄分⽚算法,完全由应⽤开发者实现,提供最⼤的灵活度。
-
表达式分片策略(inline内联分片策略): 行表达式分片策略,支持对 SQL语句中的 = 和 IN 的分片操作,但只支持单分片键。这种策略通常用于简单的分片,不需要自定义分片算法,可以直接在配置文件中接着写规则。
t_order_$->{t_order_id % 4} 代表 t_order 对其字段 t_order_id取模,拆分成4张表,而表名分别是t_order_0 到 t_order_3。
-
**强制分片策略(Hint 暗示分片策略): **Hint 分片策略,通过指定分片健而非从 SQL 中提取分片健的方式进行分片的策略。对于分⽚值⾮ SQL 决定,不是来自于分片建,甚至连分片建都没有 ,而由其他外置条件决定的场景,可使⽤Hint 分片策略 。
前面的分片策略都是解析 SQL 语句, 提取分片建和分片值,并根据设置的分片算法进行分片。Hint 分片算法 指定分⽚值而⾮从 SQL 中提取,而是手工设置的⽅式,进⾏分⽚的策略。
-
**不分⽚策略: **对应 NoneShardingStrategy。不分⽚的策略。这种严格来说不算是一种分片策略了。只是ShardingSphere也提供了这么一个配置。
2.3 Sharding-Jdbc路由原理
2.3.1 Sql的执行流程
shardingjdbc 对原有的 DataSource、Connection 等接口扩展成 ShardingDataSource、ShardingConnection,而对外暴露的分片操作接口与 JDBC 规范中所提供的接口完全一致,只要你熟悉 JDBC 就可以轻松应用 Sharding-JDBC 来实现分库分表。
一张表经过分库分表后被拆分成多个子表,并分散到不同的数据库中,在不修改原业务 SQL 的前提下,
Sharding-JDBC 就必须对 SQL进行一些改造才能正常执行。大致的执行流程如下:
-
SQL解析: 根据语法将拆分后的SQL转换为抽象语法树,通过对抽象语法树遍历,提炼出分片所需的上下文,上下文包含查询字段信息(
Field)、表信息(Table)、查询条件(Condition)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit)等,并标记出 SQL中有可能需要改写的位置。 -
SQL改写:将基于逻辑表开发的SQL改写成可以在真实数据库中可以正确执行的语句。比如查询
t_user用户表,我们实际开发中 SQL是按逻辑表t_user写的,改写后变为t_user_n 。 -
SQL执行:将路由和改写后的真实 SQL 安全且高效发送到底层数据源执行。但这个过程并不是简单的将 SQL 通过JDBC 直接发送至数据源执行,而是平衡数据源连接创建以及内存占用所产生的消耗,它会自动化的平衡资源控制与执行效率。
-
结果归并:将从各个数据节点获取的多数据结果集,合并成一个大的结果集并正确的返回至请求客户端,称为结果归并。而我们SQL中的排序、分组、分页和聚合等语法,均是在归并后的结果集上进行操作的。
2.3.2 shardingjdbc路由剖析
SQL 路由就是通过解析sql上下文,并根据我们配置的分片策略计算出SQL该在哪个真实库、真实表中执行。而sql路由有根据有无分片键区分出分片路由和广播路由。