分库分表之ShardingSphere
为什么要分库分表
- 用户请求量太大
单服务器TPS、内存、IO都是有上限的,需要将请求打散分布到多个服务器 。
- 单库数据量太大
单个数据库处理能力有限;单库所在服务器的磁盘空间有限;单库上的操作IO有瓶颈 。
- 单表数据量太大
查询、插入、更新操作都会变慢,在加字段、加索引、机器迁移都会产生高负载,影响服务。
拆分方式
垂直拆分
- 垂直分库
微服务架构时,业务切割得足够独立,数据也按照业务切分,不同业务的数据存入不同的库中。
- 垂直分表
表中字段太多且包含大字段,可将这张大表拆分为多张表。
水平拆分
- 水平分库
将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。
- 水平分库规则
不跨库、不跨表,保证同一类的数据都在同一个服务器上面。
- 水平分表
针对数据量巨大的单张表(比如订单表),按照规则把一张表的数据切分到多张表里面去。
- 水平分表规则
- RANGE
- 时间:按照年、月、日去切分。例如order_2022、order_202203、order_20220301
- 地域:按照省或市去切分。例如order_beijing、order_shanghai、order_chengdu
- 大小:从0到1000000一个表。例如1000001-2000000放一个表,每100万放一个表
- HASH
- 用户ID取模
- 站内信
- 用户维度:用户只能看到发送给自己的消息,其他用户是不可见的,这种情况下是按照
- 用户ID hash分库,在用户查看历史记录翻页查询时,所有的查询请求都在同一个库内
- 用户表
- 范围法:以用户ID为划分依据,将数据水平切分到两个数据库实例,如:1到1000W在 一张表,1000W到2000W在一张表,这种情况会出现单表的负载较高
- 按照用户ID HASH尽量保证用户数据均衡分到数据库中
...
面临的问题
由于数据存储在不同的库和表中,也带来了其他方面的一些问题,如下:
- ID主键怎么生成?
- 数据怎么保持一致?
- 数据库怎么扩容?
- 业务层代码怎么改造?
分页排序、事务问题、跨表跨库join问题等。
解决方案
主键选择
- UUID:本地生成,不依赖数据库,缺点就是作为主键性能太差
- SNOWFLAKE:雪花算法
数据一致性
- 强一致性:XA协议
- 最终一致性:TCC、saga、Seata
数据库扩容
- 成倍增加数据节点,实现平滑扩容
- 成倍扩容以后,表中的部分数据请求已被路由到其他节点上面,可以清理掉
业务层改造
- 基于代理层方式:Mycat、Sharding-Proxy、MySQL Proxy
- 基于应用层方式:Sharding-jdbc
跨库跨表join问题
- 全局表(字典表):基础数据/配置数据,所有库都拷贝一份
- 字段冗余:可以使用字段冗余就不用join查询了
- 系统层组装:可以在业务层分别查询出来,然后组装起来,逻辑较复杂
中间件之ShardingSphere
简介
Apache ShardingSphere是一款开源的分布式数据库中间件组成的生态圈。它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(规划中)这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、容器、云原生等各种多样化的应用场景。
ShardingSphere定位为关系型数据库中间件,旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力,而并非实现一个全新的关系型数据库。
Sharding-JDBC:被定位为轻量级Java框架,在Java的JDBC层提供的额外服务,以jar包形式使用。
Sharding-Proxy:被定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。
Sharding-Sidecar:被定位为Kubernetes或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或直接使用JDBC。
- 基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
- 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer和PostgreSQL。
Sharding-JDBC主要功能:
- 数据分片
- 分库、分表
- 读写分离
- 分片策略
- 分布式主键
- 分布式事务
- 标准化的事务接口
- XA强一致性事务
- 柔性事务
- 数据库治理
- 配置动态化
- 编排和治理
- 数据脱敏
- 可视化链路追踪
Sharding-JDBC 使用过程
- 引入maven依赖
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-core</artifactId> <version>${latest.release.version}</version> </dependency>
- 规则配置
Sharding-JDBC可以通过Java,YAML,Spring命名空间和Spring Boot Starter四种方式配置,开发者可根据场景选择适合的配置方式。
- 创建DataSource
通过ShardingDataSourceFactory工厂和规则配置对象获取ShardingDataSource,然后即可通过DataSource选择使用原生JDBC开发,或者使用JPA, MyBatis等ORM工具。
DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap,
shardingRuleConfig, props);
数据分片
核心概念
表概念
- 真实表
数据库中真实存在的物理表。例如b_order0、b_order1
- 逻辑表
在分片之后,同一类表结构的名称(总成)。例如b_order。
- 数据节点
在分片之后,由数据源和数据表组成。例如ds0.b_order1
- 绑定表
指的是分片规则一致的关系表(主表、子表),例如b_order和b_order_item,均按照order_id分片,则此两个表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,可以提升关联查询效率。
b_order:b_order0、b_order1 b_order_item:b_order_item0、b_order_item1 select * from b_order o join b_order_item i on(o.order_id=i.order_id) where o.order_id in (10,11);
如果不配置绑定表关系,采用笛卡尔积关联,会生成4个SQL
select * from b_order0 o join b_order_item0 i on(o.order_id=i.order_id) where o.order_id in (10,11); select * from b_order0 o join b_order_item1 i on(o.order_id=i.order_id) where o.order_id in (10,11); select * from b_order1 o join b_order_item0 i on(o.order_id=i.order_id) where o.order_id in (10,11); select * from b_order1 o join b_order_item1 i on(o.order_id=i.order_id) where o.order_id in (10,11);
如果配置绑定表关系,生成2个SQL
select * from b_order0 o join b_order_item0 i on(o.order_id=i.order_id) where o.order_id in (10,11); select * from b_order1 o join b_order_item1 i on(o.order_id=i.order_id) where o.order_id in (10,11);
- 广播表
在使用中,有些表没必要做分片,例如字典表、省份信息等,因为他们数据量不大,而且这种表可能需要与海量数据的表进行关联查询。广播表会在不同的数据节点上进行存储,存储的表结构和数据完全相同。
- 逻辑索引
某些数据库(如:PostgreSQL)不允许同一个库存在名称相同索引,某些数据库(如:MySQL)则允许只要同一个表中不存在名称相同的索引即可。 逻辑索引用于同一个库不允许出现相同索引名称的分表场景,需要将同库不同表的索引名称改写为索引名 + 表名,改写之前的索引名称成为逻辑索引。
分片键
用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段。 SQL中如果无分片字段,将执行全路由,性能较差。 除了对单分片字段的支持,ShardingSphere也支持根据多个字段进行分片。
分片算法(ShardingAlgorithm)
通过分片算法将数据分片,支持通过=、BETWEEN和IN分片。
目前提供4种分片算法。由于分片算法和业务实现紧密相关,因此并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。
- 精确分片算法
对应PreciseShardingAlgorithm,用于处理使用单一键作为分片键的=与IN进行分片的场景。需要配合StandardShardingStrategy使用。
- 范围分片算法
对应RangeShardingAlgorithm,用于处理使用单一键作为分片键的BETWEEN AND进行分片的场景。需要配合StandardShardingStrategy使用。
- 复合分片算法
对应ComplexKeysShardingAlgorithm,用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。需要配合ComplexShardingStrategy使用。
- Hint分片算法
对应HintShardingAlgorithm,用于处理使用Hint行分片的场景。需要配合HintShardingStrategy使用。
分片策略
包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前提供5种分片策略。
- 标准分片策略
对应StandardShardingStrategy。提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。
- 复合分片策略
对应ComplexShardingStrategy。复合分片策略。提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。
- 行表达式分片策略
对应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。不分片的策略。
分片策略配置
对于分片策略存有数据源分片策略和表分片策略两种维度,两种策略的API完全相同。
- 数据源分片策略
用于配置数据被分配的目标数据源。
- 表分片策略
用于配置数据被分配的目标表,由于表存在与数据源内,所以表分片策略是依赖数据源分片策略结果的。
流程剖析
ShardingSphere的3个产品的数据分片主要流程是完全一致的。 核心由SQL解析 => 执行器优化 => SQL路由 => SQL改写 => SQL执行 => 结果归并的流程组成。
- SQL解析
分为词法解析和语法解析。 先通过词法解析器将SQL拆分为一个个不可再分的单词。再使用语法解析器对SQL进行理解,并最终提炼出解析上下文。 解析上下文包括表、选择项、排序项、分组项、聚合函数、分页信息、查询条件以及可能需要修改的占位符的标记。
- 执行器优化
合并和优化分片条件,如OR等。
- SQL路由
根据解析上下文匹配用户配置的分片策略,并生成路由路径。目前支持分片路由和广播路由。
- SQL改写
将SQL改写为在真实数据库中可以正确执行的语句。SQL改写分为正确性改写和优化改写。
- SQL执行
通过多线程执行器异步执行。
- 结果归并
将多个执行结果集归并以便于通过统一的JDBC接口输出。结果归并包括流式归并、内存归并和使用装饰者模式的追加归并这几种方式。
SQL使用规范
支持项
路由至单数据节点时,目前MySQL数据库100%全兼容,其他数据库完善中。
路由至多数据节点时,全面支持DQL、DML、DDL、DCL、TCL。支持分页、去重、排序、分组、聚合、关联查询(不支持跨库关联)。
- SELECT主语句
SELECT select_expr [, select_expr ...] FROM table_reference [, table_reference ...] [WHERE where_condition] [GROUP BY {col_name | position} [ASC | DESC]] [ORDER BY {col_name | position} [ASC | DESC], ...] [LIMIT {[offset,] row_count | row_count OFFSET offset}]
- select_expr
* | [DISTINCT] COLUMN_NAME [AS] [alias] | (MAX | MIN | SUM | AVG)(COLUMN_NAME | alias) [AS] [alias] | COUNT(* | COLUMN_NAME | alias) [AS] [alias]
- table_reference
tbl_name [AS] alias] [index_hint_list] | table_reference ([INNER] | {LEFT|RIGHT} [OUTER]) JOIN table_factor [JOIN ON conditional_expr | USING (column_list)] |
不支持项
不支持CASE WHEN、HAVING、UNION (ALL)
- 支持分页子查询,但其他子查询有限支持,无论嵌套多少层,只能解析至第一个包含数据表的子查询,一旦在下层嵌套中再次找到包含数据表的子查询将直接抛出解析异常。
例如,以下子查询可以支持:
SELECT COUNT(*) FROM (SELECT * FROM b_order o)
以下子查询不支持:
SELECT COUNT(*) FROM (SELECT * FROM b_order o WHERE o.id IN (SELECT id FROM b_order WHERE status = ?))
简单来说,通过子查询进行非功能需求,在大部分情况下是可以支持的。比如分页、统计总数等;而通过子查询实现业务查询当前并不能支持。
- 由于归并的限制,子查询中包含聚合函数目前无法支持。
- 不支持包含schema的SQL。因为ShardingSphere的理念是像使用一个数据源一样使用多数据源,因此对SQL的访问都是在同一个逻辑schema之上。
- 当分片键处于运算表达式或函数中的SQL时,将采用全路由的形式获取结果。
例如下面SQL,create_time为分片键:
SELECT * FROM b_order WHERE to_date(create_time, 'yyyy-mm-dd') = '2020-05-05';
由于ShardingSphere只能通过SQL字面提取用于分片的值,因此当分片键处于运算表达式或函数中时,ShardingSphere无法提前获取分片键位于数据库中的值,从而无法计算出真正的分片值。
分页查询
完全支持MySQL和Oracle的分页查询,SQLServer由于分页查询较为复杂,仅部分支持.
- 性能瓶颈
查询偏移量过大的分页会导致数据库获取数据性能低下,以MySQL为例:
SELECT * FROM b_order ORDER BY id LIMIT 1000000, 10
这句SQL会使得MySQL在无法利用索引的情况下跳过1000000条记录后,再获取10条记录,其性能可想而知。 而在分库分表的情况下(假设分为2个库),为了保证数据的正确性,SQL会改写为:
SELECT * FROM b_order ORDER BY id LIMIT 0, 1000010
即将偏移量前的记录全部取出,并仅获取排序后的最后10条记录。这会在数据库本身就执行很慢的情况下,进一步加剧性能瓶颈。 因为原SQL仅需要传输10条记录至客户端,而改写之后的SQL则会传输1,000,010 * 2的记录至客户端。
- ShardingSphere的优化
ShardingSphere进行了以下2个方面的优化。
- 首先,采用流式处理 + 归并排序的方式来避免内存的过量占用。
- 其次,ShardingSphere对仅落至单节点的查询进行进一步优化。
- 分页方案优化
由于LIMIT并不能通过索引查询数据,因此如果可以保证ID的连续性,通过ID进行分页是比较好的解决方案:
SELECT * FROM b_order WHERE id > 1000000 AND id <= 1000010 ORDER BY id
或通过记录上次查询结果的最后一条记录的ID进行下一页的查询
SELECT * FROM b_order WHERE id > 1000000 LIMIT 10
其它功能
- 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_order2
└── b_order1
db1
├── b_order2
└── b_order1
用行表达式可以简化为:
db${0..1}.b_order${1..2}
或者
db$->{0..1}.b_order$->{1..2}
对于自定义的数据节点,如果数据结构如下:
db0
├── b_order0
└── b_order1
db1
├── b_order2
├── b_order3
└── b_order4
用行表达式可以简化为:
db0.b_order${0..1},db1.b_order${2..4}
- 分片算法配置
行表达式内部的表达式本质上是一段Groovy代码,可以根据分片键进行计算的方式,返回相应的
真实数据源或真实表名称。
ds${id % 10}
或者
ds$->{id % 10}
结果为:ds0、ds1、ds2... ds9
- 分布式主键
ShardingSphere不仅提供了内置的分布式主键生成器,例如UUID、SNOWFLAKE,还抽离出分布式主键生成器的接口,方便用户自行实现自定义的自增主键生成器。
- 内置主键生成器
- UUID :采用UUID.randomUUID()的方式产生分布式主键。
- SNOWFLAKE :在分片规则配置模块可配置每个表的主键生成策略,默认使用雪花算法,生成64bit的长整型 数据。
- 自定义主键生成器
- 自定义主键类,实现ShardingKeyGenerator接口
- 按SPI规范配置自定义主键类
在Apache ShardingSphere中,很多功能实现类的加载方式是通过SPI注入的方式完成的。
注意:在resources目录下新建META-INF文件夹,再新建services文件夹,然后新建文件的名字为org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator,打开文件,复制自定义主键类全路径到文件中保存。
- 自定义主键类应用配置
#对应主键字段名
spring.shardingsphere.sharding.tables.t_book.key-generator.column=id
#对应主键类getType返回内容
spring.shardingsphere.sharding.tables.t_book.key-generator.type=LAGOUKEY
读写分离
读写分离是通过主从的配置方式,将查询请求均匀的分散到多个数据副本,进一步的提升系统的处理能力。
- 主从架构:读写分离,目的是高可用、读写扩展。主从库内容相同,根据SQL语义进行路由。
- 分库分表架构:数据分片,目的读写扩展、存储扩容。库和表内容不同,根据分片配置进行路由。
将水平分片和读写分离联合使用,能够更加有效的提升系统性能,
下图展现了将分库分表与读写分离一同使用时,应用程序与数据库集群之间的复杂拓扑关系。
读写分离虽然可以提升系统的吞吐量和可用性,但同时也带来了数据不一致的问题,包括多个主库之间 的数据一致性,以及主库与从库之间的数据一致性的问题。 并且,读写分离也带来了与数据分片同样的 问题,它同样会使得应用开发和运维人员对数据库的操作和运维变得更加复杂。
读写分离应用方案
在数据量不是很多的情况下,我们可以将数据库进行读写分离,以应对高并发的需求,通过水平扩展从 库,来缓解查询的压力。如下:
分表+读写分离
在数据量达到500万的时候,这时数据量预估千万级别,我们可以将数据进行分表存储。
分库分表+读写分离
在数据量继续扩大,这时可以考虑分库分表,将数据存储在不同数据库的不同表中,如下:
透明化读写分离所带来的影响,让使用方尽量像使用一个数据库一样使用主从数据库集群,是ShardingSphere读写分离模块的主要设计目标。
主库、从库、主从同步、负载均衡
核心功能
- 提供一主多从的读写分离配置。仅支持单主库,可以支持独立使用,也可以配合分库分表使用独立使用读写分离,支持SQL透传。不需要SQL改写流程
- 同一线程且同一数据库连接内,能保证数据一致性。如果有写入操作,后续的读操作均从主库读取。
- 基于Hint的强制主库路由。可以强制路由走主库查询实时数据,避免主从同步数据延迟。
不支持项
- 主库和从库的数据同步
- 主库和从库的数据同步延迟
- 主库双写或多写
- 跨主库和从库之间的事务的数据不一致。建议在主从架构中,事务中的读写均用主库操作。
主从复制原理
MySQL里有一个概念,叫binlog日志,就是每个增删改类的操作,会改变数据的操作,除了更新数据以外,对这个增删改操作还会写入一个日志文件,记录这个操作的日志。
主库将变更写binlog日志,然后从库连接到主库之后,从库有一个IO线程,将主库的binlog日志拷贝到自己本地,写入一个中继日志中。接着从库中有一个SQL线程会从中继日志读取binlog,然后执行binlog日志中的内容,也就是在自己本地再次执行一遍SQL,这样就可以保证自己跟主库的数据是一样的。
这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行SQL的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。
而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。
所以mysql实际上在这一块有两个机制,一个是半同步复制,用来解决主库数据丢失问题;一个是并行复制,用来解决主从同步延时问题。
这个所谓半同步复制,semi-sync复制,指的就是主库写入binlog日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的relay log之后,接着会返回一个ack给主库,主库接收到至少一个从库的ack之后才会认为写操作完成了。
所谓并行复制,指的是从库开启多个线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。
官方文档地址:https://shardingsphere.apache.org/document/legacy/3.x/document/cn/overview/