分库分表理论基础
分库分表
背景描述
在系统搭建刚开始的时候我们一般使用的是单机数据库或主从架构,但是随着业务的发展,数据量越来越大。我们会遇到下面的一系列问题:
- 用户请求量大
单台服务器TPS、内存、IO都是有上限的,我们需要将请求分散到多个服务器去
- 单库数据量太大
单个数据库处理能力有限;单库所在的服务器磁盘空间、IO都是有限制的
- 单表数据量太大
单表数据量很大了之后,查询、插入、更新操作都会变慢,加字段、索引都会产生高负载。
解决方案:
一般我们采用分库分表来解决以上的问题。而我们分库分表又可以分为垂直拆分和水平拆分。
-
垂直拆分
1.1 垂直分库
按照业务切割,如将电商的数据库分为:用户库、商品库、订单库
1.2 垂直分表
将一个表进行切分,如将商品表分为:商品名称表、商品属性表
-
水平拆分
2.1 水平分库
将单张表的数据切分到多个服务器上去,每个服务器具有相同的库与表结构,只是数据不同。
2.2 水平分表
将单张表的数据切分到多个表里面去。消除单表数据量太大引发的性能低的问题。
分片规则
在我们分库分表的时候需要按照一定规则进行划分。我们一般划分的方法有如下几个:
- Range
- 时间:按照年、月、日去切分
- 地域:按照省或者市划分
- 大小:如从0到一千万的数据,每100万放一个表,0-100万、100-200万等。
- Hash
- 用户ID取模
- 一致性Hash
实例:
- 用户表
划分方法1:按用户id进行hash来分片。
但是这样的话如果是用户需要通过手机号登录,就要查询多个库。
划分方法2:此时可以存储一个用户ID和手机号的关系表。这个关系表用手机号来分片。
- 订单表
划分方法1:按照用户id为分片键。这样每个用户的订单都在一个库。
但是这样的话如果是商家端想查看买了自家公司产品的订单,就需要查询多个库。
划分方法2:我们可以采用空间换时间,将订单再按照商家ID进行分片,再存一份数据。
ShardingSphere
Apache ShardingSphere是一款开源的分布式数据库中间件组成的生态圈。它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(规划中)这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、容器、云原生等各种多样化的应用场景。
- Sharding JDBC:被定位为轻量级Java框架,在Java的JDBC层提供额外服务,以jar包形式使用
- Sharding Proxy:被定位为数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。
- Sharding Sidecar:被定位为Kubernates或Mesos的云原生数据库代理,以DaemonSet的形式代理所有对数据库的访问。
Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar三者区别如下:
Sharding-JDBC
Sharding-JDBC定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架的使用。
- 适用于任何基于Java的ORM框架,如:JPA、Hibernate、Mybatis、Spring JDBC Template等
- 适用于基于任何第三方的数据库连接池,如:DBCP、C3P0、BoneCP、Druid、HikariCP等
- 支持任意的实现JDBC规范的数据库,目前支持:MySQL、Oracle、SQLServer和PostgreSQL
Sharding-JDBC主要功能:
-
数据分片
- 分库分表
- 读写分离
- 分片策略
- 分布式主键
-
分布式事务
- 标准的事务接口
- XA强一致性事务
- 柔性事务
-
数据库治理
- 配置动态化
- 编排和治理
- 数据脱敏
- 可视化链路追踪
核心概念
- 表概念
- 真实表
数据库中真实存在的物理表。例如b_order0、b_order1
- 逻辑表
数据分片后,同一类表结构的名称。例如b_order
- 数据节点
在分片之后,由数据源和数据表组成。例如ds0.b_order1
- 绑定表
指的是分片规则一致的关系表(主表、子表),例如订单表和订单明细表,均按照order_id进行分片,则此两个表可以设置为绑定表关系,绑定表之间的连接查询不会出现笛卡尔积关联,提升查询效率。
如order1、order2、order_detail1、order_detail2四个表
没配置绑定表的话,关联查询时需要4个sql
select * from order1 left join order_detail1 on ....
select * from order1 left join order_detail2 on ....
select * from order2 left join order_detail1 on ....
select * from order2 left join order_detail2 on ....
配置了绑定表后:
select * from order1 left join order_detail1 on ....
select * from order2 left join order_detail2 on ....
- 广播表
在使用中,有些表没必要做分片,例如字典表、省份信息等,因为他们数据量不大,而且这种表可能需要跟一些大表关联查询。广播表会在不同的数据节点上进行存储,存储的表结构和数据完全相同。
2. 分片算法
由于分片算法和业务实现紧密相关,因此并未提供内置的分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让开发者自行实现分片算法。目前提供4种分片算法:
- 精确分片算法PreciseShardingAlgorithm
用于处理使用单一键作为分片键的=与in进行分片的场景
- 范围分片算法RangeShardingAlgorithm
用于处理使用单一键作为分片键的BETWEEN AND、<、>、>=、<=进行分片的场景
- 复合分片算法ComplexKeysSharingAlgorithm
用于处理使用多键作为分片键进行分片的场景
- Hint分片算法HintShardingAlgorithm
用于处理使用Hint行分片的场景。
- 分片策略
分片策略包含分片键和分片算法。目前提供5种分片策略:
- 标准分片策略StandardShardingStrategy
只支持单分片键,提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。
PreciseShardingAlgorithm是必选的,RangeShardingAlgorithm是可选的。但是SQL中使用了范围操作,如果不配置RangeShardingAlgorithm会采用全库路由扫描,效率低。
- 复合分片策略ComplexShardingStrategy
支持多分片键。提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。
- 行表达式分片策略InlineShardingStrategy
只支持单分片键。使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发。如: t_user_$->{u_id % 8}表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0到t_user_7。
- Hint分片策略HintShardingStrategy
通过Hint指定分片值而非从SQL中提取分片值的方式进行分片的策略。
- 不分片策略NoneShardingStrategy
不分片的策略。
SQL使用规范
支持项:
- 路由至单数据节点时,目前MySQL数据库100%全兼容,其他数据库完善中
- 路由至多数据节点时,全面支持DQL、DML、DDL、DCL、TCL。支持分页、去重、排序、分组、聚合、关联查询。不支持CASE WHEN、HAVING、UNION。支持的最复杂的查询示例:
SELECT select_expr [, select_expr ...]
FROM table_reference [, table_reference ...]
[WHERE predicates]
[GROUP BY {col_name | position} [ASC | DESC], ...]
[ORDER BY {col_name | position} [ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
- 支持分页子查询,但是只支持嵌套一层的子查询。
- 当分片键处于运算表达式或函数中的SQL时,将采用全路由的形式获取结果。
不支持项:
INSERT INTO tbl_name (col1, col2, …) VALUES(1+2, ?, …) //VALUES语句不支持运算表达式
INSERT INTO tbl_name (col1, col2, …) SELECT col1, col2, … FROM tbl_name
WHERE col3 = ? //INSERT .. SELECT
SELECT COUNT(col1) as count_alias FROM tbl_name GROUP BY col1 HAVING
count_alias > ? //HAVING
SELECT * FROM tbl_name1 UNION SELECT * FROM tbl_name2 //UNION
SELECT * FROM tbl_name1 UNION ALL SELECT * FROM tbl_name2 //UNION ALL
SELECT * FROM ds.tbl_name1 //包含schema
SELECT SUM(DISTINCT col1), SUM(col1) FROM tbl_name //同时使用普通聚合函数和DISTINCT
SELECT * FROM tbl_name WHERE to_date(create_time, ‘yyyy-mm-dd’) = ? //会导致全路由
分页查询:
查询偏移量大的分页会导致数据库获取数据性能低下,如下面这个SQL
SELECT * FROM b_order ORDER BY id LIMIT 1000000, 10
如果是分为2个库的情况,为了保证数据正确性,SQL会被改写为:
SELECT * FROM b_order ORDER BY id LIMIT 0, 1000010
并且两个库都会执行这条SQL,然后取10条数据出来。
ShardingSphere对分页查询进行了2个优化:
- 流式处理+归并排序的方式避免内存的过量占用。
由于SQL改写不可避免的占用了额外的带宽,但并不会导致内存暴涨。与直觉不同,大多数人认为ShardingSphere会将1,000,010*2记录全部加载至内存,进而占用大量内存而导致内存溢出。
由于每个结果集的记录是有序的,因此ShardingSphere每次比较仅获取各个分片的当前结果集记录10条,驻留在内存中的记录仅为当前路由到的分片的结果集的当前游标指向,即只有20条。对于本身即有序的待排序对象,归并排序的时间复杂度仅为O(n),性能损耗很小。
- 对落至单节点的查询进行进一步的优化
落至单分片查询的请求并不需要改写SQL也可以保证记录的正确性,因此在此种情况下,ShardingSphere并未进行SQL改写,从而达到节省带宽的目的。
其他功能
- Inline行表达式
InlineShardingStrategy:采用Inline行表达式进行分片的配置。Inline是可以简化数据节点和分片算法配置信息。主要是解决配置简化、配置一体化。
语法格式:
行表达式的使用非常直观,只需要在配置中使用${expression }或$->{ expression }标识行表达式即可。例如:
${begin..end} # ..表示范围区间
${[unit1, unit2, unit_x]} # [,,]表示枚举值
行表达式中如果出现多个${}或$->{}表达式,整个表达式结果会将每个子表达式结果进行笛卡尔(积)组合。例如,以下行表达式:
${['online', 'offline']}_table${1..3}
$->{['online', 'offline']}_table$->{1..3}
最终会解析成:
online_table1, online_table2, online_table3,
offline_table1, offline_table2, offline_table3
对于均匀分布的分片还是好写,那么不均匀的呢?
db0
├── b_order0
└── b_order1
db1
├── b_order2
├── b_order3
└── b_order4
可以写成这样:
db0.b_order${0..1},db1.b_order${2..4}