Sharding-Sphere 分库分表框架综述

一、初识Apache-Sharding-Sphere生态

Apache-Sharding-Sphere的前身是当当网开源的Sharding-JDBC框架,后面引入Zookeeper作为注册中心,又研发了Sharding-Proxy中间件,贡献给Apache软件基金会后,正式整合成Sharding-Sphere生态,并且支持、兼容各种数据库,Apache-Sharding-Sphere官网上可看到的发展历程如下:

图片

发展历程

目前最新的5.2.1版本属于其中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则支持多数据源的管理,部署形态如下:

图片

JDBC部署形态

Java-ORM框架在执行SQL语句时,Sharding-JDBC会以切面的形式拦截发往数据库的语句,接着根据配置好的数据源、分片规则和路由键,为SQL选择一个目标数据源,然后再发往对应的数据库节点处理。

Sharding-JDBC在整个业务系统中对性能损耗极低,但为何后面又会推出Sharding-Proxy呢?因为Sharding-JDBC配置较为麻烦,比如在分布式系统中,任何使用分库分表的服务都需要单独配置多数据源地址、路由键、分片策略....等信息,同时它也仅支持Java语言,当一个系统是用多语言异构的,此时其他语言开发的子服务,则无法使用分库分表策略。

1.2、Sharding-Proxy中间件简介

也正是由于配置无法统一管理、不支持异构系统的原因,后面又引入Sharding-Proxy来解决这两个问题,Sharding-Proxy可以将其理解成一个伪数据库,对于应用程序而言是完全透明的,它会以中间件的形式独立部署在系统中,部署形态如下:

图片

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整合Sharding-JDBC框架

<!-- 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如下:

-- >>>>>>>>>>创建用户表<<<<<<<<<<<
droptableifexists`user_info`;
createtable`user_info`(
`user_id`bigintnotnull comment '用戶id',
`user_name`varchar(255) comment '用戶姓名',
`user_sex`varchar(255) comment '用戶性別',
`user_age`int(8)notnull comment '用戶年齡',
primary key (`user_id`)using btree
)
engine =InnoDB
characterset= utf8
collate= utf8_general_ci 
row_format = compact;

-- >>>>>>>>>>创建商品表1<<<<<<<<<<<
droptableifexists`shoping_00`;
createtable`shoping_00`(
`shoping_id`bigintnotnull comment '商品id',
`shoping_name`varchar(255) comment '商品名称',
`shoping_price`int(8)notnull comment '商品价格',
primary key (`shoping_id`)using btree
)
engine =InnoDB
characterset= utf8
collate= utf8_general_ci 
row_format = compact;

-- >>>>>>>>>>创建商品表2<<<<<<<<<<<
droptableifexists`shoping_01`;
createtable`shoping_01`(
`shoping_id`bigintnotnull comment '商品id',
`shoping_name`varchar(255) comment '商品名称',
`shoping_price`int(8)notnull comment '商品价格',
primary key (`shoping_id`)using btree
)
engine =InnoDB
characterset= utf8
collate= utf8_general_ci 
row_format = compact;

-- >>>>>>>>>>创建订单表<<<<<<<<<<<
droptableifexists`order`;
createtable`order`(
`order_id`bigintnotnull comment  '订单号',
`order_price`int(8)notnull comment '订单总金额',
`user_id`bigintnotnull comment '用戶id',
primary key (`order_id`)using btree
)
engine =InnoDB
characterset= utf8
collate= utf8_general_ci 
row_format = compact;

-- >>>>>>>>>>创建订单详情表<<<<<<<<<<<
droptableifexists`order_info`;
createtable`order_info`(
`order_info_id`bigintnotnull comment  '订单详情号',
`order_id`bigintnotnull comment '订单号',
`shoping_name`varchar(255)  comment '商品名称',
`shoping_price`int(8)notnull comment '商品价格',
primary key (`order_info_id`)using btree,
  index `key_order_id`(`order_id`)using btree
)
engine =InnoDB
characterset= utf8
collate= utf8_general_ci 
row_format = compact;

库结构和表结构创建完成后,最终搭建出的项目结构如下

图片

项目结构

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
publicclassDbShardingJdbcApplicationTests{
@Test
voidcontextLoads(){
}
}

// shoping商品表的测试类
classShopingServiceImplTestextendsDbShardingJdbcApplicationTests{

@Autowired
privateShopingService shopingService;

// 测试数据插入的方法
@Test
voidinsertSelective(){
Shopingshoping=newShoping();
        shoping.setShopingId(11111111L);
        shoping.setShopingName("黄金零号竹子");
        shoping.setShopingPrice(8888);
        shopingService.insertSelective(shoping);
}
}

执行上述测试用例后,会在控制台看到如下日志:

2022-11-2514:41:23.096  INFO 17748---[main]ShardingSphere-SQL:
Logic SQL:
        insert into shoping
( shoping_id, shoping_name, shoping_price )
                values (?,?,?)

2022-11-2514:41:23.096 INFO 17748---[main]ShardingSphere-SQL:SQLStatement:
MySQLInsertStatement(.....)

2022-11-2514: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
voiddatabaseStrategyInsert(){
for(inti=1; i <=10; i++){
Shopingshoping=newShoping();
        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

通过使用内置分片算法的形式去做取模也是可以的,官方内置了取模、哈希取模、时间范围、数据范围、容量范围等多种简单的分片算法。

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
voidtableStrategyInsert(){
for(inti=1; i <=20; i++){
Shopingshoping=
newShoping((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-2516:38:22.333  INFO 15708---[main]ShardingSphere-SQL:Logic SQL:
select
        shoping_id, shoping_name, shoping_price
from
        shoping
where
        shoping_id =?

2022-11-2516: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重复这个棘手问题。

在早期的分布式系统中遇到该问题时,为了确保主键的唯一性,只能放弃递增性,选择无序的UUID来作为主键值,直到TwitterSnowflake雪花算法开源后,基本上雪花算法成为了分布式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
voidinsertSnowflake(){
for(inti=1; i <=10; i++){
Shopingshoping=newShoping();
        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存储下了怎么办呢?这实际上很简单,把存储IDLong类型改为容量更大的引用类型即可,也就是用更大的比特位来存放时间戳。

接着再聊聊分布式ID的重复问题,如果系统的并发较高,导致同一毫秒内需要生成多个ID怎么办?也就是时间戳位重复的情况下该怎么确保ID唯一性呢?其实在最后12bit上会存放一个顺序递增的序列值,212次幂为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 OrderServiceImplTestextendsDbShardingJdbcApplicationTests{
@Autowired
privateOrderService orderService;

@Autowired
privateOrderInfoService orderInfoService;

/**
     * 测试绑定表的效果
     * **/
@Test
voidorderOrOrderInfoInsert(){
// 插入一条订单数据
Orderorder=newOrder();
        order.setUserId(111111L);
        order.setOrderPrice(100000);
        orderService.insertSelective(order);

// 对同一笔订单插入三条订单详情数据
for(inti=1; i <=3; i++){
OrderInfoorderInfo=newOrderInfo();
// 前面插入订单的方法执行完成后会返回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 UserInfoServiceImplTestextendsDbShardingJdbcApplicationTests{
@Autowired
privateUserInfoService userInfoService;

@Test
voidinsertSelective(){
// 插入三条性别为男的用户数据
for(inti=1; i <=3; i++){
UserInfouserInfo=newUserInfo();
            userInfo.setUserName("竹子"+ i +"号");
            userInfo.setUserAge(18+ i);
            userInfo.setUserSex("男");
            userInfoService.insertSelective(userInfo);
}

// 插入两条性别为女的用户数据
for(inti=1; i <=2; i++){
UserInfouserInfo=newUserInfo();
            userInfo.setUserName("熊猫"+ i +"号");
            userInfo.setUserAge(18+ i);
            userInfo.setUserSex("女");
            userInfoService.insertSelective(userInfo);
}
}
}

上面插入了三条性别为男、两条性别为女的用户数据,接着来运行并看看数据库结果,如下:

图片

广播表

此时会发现,虽然咱们未曾指定用户表的分片策略,但由于将其配制成了广播表,因此对该表的所有变更操作,都会落入到所有数据节点上执行,上图的两个库中,都有插入的5条用户数据。

也正因如此,所以无论是查询单条数据,还是查询多条数据,又或者是做关联查询,都可以在单库中完成,毕竟每个库中都具备完整的表数据。但如果变更较为频繁,或数据量较大的表,并不适合配制成广播表,因为广播表十分影响性能,需要等待所有节点插入完成后,才能向客户端返回结果。

3.4、Sharding-Sphere多种分片策略实践

在分库分表中最重要的就是路由键和分片算法,但Sharding-Sphere内置的一些分片算法,都仅是一些较为简单的分片算法,这使得咱们在很多场景中,无法满足特殊的业务需求。

2.3阶段中提到过,其实Sharding-Sphere中支持Inline、Standard、Complex、Hint这四种分片策略,而5.x版本中移除了原本的Inline策略,将其改进为自动化分片策略,也就是我们口中所谓的内置算法,对于一些简单的分片场景,可直接选用这种内置算法来处理。

针对于复杂度较高的业务场景,我们可以采用后续几种分片策略,来自定义数据分片的具体实现,以此提高Sharding-Sphere对复杂业务的支持性。

  • • 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分库策略
publicclassShopStandardSAimplementsStandardShardingAlgorithm{

// 实现精确查询的方法(in、=查询会调用方法)
@Override
publicStringdoSharding(Collection collection, PreciseShardingValue psv){
// 获取逻辑表名:shoping
StringlogicTableName= psv.getLogicTableName();
// 获取路由键:psv.getColumnName()
// 获取本次SQL语句中具体的路由键值
longshopingID=(Long)psv.getValue();
// 将获取到的long值转换为BigInteger数值
BigIntegershopIdBI=BigInteger.valueOf(shopingID);
// 通过获取到的ID值对2取模,计算出目标表的后缀
BigIntegertarget= shopIdBI.mod(newBigInteger("2"));
// 拼接上逻辑表名作为前缀,得到最终的目标表名
StringtargetTable= logicTableName +"_0"+ target;
// 判断计算出的目标表是否在Logic_DB中存在
if(collection.contains(targetTable))
// 如果配置的数据节点中有这张表,则直接返回目标表名
return targetTable;
// 不存在则抛出相应的异常信息
thrownewUnsupportedOperationException(targetTable +
"表在逻辑库中不存在,请检查你的SQL语句或数据节点配置...");
}

// 实现范围查询的方法(BETWEEN AND、>、<、>=、<=会调用的方法)
@Override
publicCollection<String>doSharding(Collection collection, RangeShardingValue rsv){
// 这里实现范围查询具体的处理逻辑....

// 直接返回查询所有数据节点
return collection;
}

@Override
publicPropertiesgetProps(){
returnnull;
}

// 初始化方法
@Override
publicvoidinit(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-JDBC的水平分库,对于垂直分库却很少提及,这是由于稍具规模的项目都会采用分布式/微服务架构,本身每个核心业务服务都会具备独享库,因此也无需Sharding-Sphere来做垂直分库。

4.1、浅析Sharding-Sphere工作原理

简单聊一聊Sharding-Sphere的工作原理,其核心工作步骤会分为如下几步:

  • • 配置加载:在程序启动时,会读取用户的配置好的数据源、数据节点、分片规则等信息。

  • • SQL解析:SQL执行时,会先根据配置的数据源来调用对应的解析器,然后对语句进行拆解。

  • • SQL路由:拆解SQL后会从中得到路由键的值,接着会根据分片算法选择单或多个数据节点。

  • • SQL改写:选择了目标数据节点后,接着会改写、优化用户的逻辑SQL,指向真实的库、表。

  • • SQL执行:对于要在多个数据节点上执行的语句,内部开启多线程执行器异步执行每条SQL

  • • 结果归并:持续收集每条线程执行完成后返回的结果集,最终将所有线程的结果集合并。

  • • 结果处理:如果SQL中使用了order by、max()、count()...等操作,对结果处理后再返回。

整个Sharding-Sphere大致工作步骤如上,这个过程相对来说也比较简单,但具体的实现会比较复杂,针对于不同的数据库,内部都会实现不同的解析器,如MySQLMySQL的解析器,PgSQL也会有对应的解析器,同时还会做SQL语句做优化。而SQL路由时,除开要考虑最基本的数据分片算法外,还需要考虑绑定表、广播表等配置,来对具体的SQL进行路由。

4.2、Sharding-JDBC/Porxy、MyCat区别

Sharding-Proxy的用法基本上和Sharding-JDBC完全相同,不同的区别在于:Sharding-Proxy只是单独拧出来部署了而已。

对于SpringBoot整合Sharding-JDBC框架,官方更加推荐使用application.properties的形式配置分库分表规则,包括官方文档中给出的配置示例,也是采用properties的方式,因为通过application.yml这种方式做配置,会需要解决一些额外的问题。

其实这是为了兼容Sharding-ProxySharding-Proxy中的分片规则就是采用yml的形式配置,因此JDBC中采用yml方式配置,当需要项目再引入Proxy做代理式分库分表时,就只需要将application.yml中的配置信息拷贝过去做轻微改动即可。

最后咱们再来简单对比一下Sharding-JDBC、Sharding-Porxy、MyCat三款产品之间的区别:

对比项Sharding-JDBCSharding-ProxyMyCat
性能开销较低较高
异构支持不支持支持支持
网络次数最少一次最少两次最少两次
异构语言仅支持Java支持异构支持异构
数据库支持任意数据库MySQL、PgSQL任意数据库
配置管理去中心化中心化中心化
部署方式依赖工程中间件中间件
业务侵入性较低
连接开销
事务支持XA、Base、Local事务同前者XA事务
功能丰富度一般
社区活跃性活跃活跃一言难尽
版本迭代性极低
多路由键支持221
集群部署支持支持支持
分布式序列雪花算法雪花算法自增序列
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值