引言
前面《MySQL主从原理篇》、《MySQL主从实践篇》两章中聊明白了MySQL
主备读写分离、多主多写热备等方案,但如果这些高可用架构依旧无法满足业务规模,或业务增长的需要,此时就需要考虑选用分库分表架构。
在分库分表领域中,其实有许许多多的一些落地技术栈,如
TDDL、TSharding、Sharding-Sphere、MyCat、Atlas、Oceanus、Vitess.....
,但经时间沉淀与岁月洗礼后,如今主流的方案也就剩下了MyCat、Sharding-Sphere
两种,MyCat
近几年由于某些原因,开始逐渐走下坡路,反观投入Apache
怀抱的Sharding-Sphere
热度逐步上升,其目前的最新版本也相对稳定。
正是由于上述原因,如今越来越多的企业选用Sharding-Sphere
作为分库分表的落地方案,因此《全解MySQL专栏》中的分库分表实践篇章,同样会基于Sharding-Sphere
这套技术来展开叙述~
一、初识Apache-Sharding-Sphere生态
Apache-Sharding-Sphere
的前身是当当网开源的Sharding-JDBC
框架,后面引入Zookeeper
作为注册中心,又研发了Sharding-Proxy
中间件,贡献给Apache
软件基金会后,正式整合成Sharding-Sphere
生态,并且支持、兼容各种数据库,Apache-Sharding-Sphere官网上可看到的发展历程如下:
目前最新的5.2.1
版本属于其中5.x
阶段的一个版本,目前支持可可拔插功能,其支持的核心功能如下:
基本上对于《分库分表副作用篇》中聊到的各类问题,在该版本中都有对应的解决方案,如多表连查、数据分片、分布式事务、数据迁移与扩容、分布式ID.....
,在预计2023
年发布的6.x
版本中会结合各类云容器技术,全面兼容与拥抱云生态,在预计2025
年发布的7.x
版本中,则会彻底落实Databases-Plus
理念,支持将各类数据库作为可拔插式存储引擎使用,也意味着像MySQL
的可拔插式引擎那样,在Sharding-Sphere
中使用MySQL、Oracle、PgSQL、SQL-Server....
等关系型数据。
其实看到这里大家会发现,
Sharding-Sphere
作为Apache
软件基金会的顶级项目,其内部对于它的未来规划十分明确,主要就是构建一个分库分表技术的生态圈,就类似于Spring
框架在J2EE
中的地位一样,迄今为止已发行的5.x
系列版本,已经具备应用于生产环境的能力,其中对于分库分表核心的主干功能都已实现,从官网的发展历程来看,后续版本都属于修修补补的性质,主要是为了让其生态更完善。
Apache-Sharding-Sphere
总共由JDBC、Proxy、Sidecar
三大核心产品组成,前两者都已具备完整形态,Sidecar
还处于开发阶段,这也就代表着目前的ShardingSphere5
中只有JDBC、Proxy
两款产品,这两款产品即支持各自独立部署,也支持混合部署的模式,两者区别在于:
JDBC
:以工程形式嵌入Java
应用,兼容所有JDBC
支持的数据库,适用于任意ORM
框架。Proxy
:以独立的中间件形式部署,目前只支持MySQL、PgSQL
,但支持异构语言开发的系统。
1.1、Sharding-JDBC框架简介
Sharding-JDBC
的定位是一款轻量级Java
框架,它会以POM
依赖的形式嵌入程序,运行期间会和Java
应用共享资源,这款框架的本质可以理解成是JDBC
的增强版,只不过Java
原生的JDBC
仅支持单数据源的连接,而Sharding-JDBC
则支持多数据源的管理,部署形态如下:
Java-ORM
框架在执行SQL
语句时,Sharding-JDBC
会以切面的形式拦截发往数据库的语句,接着根据配置好的数据源、分片规则和路由键,为SQL
选择一个目标数据源,然后再发往对应的数据库节点处理。
Sharding-JDBC
在整个业务系统中对性能损耗极低,但为何后面又会推出Sharding-Proxy
呢?因为Sharding-JDBC
配置较为麻烦,比如在分布式系统中,任何使用分库分表的服务都需要单独配置多数据源地址、路由键、分片策略....等信息,同时它也仅支持Java
语言,当一个系统是用多语言异构的,此时其他语言开发的子服务,则无法使用分库分表策略。
1.2、Sharding-Proxy中间件简介
也正是由于配置无法统一管理、不支持异构系统的原因,后面又引入Sharding-Proxy
来解决这两个问题,Sharding-Proxy
可以将其理解成一个伪数据库,对于应用程序而言是完全透明的,它会以中间件的形式独立部署在系统中,部署形态如下:
使用Sharding-Proxy
的子服务都会以连接数据库的形式,与其先建立数据库连接,然后将SQL
发给它执行,Sharding-Proxy
会根据分片规则和路由键,将SQL
语句发给具体的数据库节点处理,数据库节点处理完成后,又会将结果集返回给Sharding-Proxy
,最终再由它将结果集返回给具体的子服务。
但
Sharding-Proxy
虽然可以实现分库分表配置的统一管理,以及支持异构的系统,但因为需要使用独立的机器部署,同时还会依赖Zookeeper
作为注册中心,所以硬件成本会直线增高,至少需要多出3~4
台服务器来部署。
同时SQL
执行时,需要先发给Proxy
,再由Proxy
发给数据库节点,执行完成后又会从数据库返回到Proxy
,再由Proxy
返回给具体的应用,这个过程会经过四次网络传输的动作,因此相较于原本的Sharding-JDBC
来说,性能、资源开销更大,响应速度也会变慢。
1.3、JDBC、Proxy混合部署模式
如果用驱动式分库分表,虽然能够让Java
程序的性能最好,但无法支持多语言异构的系统,但如果纯用代理式分库分表,这显然会损害Java
程序的性能,因此在Sharding-Sphere
中也支持JDBC、Proxy
做混合式部署,也就是Java
程序用JDBC
做分库分表,其他语言的子服务用Proxy
做分库分表,部署形态如下:
这种混合式的部署方案,所有的数据分片策略都会放到Zookeeper
中统一管理,然后所有的子服务都去Zookeeper
中拉取配置文件,这样就能很方便的根据业务情况,来灵活的搭建适用于各种场景的应用系统,这样也能够让数据源、分片策略、路由键....等配置信息灵活,可以在线上动态修改配置信息,修改后能够在线上环境中动态感知。
但
Sharding-Sphere
还提供了一种单机模式,即直接将数据分片配置放在Proxy
中,但这种方式仅适用于开发环境,因为无法将分片配置同步给多个实例使用,也就意味着会导致其他实例由于感知不到配置变化,从而造成配置信息不一致的错误。
二、Sharding-Sphere中的核心概念
分库分表中最重要的核心概念有两个,即路由键和分片算法,这两个将决定数据分片的位置,先稍微解释一下这两个概念:
- 路由键:也被称为分片键,也就是作为数据分片的基准字段,可以是一个或多个字段组成。
- 分片算法:基于路由键做一定逻辑处理,从而计算出一个最终节点位置的算法。
举个例子来感受一下,好比按user_id
将用户表数据分片,每八百万条数据划分一张表,那在这里,user_id
就是路由键,而按user_id
做范围判断则属于分片算法,一张表中的所有数据都会依据这两个基础,后续对所有的读写SQL
进行改写,从而定位到具体的库、表位置。
2.1、分库分表的工作流程
在Sharding-Sphere
这套技术中,无论是JDBC
还是Proxy
产品,工作的流程都遵循上述这个原则,里面除开上面介绍的路由键和分片算法的概念外,还有逻辑表、真实表、数据节点这三个概念:
- 逻辑表:提供给应用程序操作的表名,程序可以像操作原本的单表一样,灵活的操作逻辑表。
- 真实表:在各个数据库节点上真实存在的物理表,但表名一般都会和逻辑表存在偏差。
- 数据节点:主要是用于定位具体真实表的库表名称,如
DB1.tb_user1、DB2.tb_user2.....
- 均匀分布:指一张表的数量在每个数据源中都是一致的。
- 自定义分布:指一张表在每个数据源中,具体的数量由自己来定义,上图就是一种自定义分布。
以Java
程序为例,编写业务代码时写的SQL
语句,会直接基于逻辑表进行操作,逻辑表并不是一种真实存在的表结构,而是提供给Sharding-Sphere
使用的,当Sharding-Sphere
接收到一条操作某张逻辑表的SQL
语句时,它会根据已配置好的路由键和分片算法,对相应的SQL
语句进行解析,然后计算出SQL
要落入的数据节点,最后再将语句发给具体的真实表上处理即可。
Sharding-Sphere-JDBC、Proxy
的主要区别就在于:解析SQL
语句计算数据节点的时机不同,JDBC
是在Java
程序中就完成了相应的计算,从Java
程序中发出的SQL
语句就已经是操作真实表的SQL
了。而Proxy
则是在Java
应用之外做解析工作,它会接收程序操作逻辑表的SQL
语句。然后再做解析得到具体要操作的真实表,然后再执行,同时Proxy
还要作为应用程序和数据库之间,传输数据的中间人。
2.2、Sharding-Sphere中的表概念
除开上述的一些核心概念外,在Sharding-Sphere
中为了解决某些问题,同时还有一些表概念,如广播表、绑定表、单表、动态表等,接着简单介绍一下这些概念。
2.2.1、绑定表
在《分库分表后遗症-主外键约束》中聊到过,当多张表之间存在物理或逻辑上的主外键关系,如果无法保障同一主键值的外键数据落入同一节点,显然在查询时就会发生跨库查询,这无疑对性能影响是极大的,所以在其中也提到过可以使用绑定表的形式解决该问题。
比如前面案例中的
order_id、order_info_id
可以配置一组绑定表关系,这样就能够让订单详情数据随着订单数据一同落库,简单的说就是:配置绑定表的关系后,外键的表数据会随着主键的表数据落入同一个库中,这样在做主外键关联查询时,就能有效避免跨库查询的情景出现。
2.2.2、广播表
在《分库分表后遗症-跨库Join问题》中同样聊到过,当有些表需要经常被用来做连表查询时,这种频繁关联查询的表,如果每次都走跨库Join
,这显然又会造成一个令人头疼的性能问题,所以对于一些经常用来做关联查询的表,就可以将其配置为广播表,广播表在有些地方也被称为同步表、网络表、全局表,但本质上表达的含义都相同,如下:
广播表是一种会在所有库中都创建的表,以系统字典表为例,将其配置为广播表之后,向其增、删、改一条或多条数据时,所有的写操作都会发给全部库执行,从而确保每个库中的表数据都一致,后续在需要做连表查询时,只需要关联自身库中的字典表即可,从而避免了跨库Join
的问题出现。
2.2.3、单表
单表的含义比较简单,并非所有的表都需要做分库分表操作,所以当一张表的数据无需分片到多个数据源中时,就可将其配置为单表,这样所有的读写操作最终都会落入这一张单表中处理。
2.2.4、动态表
动态表的概念在Sharding-Sphere
最新的5.x
文档中已经移除了,但也可以基于分片算法去实现,所以虽然移除了动态表的概念,但也可以实现相同的效果,动态表的概念是指表会随着数据增长、或随着时间推移,不断的去创建新表,如下:
大家是否还记得之前《库内分表篇》中的实现呢?当时为了处理单月数据增长过高的问题,咱们手动实现了一套按月动态分表的方案,但在Sharding-Sphere
中可以直接支持配置,无需自己去从头搭建,因此实现起来尤为简单,配置好之后会按照时间或数据量动态创建表。
2.3、Sharding-Sphere中的数据分片策略
前面聊到过,分库分表之后读写操作具体会落入哪个库中,这是根据路由键和分片算法来决定的,而Sharding-Sphere
中的数据分片策略又分为:内置的自动化分片算法、用户自定义的分片算法两大类,Sharding-Sphere
内置的算法涵盖取模分片、哈希分片、范围分片、时间分片等这积累常规算法,而自定义分片算法又可细分为:
- 标准分片算法:适合基于单一路由键进行
=、in、between、>、<、>=、<=...
进行查询的场景。 - 复合分片算法:适用于多个字段组成路由键的场景,但路由算法需要自己继承接口重写实现。
- 强制分片算法:适用于一些特殊
SQL
的强制执行,在这种模式中可以强制指定处理语句的节点。
综上所述,在Sharding-Sphere
内部将这四种分片策略称为:Inline、Standard、Complex、Hint
,分别与上述四种策略一一对应,但这四种仅代表四种策略,具体的数据分片算法,可以由使用者自身来定义(后续会结合代码实战讲解)。
2.4、Sharding-Sphere的分库方式
在Sharding-Sphere
生态中,支持传统的主从集群分库,如搭建出读写分离架构、双主双写架构,同时也支持按业务进行垂直分库,也支持对单个库进行横向拓展,做到水平分库。
但通常都是用它来实现水平分库和读写分离,因为分布式架构的系统默认都有独享库的概念,也就是分布式系统默认就会做垂直分库,因此无需引入
Sharding-Sphere
来做垂直分库。
因此接下来会搭建一个简单的SpringBoot+MyBatis
项目,结合Sharding-Sphere-JDBC
实现水平分库~
三、SpringBoot整合Sharding-JDBC框架
SpringBoot
作为一个脚手架框架,用于整合第三方框架十分轻松,比如目前想要引入Sharding-Sphere-JDBC
来做分库分表,只需要在pom.xml
中加入如下依赖即可:
<!-- shardingsphere-jdbc-jdbc依赖 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.2.1</version>
</dependency>
目前Sharding-Sphere
最新的版本就是2022.08
月发布的5.2.1
,因此这里先引入最新的依赖作为学习版本,如若是线上业务则可落后最新的一到两个版本,或者选择官方推荐的稳定版本。
3.1、搭建项目的基础结构
接着先在数据库中创建db_sharding_01、db_sharding_02
两个库,我这里用伪集群的方式搭建水平库,毕竟线上只需要把数据库地址改为不同的机器IP
即可,SQL
如下:
-- 先将编码格式改为utf8mb4
set names utf8mb4;
set foreign_key_checks = 0;
-- 接着创建两个数据库
create databases db_sharding_01;
create databases db_sharding_02;
接着分别再在两个水平库中,创建用户表、订单表、订单详情表、商品表(两张),这四张表是接下来用于测试的表,SQL
如下:
-- >>>>>>>>>>创建用户表<<<<<<<<<<<
drop table if exists `user_info`;
create table `user_info` (
`user_id` bigint not null comment '用戶id',
`user_name` varchar(255) comment '用戶姓名',
`user_sex` varchar(255) comment '用戶性別',
`user_age` int(8) not null comment '用戶年齡',
primary key (`user_id`) using btree
)
engine = InnoDB
character set = utf8
collate = utf8_general_ci
row_format = compact;
-- >>>>>>>>>>创建商品表1<<<<<<<<<<<
drop table if exists `shoping_00`;
create table `shoping_00` (
`shoping_id` bigint not null comment '商品id',
`shoping_name` varchar(255) comment '商品名称',
`shoping_price` int(8) not null comment '商品价格',
primary key (`shoping_id`) using btree
)
engine = InnoDB
character set = utf8
collate = utf8_general_ci
row_format = compact;
-- >>>>>>>>>>创建商品表2<<<<<<<<<<<
drop table if exists `shoping_01`;
create table `shoping_01` (
`shoping_id` bigint not null comment '商品id',
`shoping_name` varchar(255) comment '商品名称',
`shoping_price` int(8) not null comment '商品价格',
primary key (`shoping_id`) using btree
)
engine = InnoDB
character set = utf8
collate = utf8_general_ci
row_format = compact;
-- >>>>>>>>>>创建订单表<<<<<<<<<<<
drop table if exists `order`;
create table `order` (
`order_id` bigint not null comment '订单号',
`order_price` int(8) not null comment '订单总金额',
`user_id` bigint not null comment '用戶id',
primary key (`order_id`) using btree
)
engine = InnoDB
character set = utf8
collate = utf8_general_ci
row_format = compact;
-- >>>>>>>>>>创建订单详情表<<<<<<<<<<<
drop table if exists `order_info`;
create table `order_info` (
`order_info_id` bigint not null comment '订单详情号',
`order_id` bigint not null comment '订单号',
`shoping_name` varchar(255) comment '商品名称',
`shoping_price` int(8) not null comment '商品价格',
primary key (`order_info_id`) using btree,
index `key_order_id`(`order_id`) using btree
)
engine = InnoDB
character set = utf8
collate = utf8_general_ci
row_format = compact;
库结构和表结构创建完成后,接着是Java
端的dao、service
层的逻辑代码,但对于mapper、xml
文件、service
的逻辑代码就在此先忽略,这些大家都会的基操步骤,就不写出来占用篇幅啦,后续会附上源码地址的,最终搭建出的项目结构如下:
虽然看起来代码不少,但俺也是通过工具快速生成的,基本上敲几下键盘~,到这里为止,项目的基础结构就搭建完成啦,后面开始真正的分库分表配置。
3.2、分库分表的核心配置
之前提到过,Sharding-Sphere
的所有产品对业务代码都是零侵入的,无论是Sharding-JDBC
也好,Sharding-Proxy
也罢,都不需要更改业务代码,这也就意味着大家在分库分表环境下做业务开发时,可以像传统的单库开发一样轻松,Sharding-Sphere
中最主要的是对配置文件的更改,Sharding-JDBC
主要修改application.properties/yml
文件,Sharding-Proxy
主要修改自身的配置文件。
但这里要注意:
SpringBoot
整合Sharding-JDBC
时,官方更加推荐使用properties
的方式做分库分表配置,这样能够让Sharding-Sphere
更好的解析,如果使用yml
配置时会出现解析问题,这里需要手动做调整,也就是引入snakeyaml
的解析包,否则可能导致解析出现错误。
3.2.1、多数据源配置
接着来聊聊Sharding-JDBC
的配置方式,如下:
spring:
shardingsphere:
# 将运行模式配置为Standalone单机模式(Cluster:集群模式)
mode:
type: Standalone
repository:
type: JDBC
# 配置多个数据源
datasource:
names: ds0,ds1
# 配置第一个数据源
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: 「数据库节点1的地址」
username: 「数据库节点1的账号」
password: 「数据库节点1的密码」
# 配置第二个数据源
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: 「数据库节点2的地址」
username: 「数据库节点1的账号」
password: 「数据库节点1的密码」
上述这组配置中,需要通过names
配置多个数据源的别名,接着需要为每个别名配置对应的数据源信息,按照上述方式编写好配置文件后,则表示完成了多数据源的配置。
3.2.2、多数据源可用性测试
为了确保多数据源的可用性,接着先简单配置一张表:
spring:
shardingsphere:
# 执行时显示SQL语句
props:
# 日志显示具体的SQL
sql-show: true
# 配置分片规则
rules:
# 配置分片策略
sharding:
# 配置所有分片表
tables:
# 首先配置商品表的分片策略
shoping:
# 声明商品表所在的真实数据节点(这里先显式声明一个节点测试)
actual-data-nodes: ds0.shoping_00
然后撰写一个测试用例,来测试一下多数据源的配置是否有效:
@SpringBootTest
public class DbShardingJdbcApplicationTests {
@Test
void contextLoads() {
}
}
// shoping商品表的测试类
class ShopingServiceImplTest extends DbShardingJdbcApplicationTests {
@Autowired
private ShopingService shopingService;
// 测试数据插入的方法
@Test
void insertSelective() {
Shoping shoping = new Shoping();
shoping.setShopingId(11111111L);
shoping.setShopingName("黄金零号竹子");
shoping.setShopingPrice(8888);
shopingService.insertSelective(shoping);
}
}
执行上述测试用例后,会在控制台看到如下日志:
2022-11-25 14:41:23.096 INFO 17748 --- [main] ShardingSphere-SQL:
Logic SQL:
insert into shoping
( shoping_id, shoping_name, shoping_price )
values ( ?, ?,? )
2022-11-25 14:41:23.096 INFO 17748 --- [main] ShardingSphere-SQL: SQLStatement:
MySQLInsertStatement(.....)
2022-11-25 14:41:23.096 INFO 17748 --- [main] ShardingSphere-SQL:
Actual SQL: ds0 :::
insert into shoping_00
( shoping_id,shoping_name,shoping_price )
values (?, ?, ?) ::: [11111111, 黄金零号竹子, 8888]
前面的Logic-SQL
逻辑语句操作的是shoping
表,但后面Actual-SQL
真实语句是在操作ds0.shoping_00
表,最终查询一下数据库的表是否有数据,如下:
select * from db_sharding_01.shoping_00;
+------------+--------------------+---------------+
| shoping_id | shoping_name | shoping_price |
+------------+--------------------+---------------+
| 11111111 | 黄金零号竹子 | 8888 |
+------------+--------------------+---------------+
此时会发现表中出现了前面插入的测试数据,这也就意味着多数据源的配置已生效。
3.2.3、inline行表达式
接着可以再配置多个真实数据节点:
actual-data-nodes: ds0.shoping_00,ds0.shoping_01,ds1.shoping_00,ds1.shoping_01
可以通过上述这种方式,以逗号隔开多个真实数据节点,但这种方式在分片节点较多的情况下,配置起来就较为麻烦,因此也可直接用Sharding-Sphere
支持的行表达式语法来快捷编写,如下:
actual-data-nodes: ds$->{
0..1}.shoping_0$->{
0..1}
# 释义:
ds$->{
0..1}则表示:ds0、ds1
# 也可以这样写:
ds$->{
['0','1']}
# 也可以组合起来用:
ds$->{
['0','1']}.shoping_0$->{
0..1}
上述两者之间的区别主要在于:前者只能配置连续、均匀的分片节点,而后者相对灵活很多,可以自行指定分片节点,两种表达式语法也可结合使用,这样能够在分片不均匀的特殊场景下,灵活适用于各类业务。
3.2.4、配置分库策略
接着需要配置分库策略,也就是指定路由键和分片算法,如下:
spring:
shardingsphere:
# 配置分片规则
rules:
# 配置分片策略
sharding:
# 配置所有分片表
tables:
# 首先配置商品表的分片策略
shoping:
# 声明商品表所在的真实数据节点(这里先写死表名,便于测试)
actual-data-nodes: ds$->{
0..1}.shoping_00
# 配置分库规则
database-strategy:
standard:
# 配置路由键为shoping_id(数据库中的列名)
sharding-column: shoping_id
# 配置分片算法(需要配置一个名词,通过别名指向具体的策略)
sharding-algorithm-name: db-inline-mod
sharding-algorithms:
# 配置前面的分库算法
db-inline-mod:
# 声明是 INLINE 简单类型的分片
type: INLINE
props:
# 选择对shoping_id做取模运算
algorithm-expression: ds$->{
shoping_id % 2}
接着依旧撰写一个简单的测试用例,来实验一下分库策略是否有效,如下:
/**
* 测试分库策略是否有效
* **/
@Test
void databaseStrategyInsert() {
for (int i = 1; i <= 10; i++){
Shoping shoping = new Shoping();
shoping.setShopingId((long) i);
shoping.setShopingName("黄金"+ i +"号竹子");
shoping.setShopingPrice(1111 * i);
shopingService.insertSelective(shoping);
}
}
按照咱们配置的对shoping_id
做取模分库,理论上数据应该呈现下述形式:
ds0(db_sharding_01)
:2、4、6、8、10
ds1(db_sharding_02)
:1、3、5、7、9
那么来运行测试案例,查询一下两个库的shoping_00
表的数据看看,如下:
很显然,结果与咱们想象的数据一致,但上述配置中的取模算法,也可以直接使用Sharding-Sphere
内置的取模算法,配置方式如下:
sharding-algorithms:
# 配置一个取模算法
key-int-mod:
# 使用ShardingSphere内置的取模算法
type: MOD
props:
# 声明分库的节点数量
sharding-count: 2
通过使用内置分片算法的形式去做取模也是可以的,官方内置了取模、哈希取模、时间范围、数据范围、容量范围等多种简单的分片算法,具体的可参考《ShardingSphere官网-分片算法》。
3.2.5、配置分表策略
上面对分库规则做了配置后,那接着来配置一下分表策略,分表的路由键可以与分库的路由键不同,也可以相同,这点可以根据业务来决定,比如我这里就使用商品名称作为分表路由键,如下:
spring:
shardingsphere:
# 配置分片规则
rules:
# 配置分片策略
sharding:
# 配置所有分片表
tables:
# 首先配置商品表的分片策略
shoping:
# 声明商品表所在的真实数据节点(把原本写死的表名改成表达式)
actual-data-nodes: ds$->{
0..1}.shoping_0$->{
0..1}
# 配置分表规则
table-strategy:
standard:
# 配置分表的路由键:商品名称
sharding-column: shoping_name
sharding-algorithm-name: key-hash-mod
sharding-algorithms:
# 配置哈希取模的分表算法
key-hash-mod:
# 使用内置的哈希取模算法
type: HASH_MOD
props:
# 声明分表的节点数量
sharding-count: 2
在原本分库配置的基础上,再次新增上述分表配置,但因为选择了shoping_name
作为分表路由键,因此无法使用简单的取模分片算法,这里就选用了哈希取模分片算法,先对商品名称做一次哈希处理,接着再使用哈希值做取模运算,接着来撰写测试用例:
/**
* 测试按商品名称的分表策略是否有效
* **/
@Test
void tableStrategyInsert() {
for (int i = 1; i <= 20; i++){
Shoping shoping =
new Shoping((long) i, "白玉"+ i +"号竹子", i * 888);
shopingService.insertSelective(shoping);
}
}
测试之前先将原本表中的数据清空,接着执行上述代码,数据库中的结果如下:
此时观察四张表中的数据,数据并未出现重复,但插入的20
条测试数据,是怎么到每张表中去的呢?首先会根据shoping_id
做取模运算,将偶数ID
全部落入ds0(sharding_01)
库,将奇数ID
全部落入ds1(sharding_02)
库,接着再基于shoping_name
做哈希取模,将数据再分发到具体的表中。
当然,上述我仅只是用于参考学习,在线上环境时,对于路由键的选择一定要慎重,这将关乎到所有读写请求的走向,在路由键、分片算法配置不合理的情况下,可能会导致读写操作变得尤为复杂。
3.2.6、数据查询测试
上面配置好分库分表的规则后,插入数据都没有问题,接着再来试试查询场景,下面撰写两个测试用例,分别查询单条数据,以及查询所有数据,如下:
/**
* 根据商品ID查询单条数据
* **/
@Test
void findByShopingID() {
Shoping shoping = shopingService.selectByPrimaryKey(1L);
System.out.println(shoping);
}
此时运行该测试用例会出现如下日志:
2022-11-25 16:38:22.333 INFO 15708 --- [main] ShardingSphere-SQL: Logic SQL:
select
shoping_id, shoping_name, shoping_price
from
shoping
where
shoping_id = ?
2022-11-25 16:38:22.334 INFO 15708 --- [main] ShardingSphere-SQL: Actual SQL: ds1 :::
select
shoping_id, shoping_name, shoping_price
from
shoping_00
where
shoping_id = ?
UNION ALL
select
shoping_id, shoping_name, shoping_price
from
shoping_01
where
shoping_id = ?
::: [1, 1]
Shoping{shopingId=1, shopingName='白玉1号竹子', shopingPrice=888}
此时数据的确查询出来了,但注意上述最终执行的语句,此时会发现会通过UNION ALL
拼接查询两张表,这是为什么呢?因为咱们之前分表选择的路由键为shoping_name
,但此时是通过shoping_id
在查询数据,ShardingSphere
只能根据ID
确定查询的库,但无法确定当前查询的数据位于哪张表,所以只能将两张表全部查询一次,最终才能得到shopingId=1
的商品数据。
从这里相信大家就能明显感受出:选择一个合适的字段作为路由键的重要性,如果路由键设计的不合理,这会导致出现大量不必要产生的查询开销,因此大家在实际业务中,对路由键的选择一定要慎重!慎重!再慎重!!!
接着再撰写一个查询所有数据的测试用例,如下:
/**
* 查询所有商品数据
* **/
@Test
void queryAllShopingData() {
List<Shoping> shopings = shopingService.getAll();
shopings.forEach(System.out::println);
}
执行结果如下:
此时大家会发现,虽然将前面插入的所有数据都查询出来了,但显然没有了顺序,这是因为Sharding-Sphere
会直接按照查询表的顺序组装结果集,因此数据是无序的,如果要求按shoping_id
做排序,那可以将SQL
语句最后加上order by shoping_id asc
,这样Sharding-Sphere
在组装数据时,会自动按shoping_id
从大到小做排序。
不过虽然
Sharding-Sphere
支持order by
这种语句,但对于很多语法并不支持,如批量插入语句、批量修改语句、复杂的聚合函数、子查询、having
查询、跨库Join
查询、CASE WHEN
查询等复杂性较高的语句。
3.2.7、分布式序列生成算法
前面叨叨絮絮了很多内容,但这里遗忘了一个较为致命的问题,因为前面所有的插入测试都是咱们手动指定了主键ID
值,但实际业务中更多会依赖于数据库的自增机制,以此来确保主键的唯一性和递增性,但在之前聊《分库分表后遗症-ID主键唯一性问题》时讲到过:
如果在分库环境中再依赖于数据库自身的自增机制,这显然会造成
ID
重复的问题出现,虽然能够通过设置自增步长的方式解决,但这种形式对后续的扩容又不大友好,因此在分布式场景中,急需一种既能确保全局唯一、又能保障顺序递增的技术出现,以此来解决ID
重复这个棘手问题。
在早期的分布式系统中遇到该问题时,为了确保主键的唯一性,只能放弃递增性,选择无序的UUID
来作为主键值,直到Twitter
将Snowflake
雪花算法开源后,基本上雪花算法成为了分布式ID
的主流方案,而对于该算法,MyBatis-Plus、Sharding-Sphere
中都有内置的支持,咱们首先来做个简单配置:
spring:
shardingsphere:
# 配置分片规则
rules:
# 配置分片策略
sharding:
# 配置所有分片表
tables:
# 首先配置商品表的分片策略
shoping:
# 配置shoping表的主键生成策略
key-generate-strategy:
# 声明主键为shoping_id
column: shoping_id
# 同样指向global-id-snowflake这个具体的主键生成策略
keygenerator-name: global-id-snowflake
key-generators:
# 配置上面的主键生成策略
global-id-snowflake:
# 选择使用内置的雪花算法
type: SNOWFLAKE
props:
# 分配一个工作节点ID(要确保全局唯一)
worker-id: 111
上述这组配置的含义是:shoping_id
的值将通过雪花算法来生成,接着编写一个测试用例,来简单试试效果,如下:
/**
* 测试分布式序列算法 - 雪花算法的效果
* **/
@Test
void insertSnowflake() {
for (int i = 1; i <= 10; i++) {
Shoping shoping = new Shoping();
shoping.setShopingName("黄金"+ i +"号竹子");
shoping.setShopingPrice(8888);
shopingService.insertSelective(shoping);
}
}
注意看,在这个测试用例中咱们就没有手动指定shoping_id
值了,接着先将原本库中的表数据清空,然后运行后表的数据如下:
图中圈出的一连串数字,即是Sharding-Sphere
为咱们生成的分布式ID
,那究竟雪花算法是如何保障全局唯一性的呢?接下来一起深入聊聊雪花算法的实现原理。
3.2.8、Snowflake雪花算法的实现原理
雪花算法生成的分布式ID
,在Java
中会使用Long
类型来承载,Long
类型占位8bytes
,也就正好对应上述这张图的64
个比特位,这64bit
会被分为四部分:
- 符号位(
1bit
):永远为零,表示生成的分布式ID
为正数。 - 时间戳位(
2~42bit
):会将当前系统的时间戳插入到这段位置。 - 工作进程位(
43~53bit
):在集群环境下,每个进程唯一的工作ID
。 - 序列号位(
54~64bit
):该序列是用来在同一个毫秒内生成不同的序列号。
当需要生成一个分布式ID
时,Sharding-Sphere
首先会获取当前系统毫秒级的时间戳,放入到第2~42bit
,总共占位41
个比特,一年365
天中,一共会存在365*24*60*60*1000
个不同的毫秒时间戳,此时可以做组计算:
Math.pow(2, 41) / (365*24*60*60*1000) ≈ 69.73年
也就是41bit
的空间,可以存下大概69.73
年生成的毫秒时间戳,Sharding-Sphere
雪花算法的时间纪元是从2016.11.01
日开始的,这也就代表着使用Sharding-Sphere
雪花算法生成的分布式ID
,在未来近70
年内无需担心出现时间戳存不下的问题。
有人也许会纠结,万一我的系统会在线上运行百年之久呢?这种情况下,获取到的时间戳,就无法使用
41bit
存储下了怎么办呢?这实际上很简单,把存储ID
的Long
类型改为容量更大的引用类型即可,也就是用更大的比特位来存放时间戳。
OK~,想明白上面的问题后,接着再聊聊分布式ID
的重复问题,如果系统的并发较高,导致同一毫秒内需要生成多个ID
怎么办呢?也就是时间戳位重复的情况下该怎么确保ID
唯一性呢?其实在最后12bit
上会存放一个顺序递增的序列值,2
的12
次幂为4096
,也就意味着同一毫秒内可以生成4096
个不同的ID
值。
但似乎又出现了一个问题:当系统每毫秒的并发
ID
需求超出4096
怎么办呢?Sharding-Sphere
的做法是留到下个毫秒时间戳时再生成ID
,基本只要你的业务不是持续性的超出4096
这个阈值,Sharding-Sphere
的雪花算法都是够用的,毕竟一秒409.6w
并发量,相信能够从容应对各类业务。
但一般分布式系统中,都会采用集群的模式部署核心业务,如果使用雪花算法的节点存在多个,并且部署在不同的机器上,这会导致同一个毫秒时间戳内,出现不同的并发需求,之前说到的解决方案,由于自增序列是基于堆中的对象实现,不同机器存在多个堆空间,也就是每个节点之间都维护着各自的自增序列,因此集群环境下依旧会产生重复的分布式ID
。
为了解决这个问题,雪花算法生成分布式
ID
中,第43~53bit
会用来存储工作进程ID
,当一个服务采用了集群方案部署时,不同的节点配置不同的worker-id
即可。因为worker-id
不同,所以就算毫秒时间戳、自增序列号完全一致,依旧不会导致ID
出现冲突,从而确保分布式ID
的全局唯一性。
上述这个过程便是雪花算法的实现原理,基本上能够确保任何时刻的ID
不会出现重复,而且是基于时间戳+自增序列实现的原因,因此也能够确保ID
呈现自增性增长,从而避免索引树的频繁分裂。
3.3、Sharding-Sphere绑定表、广播表实践
前面完成了最基本的库表数据分片,接着来聊一聊Sharding-Sphere
中其他的一些表类型,如绑定表、广播表,这两种表类型也是实际业务中经常会需要使用的表类型,对于为何需要使用的缘故,在2.2.1、2.2.2
阶段已经详细说明过了,这里就不再重复赘述。
3.3.1、绑定表配置实战
在之前创建表的时候,咱们创建了order、order_info
两张表,这两张表十分具有代表性,因为订单表和订单详情表之间,存在明显的主外键关系,一笔订单可能结算多个商品,所以会生成多笔订单详情记录,订单数据和订单详情数据属于一对多的关系。
如果按照传统的分片规则,对两张表的数据做分发,这就很有可能导致一笔订单记录中,多笔订单详情记录被分发到不同的节点中存储,当需要通过关联订单表、订单详情表查询某笔订单数据时,就会出现跨库查询的情况。
为了避免上述问题产生,在Sharding-Sphere
引入了一种绑定表的概念(MyCat
中的ER
表),专门用于处理存在主外键关系的多张表,接着做如下配置:
spring:
shardingsphere:
rules:
sharding:
tables:
# 配置订单表的分片策略
order:
# 声明订单表所在的真实数据节点(ds0.order、ds1.order)
actual-data-nodes: ds$->{
0..1}.order
# 配置分库规则
database-strategy:
standard:
# 配置路由键为order_id(数据库中的列名)
sharding-column: order_id
# 配置分片算法(使用内置的取模分片算法)
sharding-algorithm-name: key-int-mod
# 配置订单表的主键生成策略
key-generate-strategy:
# 声明主键为order_id
column: order_id
# 同样使用之前的雪花算法
keygenerator-name: global-id-snowflake
# 配置订单详情表的分片策略
order_info:
# 声明商品详情表所在的真实数据节点(ds0.order_info、ds1.order_info)
actual-data-nodes: ds$->{
0..1}.order_info
# 配置分库规则
database-strategy:
standard:
# 配置路由键为order_id(这里的路由键要和订单表一致)
sharding-column: order_id
# 配置分片算法(使用内置的取模分片算法)
sharding-algorithm-name: key-int-mod
# 配置订单详情表的主键生成策略
key-generate-strategy:
# 声明主键为order_info_id
column: order_info_id
# 同样使用之前的雪花算法
keygenerator-name: global-id-snowflake
# 这里配置绑定表关系
binding-tables:
# 配置第一组绑定表的关系(订单表、订单详情表)
- order,order_info
首先将两张表的路由键和分片算法设为一致,接着最后通过binding-tables
属性来设置一组绑定表即可,此时来做一个插入测试:
class OrderServiceImplTest extends DbShardingJdbcApplicationTests {
@Autowired
private OrderService orderService;
@Autowired
private OrderInfoService orderInfoService;
/**
* 测试绑定表的效果
* **/
@Test
void orderOrOrderInfoInsert() {
// 插入一条订单数据
Order order = new Order();
order.setUserId(111111L);
order.setOrderPrice(100000);
orderService.insertSelective(order);
// 对同一笔订单插入三条订单详情数据
for (int i = 1; i <= 3; i++) {
OrderInfo orderInfo = new OrderInfo();
// 前面插入订单的方法执行完成后会返回orderID
orderInfo.setOrderId(order.getOrderId());
orderInfo.setShopingName("黄金1号竹子");
orderInfo.setShopingPrice(8888);
orderInfoService.insertSelective(orderInfo);
}
}
}
执行上述代码后,数据库两张表的结果如下:
此时会发现,测试插入的这笔订单数据的三条订单详情,都会随着订单数据落入同一个库中,而且由于配置了绑定表的原因,后续基于这两张表做关联查询时,如果是通过order_id
这个字段在做关联,Sharding-Sphere
也只会查询一个库,而不会将所有的库全部做一次笛卡尔积查询。
3.3.2、广播表配置实战
Sharding-Sphere
的广播表也就是MyCat
中的全局表,主要是针对于一些所有库中都会用到的字典表使用的,例如系统菜单表、地区表、民族表、国籍表、职级表等,这种类型的表在所有库中经常被用于关联查询,因此可以将其直接配置为广播表,我这里以用户表为例,将其配置为一张广播表:
spring:
shardingsphere:
rules:
sharding:
tables:
# 配置用户详情表的分片策略
user_info:
# 声明用户详情表所在的真实数据节点(ds0.user_info、ds1.user_info)
actual-data-nodes: ds$->{
0..1}.user_info
# 配置用户详情表的主键生成策略
key-generate-strategy:
# 声明主键为user_id
column: user_id
# 同样使用之前的雪花算法
keygenerator-name: global-id-snowflake
# 配置广播表信息
broadcast-tables:
- user_info
此时注意上述的配置,其中并未指定数据的分片策略,仅在最后将user_info
表配置成了广播表,接着来插入一些数据测试看看效果:
class UserInfoServiceImplTest extends DbShardingJdbcApplicationTests {
@Autowired
private UserInfoService userInfoService;
@Test
void insertSelective() {
// 插入三条性别为男的用户数据
for (int i = 1; i <= 3; i++){
UserInfo userInfo = new UserInfo();
userInfo.setUserName("竹子" + i + "号");
userInfo.setUserAge(18 + i);
userInfo.setUserSex("男");
userInfoService.insertSelective(userInfo);
}
// 插入两条性别为女的用户数据
for (int i = 1; i <= 2; i++){
UserInfo userInfo = new UserInfo();
userInfo.setUserName("熊猫" + i + "号");
userInfo.setUserAge(18 + i);
userInfo.setUserSex("女");
userInfoService.insertSelective(userInfo);
}
}
}
上面插入了三条性别为男、两条性别为女的用户数据,接着来运行并看看数据库结果,如下:
此时会发现,虽然咱们未曾指定用户表的分片策略,但由于将其配制成了广播表,因此对该表的所有变更操作,都会落入到所有数据节点上执行,上图的两个库中,都有插入的5
条用户数据。
也正因如此,所以无论是查询单条数据,还是查询多条数据,又或者是做关联查询,都可以在单库中完成,毕竟每个库中都具备完整的表数据。但如果变更较为频繁,或数据量较大的表,并不适合配制成广播表,因为广播表十分影响性能,需要等待所有节点插入完成后,才能向客户端返回结果。
3.4、Sharding-Sphere多种分片策略实践
经过上述的学习后,咱们已经将Sharding-Sphere
的基础用法玩明白了,在分库分表中最重要的就是路由键和分片算法,但Sharding-Sphere
内置的一些分片算法,都仅是一些较为简单的分片算法,这使得咱们在很多场景中,无法满足特殊的业务需求。
在2.3
阶段中提到过,其实Sharding-Sphere
中支持Inline、Standard、Complex、Hint
这四种分片策略,而5.x
版本中移除了原本的Inline
策略,将其改进为自动化分片策略,也就是我们口中所谓的内置算法,对于一些简单的分片场景,可直接选用这种内置算法来处理。
针对于复杂度较高的业务场景,我们可以采用后续几种分片策略,来自定义数据分片的具体实现,以此提高
Sharding-Sphere
对复杂业务的支持性,接着咱们一起来简单聊一聊。
如果有玩过Sharding-Sphere4.x
版本的小伙伴应该知道,在原先的版本中想要实现自定义分片策略,官方提供的SPI
接口过于复杂,十分难理解,因此在5.x
版本中做了优化:
4.x
中自定义Standard
分片策略的SPI
接口:RangeShardingAlgorithm<~>
:自定义范围查询分片策略时,需要实现的接口。PreciseShardingAlgorithm<~>
:实现精准查询分片策略时,需要实现的接口。
5.x
中自定义Standard
分片策略的SPI
接口:StandardShardingAlgorithm
:自定义精准查询、范围查询时,需要实现的接口。
而Complex、Hint
策略的接口依旧不变,是原有的老名字,即ComplexKeysShardingAlgorithm、HintShardingAlgorithm
两个接口,接着来实际演练一下。
3.4.1、自定义Standard分片策略实战
这种分片策略只适用于范围查询和精确查询的场景,如BETWEEN AND、>、<、>=、<=
等这类范围操作时,Sharding-Sphere
的内置分片策略(Inline
)模式下是不支持的,因此想要让你的程序支持这类范围查询操作,需要咱们手动编写对应的分片算法类,即使用Standard
策略。
但
4.1
以后的版本中,Inline
模式下也支持范围查询操作,但需要手动开启相关支持,在InlineShardingStrategy
中将allow-range-query-with-inline-sharding
设置为true
即可。
但为了版本兼容性,一般咱们都会选择自己实现Standard
策略,撰写相关的实现类,接着做个简单的演示,首先需要在shardingAlgorithms
属性下指定对应的分片算法实现类,格式如下:
sharding-algorithms:
type: CLASS_BASED
props:
strategy: STANDARD
algorithmClassName: xxx
接着基于shoping
表做个实现,算法实现类如下:
// 商品表的Standard分库策略
public class ShopStandardSA implements StandardShardingAlgorithm {
// 实现精确查询的方法(in、=查询会调用方法)
@Override
public String doSharding(Collection collection, PreciseShardingValue psv) {
// 获取逻辑表名:shoping
String logicTableName = psv.getLogicTableName();
// 获取路由键:psv.getColumnName()
// 获取本次SQL语句中具体的路由键值
long shopingID = (Long)psv.getValue();
// 将获取到的long值转换为BigInteger数值
BigInteger shopIdBI = BigInteger.valueOf(shopingID);
// 通过获取到的ID值对2取模,计算出目标表的后缀
BigInteger target = shopIdBI.mod(new BigInteger("2"));
// 拼接上逻辑表名作为前缀,得到最终的目标表名
String targetTable = logicTableName + "_0" + target;
// 判断计算出的目标表是否在Logic_DB中存在
if (collection.contains(targetTable))
// 如果配置的数据节点中有这张表,则直接返回目标表名
return targetTable;
// 不存在则抛出相应的异常信息
throw new UnsupportedOperationException(targetTable +
"表在逻辑库中不存在,请检查你的SQL语句或数据节点配置...");
}
// 实现范围查询的方法(BETWEEN AND、>、<、>=、<=会调用的方法)
@Override
public Collection<String> doSharding(Collection collection, RangeShardingValue rsv) {
// 这里实现范围查询具体的处理逻辑....
// 直接返回查询所有数据节点
return collection;
}
@Override
public Properties getProps() {
return null;
}
// 初始化方法
@Override
public void init(Properties properties) {
System.out.println("正在使用自定义的Standard分片算法......");
}
}
在上面的分片算法实现类中,实现了精准查询和范围查询的分片逻辑后,接着在yml
文件中配置一下使用该算法类即可,如下:
spring:
shardingsphere:
# 配置分片规则
rules:
# 配置分片策略
sharding:
# 配置所有分片表
tables:
# 首先配置商品表的分片策略
shoping:
# 声明商品表所在的真实数据节点(把原本写死的表名改成表达式)
actual-data-nodes: ds$->{
0..1}.shoping_0$->{
0..1}
# 配置分库规则
database-strategy:
standard:
# 配置路由键为shoping_id(数据库中的列名)
sharding-column: shoping_id
# 配置分片算法
sharding-algorithm-name: db-inline-mod
# 配置分表规则
table-strategy:
standard:
# 配置分表的路由键:商品名称
sharding-column: shoping_id
# 配置算法的实现方式指向自定义的算法类
sharding-algorithm-name: shop-standard-sharding
sharding-algorithms:
# 配置一个自定义的Standard分片算法
shop-standard-sharding:
# 声明使用自定义的算法实现类
type: CLASS_BASED
props:
# 声明分片策略
strategy: STANDARD
# 指明算法实现类(配置全限定名)
algorithmClassName: com.zhuzi.dbshardingjdbc.shardingAlgorithm.ShopStandardSA
通过上述这种方式就实现了最基本的standard
的定义,但实际上Sharding-Sphere5.x
中默认使用的即是standard
分片策略,只不过之前咱们是通过行表达式和内置算法来配置分片规则,现在换成了自定义算法类来实现分片规则。
自定义
Complex、Hint
分片策略的步骤大致相同,先实现对应接口,重写里面的doSharding()
方法,自己撰写逻辑返回对应的具体库或表,接着在yml
中配置一下对应的分片类路径即可,这里就不再重复赘述,感兴趣的可自行实验~
最后简单说明一下Complex、Hint
分片策略的适用场景:
Complex
:适用于多路由键的场景,一张表需要通过多个核心字段查询时,可以配置多个路由键,此时就需要自己实现分片路由的算法。Hint
:当一张表经常需要执行一些较为复杂的SQL
语句时,这种SQL
语句Sharding-Sphere
无法自动解析,就可以自己编写Hint
策略的实现类,强制指定这些SQL
落入到哪些节点中处理。
四、Sharding-Sphere框架总结
上述一点点的从引入概念,到Sharding-Sphere
的上手实战,携手诸位大致将分库分表实践操作进行了落地,其实相对来说也并不复杂,主要在于库表的分片规则一定要配置好,因为这是Sharding-Sphere
的核心,也是大多数分库分表技术栈的核心。不过值得吐槽的一点是:Sharding-Sphere
每个大版本之间,配置信息中的名称和样式都会存在不小差距,也正因如此,接触过之前版本的小伙伴在熟悉新版本时,会踩下很多坑~
但这点目前来说也不必太过担心,之前
3.x、4.x
系列中,因为整个生态还未彻底完善,所以每次大版本更新后,配置也会出现较大的变动,但目前的5.x
系列趋向于稳定状态,因此学习好了5.x
版本的配置,后续升级到更高的版本时,也不会出现太大的变动。
而且就算出现大变动也没关系,由于Sharding-Sphere
是当当网贡献的原因,所以Apache
官网的中文开发手册也特别详细,也不像Spring
官网那样,有些地方翻译过来语义都存在偏差,Sharding-Sphere
官网的开发手册,基本上有过开发经验的小伙伴都能读懂,撰写的较为细致。
但本文更多的是偏向于讲
Sharding-Sphere-JDBC
的水平分库,对于垂直分库却很少提及,这是由于稍具规模的项目都会采用分布式/微服务架构,本身每个核心业务服务都会具备独享库,因此也无需Sharding-Sphere
来做垂直分库。
但本文中,还有一个较为重要、且常用的技术没有提及,那就是通过Sharding-Sphere
来实现读写分离,虽然在之前的《MySQL主从实践篇》中,咱们从零开始搭建出了一套MySQL
主从架构,但读写分离的实现依旧要靠客户端来实现,客户端在分发SQL
时,将select
操作发向从库,将insert、delete、update
等操作发向主库,而Sharding-Sphere
就支持实现客户端的读写SQL
分发。
对于空缺的读写分离配置,在《MySQL系列》完结后,我会抽时间回来补齐,其实和水平分库的配置也相差不大,配置多个数据源,然后配置好分发策略即可。
4.1、浅析Sharding-Sphere工作原理
因为之前我用的是3.x
版本,因此只翻阅过3.x
系列的部分源码,这里也就不对Sharding-Sphere
的工作原理做深入展开了,咱们这里就简单聊一聊Sharding-Sphere
的工作原理,其核心工作步骤会分为如下几步:
- 配置加载:在程序启动时,会读取用户的配置好的数据源、数据节点、分片规则等信息。
SQL
解析:SQL
执行时,会先根据配置的数据源来调用对应的解析器,然后对语句进行拆解。SQL
路由:拆解SQL
后会从中得到路由键的值,接着会根据分片算法选择单或多个数据节点。SQL
改写:选择了目标数据节点后,接着会改写、优化用户的逻辑SQL
,指向真实的库、表。SQL
执行:对于要在多个数据节点上执行的语句,内部开启多线程执行器异步执行每条SQL
。- 结果归并:持续收集每条线程执行完成后返回的结果集,最终将所有线程的结果集合并。
- 结果处理:如果
SQL
中使用了order by、max()、count()...
等操作,对结果处理后再返回。
整个Sharding-Sphere
大致工作步骤如上,这个过程相对来说也比较简单,但具体的实现会比较复杂,针对于不同的数据库,内部都会实现不同的解析器,如MySQL
有MySQL
的解析器,PgSQL
也会有对应的解析器,同时还会做SQL
语句做优化。而SQL
路由时,除开要考虑最基本的数据分片算法外,还需要考虑绑定表、广播表等配置,来对具体的SQL
进行路由。
同样,对于
Sharding-Sphere
原理的深入剖析,这里也先“赊一下账”,后续时间充裕后,自己先大致摸一遍源码后,再开一篇新的《Sharding-Sphere
源码分析篇》来补齐空缺。
4.2、Sharding-JDBC/Porxy、MyCat区别
本文虽说是讲Sharding-Sphere
的教学,但主体内容更偏向于讲Sharding-JDBC
,对于Sharding-Proxy
却鲜有提及,其实这也是个人刻意为之的结果,Sharding-Proxy
的用法基本上和Sharding-JDBC
完全相同,不同的区别在于:Sharding-Proxy
只是单独拧出来部署了而已。
同时在前面我也曾提过一句,对于
SpringBoot
整合Sharding-JDBC
框架,官方更加推荐使用application.properties
的形式配置分库分表规则,包括官方文档中给出的配置示例,也是采用properties
的方式,因为通过application.yml
这种方式做配置,会需要解决一些额外的问题,但为何我在文中给出的所有配置都是基于yml
形式的呢?
其实这是为了兼容Sharding-Proxy
,Sharding-Proxy
中的分片规则就是采用yml
的形式配置,因此JDBC
中采用yml
方式配置,当需要项目再引入Proxy
做代理式分库分表时,就只需要将application.yml
中的配置信息拷贝过去做轻微改动即可。
最后咱们再来简单对比一下Sharding-JDBC、Sharding-Porxy、MyCat
三款产品之间的区别:
对比项 | Sharding-JDBC | Sharding-Proxy | MyCat |
---|---|---|---|
性能开销 | 较低 | 较高 | 高 |
异构支持 | 不支持 | 支持 | 支持 |
网络次数 | 最少一次 | 最少两次 | 最少两次 |
异构语言 | 仅支持Java | 支持异构 | 支持异构 |
数据库支持 | 任意数据库 | MySQL、PgSQL | 任意数据库 |
配置管理 | 去中心化 | 中心化 | 中心化 |
部署方式 | 依赖工程 | 中间件 | 中间件 |
业务侵入性 | 较低 | 无 | 无 |
连接开销 | 高 | 低 | 低 |
事务支持 | XA、Base、Local事务 | 同前者 | XA事务 |
功能丰富度 | 多 | 多 | 一般 |
社区活跃性 | 活跃 | 活跃 | 一言难尽 |
版本迭代性 | 高 | 高 | 极低 |
多路由键支持 | 2 | 2 | 1 |
集群部署 | 支持 | 支持 | 支持 |
分布式序列 | 雪花算法 | 雪花算法 | 自增序列 |
三款产品之间的大致区别如上,其实阿里云自带的DRDS
这款产品,相对来说比两者更为全面,但还是那个道理,好用的产品都得收费,所以大家可根据业务自行做抉择,也包括选型时也可放眼于分布式数据库方向,如TiDB
这类产品,而不仅仅将目光停留在传统的关系型数据库。
最后,如若诸位对本文中的分库分表案例感兴趣,这里也附上源码地址:《GitHub-ShardingSphere分库分表案例》,其中也包含了
Sharding-Sphere5.2.1
版本的官方开发手册-PDF版,毕竟官网访问起来速度并不那么可观,因此我顺手上传了一份Sharding-Sphere5.2.1
所有功能与技术细节,有需要的可在