(二十六)MySQL分库篇:Sharding-Sphere分库分表框架的保姆级教学!

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云原生网关 MSE Higress,422元/月
简介: 前面《MySQL主从原理篇》、《MySQL主从实践篇》两章中聊明白了MySQL主备读写分离、多主多写热备等方案,但如果这些高可用架构依旧无法满足业务规模,或业务增长的需要,此时就需要考虑选用分库分表架构。

引言

   前面《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生态

001.png

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

002.png

目前最新的5.2.1版本属于其中5.x阶段的一个版本,目前支持可可拔插功能,其支持的核心功能如下:

003.png

基本上对于《分库分表副作用篇》中聊到的各类问题,在该版本中都有对应的解决方案,如多表连查、数据分片、分布式事务、数据迁移与扩容、分布式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则支持多数据源的管理,部署形态如下:

004.png

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

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

1.2、Sharding-Proxy中间件简介

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

005.png

使用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做分库分表,部署形态如下:

006.png

这种混合式的部署方案,所有的数据分片策略都会放到Zookeeper中统一管理,然后所有的子服务都去Zookeeper中拉取配置文件,这样就能很方便的根据业务情况,来灵活的搭建适用于各种场景的应用系统,这样也能够让数据源、分片策略、路由键....等配置信息灵活,可以在线上动态修改配置信息,修改后能够在线上环境中动态感知。

Sharding-Sphere还提供了一种单机模式,即直接将数据分片配置放在Proxy中,但这种方式仅适用于开发环境,因为无法将分片配置同步给多个实例使用,也就意味着会导致其他实例由于感知不到配置变化,从而造成配置信息不一致的错误。

二、Sharding-Sphere中的核心概念

   分库分表中最重要的核心概念有两个,即路由键和分片算法,这两个将决定数据分片的位置,先稍微解释一下这两个概念:

  • 路由键:也被称为分片键,也就是作为数据分片的基准字段,可以是一个或多个字段组成。
  • 分片算法:基于路由键做一定逻辑处理,从而计算出一个最终节点位置的算法。

举个例子来感受一下,好比按user_id将用户表数据分片,每八百万条数据划分一张表,那在这里,user_id就是路由键,而按user_id做范围判断则属于分片算法,一张表中的所有数据都会依据这两个基础,后续对所有的读写SQL进行改写,从而定位到具体的库、表位置。

2.1、分库分表的工作流程

007.png

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、绑定表

008.png

《分库分表后遗症-主外键约束》中聊到过,当多张表之间存在物理或逻辑上的主外键关系,如果无法保障同一主键值的外键数据落入同一节点,显然在查询时就会发生跨库查询,这无疑对性能影响是极大的,所以在其中也提到过可以使用绑定表的形式解决该问题。

比如前面案例中的order_id、order_info_id可以配置一组绑定表关系,这样就能够让订单详情数据随着订单数据一同落库,简单的说就是:配置绑定表的关系后,外键的表数据会随着主键的表数据落入同一个库中,这样在做主外键关联查询时,就能有效避免跨库查询的情景出现。

2.2.2、广播表

009.png

《分库分表后遗症-跨库Join问题》中同样聊到过,当有些表需要经常被用来做连表查询时,这种频繁关联查询的表,如果每次都走跨库Join,这显然又会造成一个令人头疼的性能问题,所以对于一些经常用来做关联查询的表,就可以将其配置为广播表,广播表在有些地方也被称为同步表、网络表、全局表,但本质上表达的含义都相同,如下:

010.png

广播表是一种会在所有库中都创建的表,以系统字典表为例,将其配置为广播表之后,向其增、删、改一条或多条数据时,所有的写操作都会发给全部库执行,从而确保每个库中的表数据都一致,后续在需要做连表查询时,只需要关联自身库中的字典表即可,从而避免了跨库Join的问题出现。

2.2.3、单表

单表的含义比较简单,并非所有的表都需要做分库分表操作,所以当一张表的数据无需分片到多个数据源中时,就可将其配置为单表,这样所有的读写操作最终都会落入这一张单表中处理。

2.2.4、动态表

动态表的概念在Sharding-Sphere最新的5.x文档中已经移除了,但也可以基于分片算法去实现,所以虽然移除了动态表的概念,但也可以实现相同的效果,动态表的概念是指表会随着数据增长、或随着时间推移,不断的去创建新表,如下:

011.png

大家是否还记得之前《库内分表篇》中的实现呢?当时为了处理单月数据增长过高的问题,咱们手动实现了一套按月动态分表的方案,但在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的逻辑代码就在此先忽略,这些大家都会的基操步骤,就不写出来占用篇幅啦,后续会附上源码地址的,最终搭建出的项目结构如下:

012.png

虽然看起来代码不少,但俺也是通过工具快速生成的,基本上敲几下键盘~,到这里为止,项目的基础结构就搭建完成啦,后面开始真正的分库分表配置。

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表的数据看看,如下:

013.png

很显然,结果与咱们想象的数据一致,但上述配置中的取模算法,也可以直接使用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);
    }
}

测试之前先将原本表中的数据清空,接着执行上述代码,数据库中的结果如下:

014.png

此时观察四张表中的数据,数据并未出现重复,但插入的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);
}

执行结果如下:

015.png

此时大家会发现,虽然将前面插入的所有数据都查询出来了,但显然没有了顺序,这是因为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来作为主键值,直到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
void insertSnowflake() {
   
   
    for (int i = 1; i <= 10; i++) {
   
   
        Shoping shoping = new Shoping();
        shoping.setShopingName("黄金"+ i +"号竹子");
        shoping.setShopingPrice(8888);
        shopingService.insertSelective(shoping);
    }
}

注意看,在这个测试用例中咱们就没有手动指定shoping_id值了,接着先将原本库中的表数据清空,然后运行后表的数据如下:

017.png

图中圈出的一连串数字,即是Sharding-Sphere为咱们生成的分布式ID,那究竟雪花算法是如何保障全局唯一性的呢?接下来一起深入聊聊雪花算法的实现原理。

3.2.8、Snowflake雪花算法的实现原理

016.png

雪花算法生成的分布式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类型改为容量更大的引用类型即可,也就是用更大的比特位来存放时间戳。

OK~,想明白上面的问题后,接着再聊聊分布式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 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);
        }
    }
}

执行上述代码后,数据库两张表的结果如下:

018.png

此时会发现,测试插入的这笔订单数据的三条订单详情,都会随着订单数据落入同一个库中,而且由于配置了绑定表的原因,后续基于这两张表做关联查询时,如果是通过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);
        }
    }
}

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

019.png

此时会发现,虽然咱们未曾指定用户表的分片策略,但由于将其配制成了广播表,因此对该表的所有变更操作,都会落入到所有数据节点上执行,上图的两个库中,都有插入的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大致工作步骤如上,这个过程相对来说也比较简单,但具体的实现会比较复杂,针对于不同的数据库,内部都会实现不同的解析器,如MySQLMySQL的解析器,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-ProxySharding-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版,毕竟官网访问起来速度并不那么可观,因此我顺手上传了一份PDF文档,里面涵盖了Sharding-Sphere5.2.1所有功能与技术细节,有需要的可在PDF目录下查找。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3月前
|
存储 算法 关系型数据库
(二十二)全解MySQL之分库分表后带来的“副作用”一站式解决方案!
上篇《分库分表的正确姿势》中已经将分库分表的方法论全面阐述清楚了,总体看下来用一个字形容,那就是爽!尤其是分库分表技术能够让数据存储层真正成为三高架构,但前面爽是爽了,接着一起来看看分库分表后产生一系列的后患问题,注意我这里的用词,是一系列而不是几个,也就是分库分表虽然好,但你要解决的问题是海量的。
358 3
|
21天前
|
SQL Java 关系型数据库
java连接mysql查询数据(基础版,无框架)
【10月更文挑战第12天】该示例展示了如何使用Java通过JDBC连接MySQL数据库并查询数据。首先在项目中引入`mysql-connector-java`依赖,然后通过`JdbcUtil`类中的`main`方法实现数据库连接、执行SQL查询及结果处理,最后关闭相关资源。
|
29天前
|
SQL Oracle 关系型数据库
安装最新 MySQL 8.0 数据库(教学用)
安装最新 MySQL 8.0 数据库(教学用)
106 4
|
2月前
|
存储 SQL 关系型数据库
一篇文章搞懂MySQL的分库分表,从拆分场景、目标评估、拆分方案、不停机迁移、一致性补偿等方面详细阐述MySQL数据库的分库分表方案
MySQL如何进行分库分表、数据迁移?从相关概念、使用场景、拆分方式、分表字段选择、数据一致性校验等角度阐述MySQL数据库的分库分表方案。
395 15
一篇文章搞懂MySQL的分库分表,从拆分场景、目标评估、拆分方案、不停机迁移、一致性补偿等方面详细阐述MySQL数据库的分库分表方案
|
1月前
|
前端开发 Java 数据库连接
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
本文是一份全面的表白墙/留言墙项目教程,使用SpringBoot + MyBatis技术栈和MySQL数据库开发,涵盖了项目前后端开发、数据库配置、代码实现和运行的详细步骤。
41 0
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
3月前
|
搜索推荐 前端开发 数据可视化
基于Python协同过滤的旅游景点推荐系统,采用Django框架,MySQL数据存储,Bootstrap前端,echarts可视化实现
本文介绍了一个基于Python协同过滤算法的旅游景点推荐系统,该系统采用Django框架、MySQL数据库、Bootstrap前端和echarts数据可视化技术,旨在为用户提供个性化的旅游推荐服务,提升用户体验和旅游市场增长。
276 9
基于Python协同过滤的旅游景点推荐系统,采用Django框架,MySQL数据存储,Bootstrap前端,echarts可视化实现
|
3月前
|
搜索推荐 前端开发 算法
基于用户画像及协同过滤算法的音乐推荐系统,采用Django框架、bootstrap前端,MySQL数据库
本文介绍了一个基于用户画像和协同过滤算法的音乐推荐系统,使用Django框架、Bootstrap前端和MySQL数据库构建,旨在为用户提供个性化的音乐推荐服务,提高推荐准确性和用户满意度。
251 7
基于用户画像及协同过滤算法的音乐推荐系统,采用Django框架、bootstrap前端,MySQL数据库
|
3月前
|
Java 应用服务中间件 Maven
从零到英雄:一步步构建你的首个 JSF 应用程序,揭开 JavaServer Faces 的神秘面纱
【8月更文挑战第31天】JavaServer Faces (JSF) 是一种强大的 Java EE 标准,用于构建企业级 Web 应用。它提供了丰富的组件库和声明式页面描述语言 Facelets,便于开发者快速开发功能完善且易于维护的 Web 应用。本文将指导你从零开始构建一个简单的 JSF 应用,包括环境搭建、依赖配置、Managed Bean 编写及 Facelets 页面设计。
91 0
|
3月前
|
SQL 关系型数据库 MySQL
【超全整理】SQL日期与时间函数大汇总会:MySQL与SQL Server双轨对比教学,助你轻松搞定时间数据处理难题!
【8月更文挑战第31天】本文介绍了在不同SQL数据库系统(如MySQL、SQL Server、Oracle)中常用的日期与时间函数,包括DATE、NOW()、EXTRACT()、DATE_ADD()、TIMESTAMPDIFF()及日期格式化等,并提供了具体示例。通过对比这些函数在各系统中的使用方法,帮助开发者更高效地处理日期时间数据,满足多种应用场景需求。
347 0
|
3月前
|
SQL 关系型数据库 MySQL
(二十五)MySQL主从实践篇:超详细版读写分离、双主热备架构搭建教学
在上篇《主从原理篇》中,基本上把主从复制原理、主从架构模式、数据同步方式、复制技术优化.....等各类细枝末节讲清楚了,本章则准备真正对聊到的几种主从模式落地实践,但实践的内容通常比较枯燥乏味,因为就是调整各种配置、设置各种参数等步骤。
540 2