ShardingSphere-JDBC入门到精通


在数据量较大的场景下分库分表就变成了不可或缺的技术,阿里开发规范建议单表500万或者2GB应该进行分库分表的拆分,不过这也并不是唯一准则,我们应该根据实际场景来进行判断,如果业务上并不需要这么高的响应速度,那我们是可以根据实际需要进行调整分库分表的界限的。ShardingSphere与MyCat是两个使用最广的开源的分库分表框架,其中ShardingSphere支持JDBC层的分库分表和DB Server代理层的分库分表,MyCat则主要是做DB Server代理层的分库分表。本篇文章用以总结ShardingSphere的分库分表方案。
sharding-jdbc 官方文档https://shardingsphere.apache.org/document/5.1.1/cn/overview/

一、Mysql主从搭建

实现分库分表,需要先有一个主从的数据库环境,这里使用一主多从的Mysql进行演示主从搭建,不过这里的一主多从只做基础的分库分表的配置,其他配置采用默认值不进行显示的配置。Mysql的主从同步的原理是通过Binlog进行主从数据的同步,下面是一张原理图
在这里插入图片描述

主从复制过程:
1.主库更新数据写入binlog
2.主库使用binlog dump线程通知从库
2…从库读取主库的binlog日志,将其写入到自己的中继日志(relay log)
3.从库将中继日志中的数据写入到从库中,注意这个过程默认单线程,5.7以后支持多线程配置

主从复制必要不充分条件:
1.主库开启binlog配置
2.主从的server-id不同
3.从库中继日志开启

1.主从搭建-主库

这里使用docker进行环境搭建,Mysql选用8.0.30的镜像。配置文件和数据使用数据卷的方式映射到宿主机,下面是主机的搭建命过程

1.1 创建主库

这里进行分步骤说明,具体如下:

  • 创建所需文件夹

    # 创建所需文件夹
    mkdir -p /apps/mysql/config
    mkdir -p /apps/mysql/data/mysql
    
  • 创建主库

    docker run --restart=always -d \
    -v /apps/mysql/config:/etc/mysql/conf.d \
    -v /apps/mysql/data/mysql:/var/lib/mysql \
    -p 3306:3306 \
    --name mysql8-master \
    -e MYSQL_ROOT_PASSWORD=super mysql:8.0.30
    
  • 新增配置文件
    新增配置文件

    vim /apps/mysql/config/my.cnf
    

    内容如下:

    [mysqld]
    server-id = 1                                  # 节点ID,主从之中唯一不可重复
    log-bin = mysql-bin                            #开启mysql的binlog日志功能,binlog日志位置
    sync_binlog = 1                                #控制数据库的binlog刷到磁盘上去 , 0 不控制,性能最好,1每次事物提交都会刷到日志文件中,性能最差,最安全
    binlog_format = mixed                          #binlog日志格式,mysql默认采用statement,建议使用mixed
    expire_logs_days = 7                           #binlog过期清理时间
    max_binlog_size = 512m                         #binlog每个日志文件大小
    binlog_cache_size = 4m                         #binlog缓存大小
    
    # binlog-do-db=指定同步的数据库名1                #需要同步的数据库,多库多行配置
    # binlog-do-db=指定同步的数据库名1                #需要同步的数据库
    binlog-ignore-db=mysql                          # 设置不需要复制的数据,可设置多行
    binlog-ignore-db=infomation_schema
    
    max_binlog_cache_size= 512m                     #最大binlog缓存大
    
    auto-increment-offset = 1                      # 自增值的偏移量
    auto-increment-increment = 1                   # 自增值的自增量
    slave-skip-errors = all                        #跳过从库错误
    

    参数说明:binlog_format
    binlog格式有三种,分别如下:
    ROW:直接记录行数据,set update=now() 这里记录的是当前的时间,而不是now()主从同步时不会有问题,但是效率不是很高
    STATEMENT:记录的是执行语句,比如set update=now(),binlog直接记录这个语句,因此从库和主库的更新时间可能不一样
    MIXED:以上两者的混和,非函数部分使用STATEMENT记录提高效率,函数部分使用ROW记录保持准确。

    参数说明:binlog-do-db 与 binlog-ignore-db
    这俩参数用于指定复制的库和不复制的库,逻辑如下:
    如果只配置了binlog-do-db:则满足的记录binlog,不满足的不记录
    如果只配置了binlog-ignore-db:满足的直接不记录,不满足的记录
    如果都配了:按照binlog-do-db的规则走

  • 重启Mysql连接测试
    重启使用docker restart 容器id,即可实现重启,然后使用navicat进行连接测试(注意网络与防火墙端口方向):

    # 使用如下命令进入容器可以防止容器内部的中文乱码,同时支持容器内部输入中文
    docker exec -it 容器id env LANG=C.UTF-8 /bin/bash
    

    在这里插入图片描述

    这里如果连接报了如下异常(根本原因是你的连接工具版本太低,不适配Mysql8的密码验证方式):
    在这里插入图片描述

    请在服务端执行以下命令,更改Mysql密码验证方式为Mysql5.7的就行(放心执行对其他没有任何影响):

    # 修改用户的密码验证方式,和密码
    alter user 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'super';
    # 刷新权限
    flush privileges;
    

    注意,如果没有报上面的错误,也需要进行调整,后面的ShardingSphere也是使用这种密码验证方式

1.2 创建用于主从复制的用户

这里创建的用户专用于组从复制使用,并不会赋予其他使用权限,不可作为其他常见的用户使用。
命令如下:

# 创建用户,所有ip可用
create user 'slave_user'@'%';
# 设置密码
alter user 'slave_user'@'%' IDENTIFIED WITH mysql_native_password BY 'super';
# 授予复制权限
grant replication slave on *.* to 'slave_user'@'%';
# 刷新权限
flush privileges;

在这里插入图片描述

1.3 查询主机binlog位置信息

这里的查询结果一会从库需要使用,所以在此期间尽量不要使用,防止位置发生变化,命令如下:

show master status;

下面是我的信息,第一列是我们的binlog,第二列是binlog文件中目前的使用位置,这俩信息一会搭建从库需要用的。
在这里插入图片描述

2.主从搭建-从库

从服务器这里会搭建在一台机子上进行演示,他们的配置文件和数据文件会分开存放。

  • 创建映射文件夹

    
    # 从节点1
    mkdir -p /apps/mysql3306/config
    mkdir -p /apps/mysql3306/data/mysql
    
    # 从节点2
    mkdir -p /apps/mysql3307/config
    mkdir -p /apps/mysql3307/data/mysql
    
  • 创建从库
    这里和上面创建主库几乎没有区别,主要是名称、端口不同了,以下是两个节点的建库语句

    # 从节点1
    docker run --restart=always -d \
    -v /apps/mysql3306/config:/etc/mysql/conf.d \
    -v /apps/mysql3306/data/mysql:/var/lib/mysql \
    -p 3306:3306 \
    --name mysql8-slave1 \
    -e MYSQL_ROOT_PASSWORD=super mysql:8.0.30
    
    # 从节点2 
    docker run --restart=always -d \
    -v /apps/mysql3307/config:/etc/mysql/conf.d \
    -v /apps/mysql3307/data/mysql:/var/lib/mysql \
    -p 3307:3306 \
    --name mysql8-slave2 \
    -e MYSQL_ROOT_PASSWORD=super mysql:8.0.30
    
  • 创建从库的配置文件
    这里两个配置文件的信息基本是一样的,只有server-id不同

    下面是slave1配置内容:
    vim /apps/mysql3306/config/my.cnf

    [mysqld]
    server-id=2 # 主从集群中不可重复
    # log-bin=mysql-bin #如果从数据库,不需要再往其他数据库同步,可以注释掉
    relay-log=slave-relay-bin #必须开启,从主数据库同步的binlog会写入到该目录下
    relay-log-index=slave-relay-bin
    #如果主从数据库名称相同
    # replication-do-db=数据库名
    #如果主从数据库名称不同
    # replication-rewrite-db= 主数据库名 -> 从数据库名
    

    下面是slave2配置内容:
    vim /apps/mysql3307/config/my.cnf

    [mysqld]
    server-id=3 # 主从集群中不可重复
    # log-bin=mysql-bin #如果从数据库,不需要再往其他数据库同步,可以注释掉
    relay-log=slave-relay-bin #必须开启,从主数据库同步的binlog会写入到该目录下
    relay-log-index=slave-relay-bin
    #如果主从数据库名称相同
    # replication-do-db=数据库名
    #如果主从数据库名称不同
    # replication-rewrite-db= 主数据库名 -> 从数据库名
    
  • 放行root用户对ip等的访问限制
    注意两个slave都需要执行下面的命令

    # 登录mysql容器内部
    docker exec -it 容器id env LANG=C.UTF-8 /bin/bash
    # 设置密码,更新验证方式
    alter user 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'super';
    # 刷新权限
    flush privileges;
    

3.主从搭建-启动主从配置

这里配置分为两部分,首先应该配置从服务器的主节点信息,

3.1 配置从节点的主库信息

这一步是为了告诉从节点,谁是他的主节点,注意两个节点都需要执行下面的命令

# 注意这里的信息配置的都是当前从节点的主库信息,基本都在第1部分
CHANGE MASTER TO MASTER_HOST='192.168.150.140',MASTER_USER='slave_user',MASTER_PASSWORD='super',MASTER_PORT=3306,MASTER_LOG_FILE='mysql-bin.000002',MASTER_LOG_POS=1982;

3.2 启动从节点的从模式,开启主从

两个节点操作没有区别,这里只说一个节点了

# 启动从机模式
start slave;

然后查看从节点的关键参数,看看是否正常工作:

show slave status\G;

如果操作均正常就会展示如下信息,注意这俩个都是yes则表明主从集群ok了
在这里插入图片描述
然后就可以验证了,这时在主库增加一个库,表,数据都会立刻进行同步了,这里就不展示了。

Slave_IO_Running: 负责同步主库binlog的线程,将数据存储到中继日志
Slave_SQL_Running: 负责将中级日志的数据存储到表的线程

3.3 Slave_IO_Running为No的问题

这个问题是搭建主从时最容易碰到的问题,他的根本原因是主库和从库的连接出了问题,也就是说从库无法正常监听主库的binlog,所以排查这个问题,就需要考虑所有主从之间交互的场景,比如:

  • 服务器相互之间网络不通:可用ping、telnet验证
  • 配置的专用于主从复制的用户名、密码输入错误,或者用户的权限不足没有复制权限
  • 端口不正确等

排查这个问题抓住核心:主从之间的连通性除了问题,跟这个相关的配置我们都应该检查一下

3.4 Slave_SQL_Running为No的问题

这个线程的作用是将中级日志的数据复制到表中,如果数据已经到了中继日志却无法到表里,大概率是一个问题,就是主库同步过来的增量场景的数据的原始数据在从库不存在。比如在主库新增一条表记录,但是从库没有该表的表结构,此时会展示为No。造成这个问题的原因还是因为在确定了主库的binlog文件和位置以前的数据主从没有先对其。当然本文使用新建的库去演示的,所以不容易出这个问题。

3.5 停止和重置主从模式

主从模式的启动关键在于从节点,启动从节点后从节点的IO线程负责监控主库的binlog,SQL线程将中继日志写入到表中。如果我们想要暂停主从模式,直接在从节点上执行下面命令就行:

stop slave;

执行以上命令后,从机的IO和SQL线程就不工作了,那如果还想回复主从呢?
如果还想要回复主从,注意需要先将主从库的数据做到同步,然后再执行以下命令:

# 设置主节点的信息
CHANGE MASTER TO MASTER_HOST='192.168.150.140',MASTER_USER='slave_user',MASTER_PASSWORD='super',MASTER_PORT=3306,MASTER_LOG_FILE='mysql-bin.000002',MASTER_LOG_POS=1982;
# 启动从机模式
start slave;

上面的信息注意binlog的文件和位置可以使用原来的,保证在binlog的位置之前主从数据已经一致了即可,不然启动主从后就会出现3.4的问题。
此外如果想要重置中继日志 的话,可以使用如下命令:

reset slave;

如果想要重置binlog(主库)可以使用如下命令,注意该命令会删除所有的binlog,然后重新生成,一般用于第一次搭建时的日志初始化工作。

reset master;

到这里主从复制需要说的基本都说了,后面就开始做ShardingSphere的总结了,这里属于基础部分。

二、ShardingSphere-JDBC-读写分离

这里先从读写分离开始介绍,这里需要涉及到SpringBoot、Mybatis-plus等基础知识,这里不做这部分信息的展示了,不然篇幅会太长,如果对这块不熟悉的话需要先进行提前熟悉,这里不做延伸了。

1.基础环境准备-数据库

第一部分已经搭建了一主两从的数据库架构,这里就需要开始使用了,我们需要在主库中增加数据库readwrite-db,然后再readwrite-db中增加表consumer表。然后从库就可以自动同步过去了。下面是建库和建表语句。

  • 建库sql

    # 创建数据库
    create database `readwrite-db`;
    
  • 建表sql

    # 创建表
    CREATE TABLE `customer`  (
      `id` int NOT NULL,
      `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
    

以上就是需要在主库中建立的库和表了,建立完以后从库就会自动同步对应的库和表了。

2.基础环境准备-SpringBoot服务

这里需要准备一个使用Mybatis-plus+SpringBoot的环境,要求是可以通过接口操作数据库的增删改查,这里不对这部分进行代码和配置粘贴,只说明需要额外处理的部分。有一点补充下,这里使用的是Mysql8.0.30。

  • 所以在引入连接驱动时应该是下面的驱动版本:
    <!--引入连接 mysql5.7 的驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.38</version>
    </dependency>
    
    <!--引入连接 mysql8 的驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.11</version>
    </dependency>
    
  • 而且在配置驱动类时,也和Mysql5.7不同,下面列出了Mysql5.7和Mysql8的驱动类注意别配置错了,其他Mysql8和Mysql5.7 则没有太多使用区别了:
    # 这个是 Mysql5.7 的驱动类
    driver-class-name: com.mysql.jdbc.Driver
    #  这个是 Mysql8.0 的驱动类
    driver-class-name:  com.mysql.cj.jdbc.Driver
    
  • 还有需要注意的是,就是Mysql8和Mysql5.7的密码校验方式不同,这里在上面已经更改了,可以使用如下方式将密码校验方式更改为Mysql5.7的
    # 修改用户的密码验证方式为Mysql5.7的(数据库是Mysql8),和密码
    alter user 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'super';
    

在以上环境准备完毕(可以实现增删改查的一个简单服务,并且数据库可以实现主从同步)以后,我们就可以开始ShardingSphere的引入了,我们需要引入以下的ShardingSphere和其他使用的依赖:

<dependencies>

        <!-- 这里使用的是SpringBoot的starter,可以方便我们配置 -->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
            <version>5.1.1</version>
        </dependency>

        <!--引入OpenFign,父工程引入了cloud的包管理,这里无需声明version-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- nacos注册中心客户端,父工程引入了cloud的包管理,这里无需声明这里无需声明version-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- 引入loadbalancer负载均衡,父工程引入了cloud的包管理,这里无需声明这里无需声明version-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>


        <!--        引入mybatisplus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>

        <!--引入mybatis的依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>

        <!--引入连接 mysql8 的驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>

        <!--引入数据源连接池依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.4</version>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-test</artifactId>
            <version>2.6.11</version>
            <scope>compile</scope>
        </dependency>

    </dependencies>

然后就可以配置ShardingSphere的相关配置了,官方文档参考这里:https://shardingsphere.apache.org/document/5.1.1/cn/overview/,这里配置项如下,配置项里已经添加了注释,不再重复进行描述了:

server:
  port: 6300

spring:
  application:
    name: ebbing-test-shardingsphere-jdbc
  cloud:
    nacos:
      server-addr: 192.168.150.140:8848
      user-name: nacos
      password: nacos
      discovery:
        namespace: public

#spring:
  shardingsphere:
    # 属性配置模块
    props:
      sql-show: true # 展示sql:逻辑sql、实际sql
      # sql-simple: true # 展示简单sql,需要上一个配置的基础上配置

    mode:
      # 模式分为,内存、单机、集群,线上必须使用集群,才能保证高可用
      type: Memory

    # 数据源模块
    datasource:
      names: master,slave1,slave2
      master:
        # 数据源的类型,这里使用hikari,druid的话需要额外的配置
#        type: com.alibaba.druid.pool.DruidDataSource
        type: com.zaxxer.hikari.HikariDataSource
        # 这个是Mysql5.7 的驱动类
        #    driver-class-name: com.mysql.jdbc.Driver
        #  这个是Mysql8.0 的驱动类
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.140:3306/readwrite-db
        username: root
        password: super
      slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.138:3306/readwrite-db
        username: root
        password: super
      slave2:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.138:3307/readwrite-db
        username: root
        password: super

    # 读写分离规则模块
    rules:
      readwrite-splitting:
        data-sources:

          # 注意 ebbing-dbs 这是自定义的数据源名(名字随便起逻辑数据源),相当于上面的master、slave1、slave2的集合
          ebbing-dbs:
            # 读写分离类型,如: Static(数据源写死的),Dynamic(动态获取),一般使用Static,比如上面的三个数据源就是写死的
            type: Static
            props:
              # 写数据源名称
              write-data-source-name: master
              # 读数据源名称,多个从数据源用逗号分隔
              read-data-source-names: slave1,slave2
            # 负载均衡算法名称,注意这里和下面的负载配置是对应的
            load-balancer-name: ebbing-load-balancer


        # 负载均衡算法配置
        load-balancers:
          ebbing-load-balancer:
            # 负载均衡算法类型:ROUND_ROBIN、RANDOM(随机)、WEIGHT(权重)
            type: ROUND_ROBIN
            # 负载均衡算法属性配置,轮询不需要
            # props:
            #  xxx:

3.验证读写分离-写

完成了上面的配置以后,启动项目就可以开始验证了,这里不做新增接口代码的展示,当我们调用新增接口时会在控制台有如下输出(因为增加了sql-show):
在这里插入图片描述
在增加了sql-show的属性后,对于sql会解析成一个逻辑sql,该sql是展示需要执行的sql是什么,还会打印一个实际sql,这条sql可以看出来真正走了那个库进行新增的数据,从上面可以看到,走的正是master库,也可以多次进行验证,这里肯定都是master。

4.验证读写分离-读

上面配置文件中配置的读库是slave1、slave2且负载策略是轮询,那么只要验证结果是轮流从这两个库查询结果那么就是对的。下面对刚刚插入的数据进行查询,第一次查询结果如下,走的是slave1库:
在这里插入图片描述
下面是执行第二次以后的截图,可以看到第一次是slave1第二次是slave2,如果继续执行查询则会继续这么轮询下去
在这里插入图片描述

到这里我们也验证了读写分离与他的负载均衡都是没有问题的了

5.生产应用读写分离还差什么

上面的配置作为验证使用读写分离是没有问题的,不过要是直接使用到生产肯定不行,下面说说如果应用到生产,哪些是必须优化和改进的。

5.1 使用集群模式

上面在配置文件中的注释提到过,生产必须使用集群模式,这里使用的还是内存模式,只适用于开发环境,不可用于生产,ShardingSphere官方文档明确指出,生产环境必须使用集群模式,否则无法保证可靠性,这里的集群并不只是可以用于读写分离,还可以使用在数据库的水平和垂直拆分中都是适用的。那这里集群模式到底集群的是什么呢?是不是会有这个疑问,其实这里的集群是指多个ShardingSphere-JDBC间实现配置信息的持久化与共享,ShardingSphere5.2.1开始支持的集群模式有以下选择:ZK、Nacos、etcd、Consul四种方式注意如果使用的是5.2.1 之前的版本最好是使用ZK作为注册中心了,官方文档位置:

在这里插入图片描述
支持的配置项可以在这里看,也有配置示例:
在这里插入图片描述
笔者这里使用的是5.1.1 版本,所以集群模式不支持nacos,使用ZK演示,这里做ZK的配置展示如下:

mode:
  type: Cluster
  repository:
    type: zookeeper
    props:
      namespace: governance_ds
      server-lists: localhost:2181
      retryIntervalMilliseconds: 500
      timeToLiveSeconds: 60
      maxRetries: 3
      operationTimeoutMilliseconds: 500

可参见官方文档位置:
在这里插入图片描述

5.2 不要使用Druid,该数据源无法直接实现代理

这里对于国内小伙伴常用的druid数据源无法直接做代理,具体需要如何更改笔者没有去研究,因为使用默认的HikariDataSource数据源也没有多少区别,所以建议不要使用DruidDataSource,而是直接使用HikariDataSource。
比如下面配置数据源时使用的是HikariDataSource,在SpringBoot中如果不声明type则默认使用该数据源作为连接池,但是这里如果不声明type则不会有默认设置,所以是需要我们自己进行手动设置数据源的:

      master:
        # 数据源的类型,这里使用hikari,druid的话需要额外的配置
#        type: com.alibaba.druid.pool.DruidDataSource
        type: com.zaxxer.hikari.HikariDataSource
        # 这个是Mysql5.7 的驱动类
        #    driver-class-name: com.mysql.jdbc.Driver
        #  这个是Mysql8.0 的驱动类
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.140:3306/readwrite-db
        username: root
        password: super
      slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.138:3306/readwrite-db
        username: root
        password: super
      slave2:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.138:3307/readwrite-db
        username: root
        password: super

5.3 Hikari 数据源配置

这里使用了HikariDataSource数据源,数据源的使用肯定不能直接使用默认配置,不然不足以在生产使用。具体需要如何定义数据源的参数是需要根据自己的生产环境的系统并发服务器配置等来决定的,不过我们依然是可以做一个简单的配置,然后通过线上的并发压测来探测配置的瓶颈是否合适的,我们可以先看看如果我们不配置数据源的相关配置,Shardingsphere为我们增加了哪些默认值,可以找到Shardingsphere的自动配置文件ShardingSphereAutoConfiguration,然后再下面位置打上端点debug启动项目就可以看到了
在这里插入图片描述
在这里加上端点以后看到的配置信息如下:
在这里插入图片描述
很显然最核心的两个参数配置的都很不合理,最小空闲连接是1个,连接池的最大容量是50,这个配置只需要少量的并发就会打满,空闲连接较小会有一个明显的问题就是每次请求第一次进来建立连接的时间会比较长,因此第一次请求会比较耗时,因此不可用,所以还是需要我们去自己提供合适的配置的,下面是一个配置的模版,最小空闲连接使用的是200,连接池最大连接是1000,其他的超时时间等略调整,如下,生产可以根据该配置进行压测调整:

# 这里省略了和数据源关系不大的配置,其他数据源配置与此相同,不做重复展示
spring:
  shardingsphere:
    # 数据源模块
    datasource:
      names: master,slave1,slave2
      master:
        type: com.zaxxer.hikari.HikariDataSource
        #  这个是Mysql8.0 的驱动类
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.140:3306/readwrite-db
        username: root
        password: super
        # 数据源配置
        poolName: masterHikari
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

然后配置完重启看看我们的配置有没有生效,如下可以看到已经生效了(poolName没有生效其他都生效了,poolName暂且不追究了,也没啥用):
在这里插入图片描述

到这里使用Shardingsphere进行读写分离就说完了,使用以上信息足以在生产使用了,没有说到的地方欢迎补充。

5.4 读写分离其他需要注意的问题

    1. 单个事务内读写不分离
      在同一个事务内的读写会优先使用当前库,也就是使用主库了,此时不会将读操作发送到从库,不然数据库的可重复读就出现问题了。所以在同一个事务内会使用同一个库来完成操作。

三、ShardingSphere-JDBC-垂直分库

这一部分介绍如何使用Shardingsphere-JDBC来实现垂直分库,垂直分库在日常场景中还是很容易碰到的,所以说对这块的掌握还是很有必要的。

1.什么是垂直分库

垂直分库是数据库拆分的一种方式,主要是将一个大型数据库按照业务模块的不同进行拆分,形成多个小型的数据库。每个小型的数据库只包含与其业务模块相关的数据,各个数据库之间在物理上是独立存在的,它们之间通过网络来进行通信和数据交换。垂直分库一般会把数据库拆分多多台物理机上,而不是在同一台机子上进行分库,如果同一台机子的分库一般意义不大,因为在同一台机子所以并不能对分库之后的不同库进行性能提升,所以针对大数量的垂直拆分需要在不同的物理机上。
那垂直分库有哪些优点呢?

  • 降低单库压力:
    将原本集中在一个数据库上的压力分散到多个数据库上,降低了单库的读写压力。单库的数据量下降带来的检索和插入性能上的提升是显而易见的,这也是我们分库的最直接目的。

  • 提高扩展性:
    可以根据业务模块的需要独立进行扩展,如增加数据库服务器、增加缓存等,降低系统的耦合度,让不同的业务进一步区分开来

  • 提高安全性:
    不同的业务模块可以有不同的访问权限和安全策略,提高了数据的安全性。

  • 简化维护:
    拆分后的数据库更加模块化,便于维护和管理。

2.垂直分库带来哪些问题如何解决

垂直分库有一定的优点,但是也带来了一些问题,比如:

  • 系统复杂性增加:数据库拆分后,系统架构变得更加复杂,对开发和运维人员的要求更高

  • 跨库事务处理:
    这里因为涉及到了多个库,我们如果想要同时操作多个库,此时单个库的事务已经不能满足场景了。就涉及到了分布式事务的处理,分布式事务的引入肯定会增加系统的复杂程度,提升了系统的运维和使用难度,也增加了问题的可能性,毕竟分布式事务的异常概率受网络等影响比较大发生故障的可能也更大了。

库的垂直拆分后,这里的核心问题就是分布式事务,如何保证不同数据库的操作可以同时满足ACID的特性?这里其实是有两种方案的。

  • 使用分布式事务
    场景的分布式事务方案比如XA(2PC、3PC)、SAGA、TCC、可靠消息等都可以帮助我们解决这个问题。
  • 使用垂直分库框架
    最流行的垂直分库的框架就是Shardingsphere了,Shardingsphere支持的功能较多,也是包含垂直分库的支持的,我们使用Shardingsphere之后可以像操作同一个库的数据一样来操作不同的数据库数据。那Shardingsphere可以实现垂直分库的原理是什么呢?其实他也是使用的分布式事务,只不过Shardingsphere对分布式事务进行了封装使得我们可以直接像操作单库一样来操作不同的库。

3.环境准备

假设现在有一个独立的库包含了customer表和order表,然后拆分为两个独立的库,他们中分别有customer表和order表。我们的诉求是同时可以操作这俩不同服务器上的表,来保证事务的成功与失败。这里演示笔者采用将上面搭建的一主两从的架构停掉,然后使用两个从库来进行演示,停掉主从架构后两个从库就是独立的数据库了。下面是两个表的sql,数据库啥的还是上面主从的数据库:

# customer表
CREATE TABLE `customer` (
  `id` int NOT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC

# order表
CREATE TABLE `order1` (
  `id` int NOT NULL,
  `customer_id` int DEFAULT NULL,
  `order_no` int DEFAULT NULL,
  `order_desc` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

这样分库的环境就ok了,下面就可以开始配置Shardingsphere的垂直分库的配置了(省略SpringBoot部分的代码了)
全部配置如下:

server:
  port: 6300

spring:
  application:
    name: ebbing-shardingsphere-chuizhifenku

  shardingsphere:
    # 属性配置模块
    props:
      sql-show: true # 展示sql:逻辑sql、实际sql
      # sql-simple: true # 展示简单sql,需要上一个配置的基础上配置

    mode:
      # 模式分为,内存、单机、集群,线上必须使用集群,才能保证高可用
      type: Memory
      # shardingsphere5.2.1 以后支持nacos
#      repository:
#        type: Nacos
#        props:
#          namespace: public
#          server-lists: 192.168.150.140:8848


    # 数据源模块
    datasource:
      names: customerDB,orderDB

      customerDB:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.138:3306/readwrite-db
        username: root
        password: super
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

      orderDB:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.138:3307/readwrite-db
        username: root
        password: super
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

    # 规则配置
    rules:
      # 分片配置:  由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。
                # 缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)
                # 或只分库不分表且所有库的表结构完全一致的情况
      sharding:
        tables:
          # 这个填逻辑表名与实体的表明注解一致
          customer:
            # 这里注明这个表是哪个数据源的哪个表,实际表名
            actual-data-nodes: customerDB.customer
          # 这个填逻辑表名与实体的表明注解一致
          order1:
            actual-data-nodes: orderDB.order1

上面的配置与读写分离,差距在于规则配置(数据源这块配置属于共用配置),在读写分离中我们配置的是readwrite-splitting模块,这里配置的是sharding模块,该模块用于配置数据分片也是就分库的相关配置,这里只是简单的配置了两个表customer、order1,注意这俩必须是和数据库表明完全一致,这样数据源才能准确找到需要代理的表。另外actual-data-nodes后面跟的是实际表对应的数据源和表名称中间以点分割。垂直分库的配置这样就ok了。

当配置完以上信息后,在当前服务操作customer、order1两张表就和操作一张表没有区别了,如果使用的是Spring,那我们使用Transactional注解就可以实现不同库的事务了,这块上面也说了Shardingsphere其实底层是使用了XA来保证了分布式事务的。下面就开始验证下吧。

4.验证新增

下面贴下新增的代码,这块验证分为两部分,不加事务时直接新增两张表,这种验证没有任何意外是成功的,但是还不够,我们还希望异常场景下两个库的数据都可以同时回滚,此时我们抛出异常,看看能不能达到效果,java代码如下:

package com.cheng.ebbing.controller;

import com.cheng.ebbing.entity.Customer;
import com.cheng.ebbing.entity.Order;
import com.cheng.ebbing.service.CustomerService;
import com.cheng.ebbing.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 * 
 *
 * @author pcc
 * @date 2024-04-14 16:29:04
 */
@RestController
@RequiredArgsConstructor
@RequestMapping("/customer" )
public class CustomerOrderController {

    private final  CustomerService customerService;

    private final OrderService orderService;


    @PostMapping("/testAdd")
    @Transactional(rollbackFor = Exception.class)
    public String testAdd(@RequestBody Customer customer){

        customerService.save(customer);

        Order order = new Order();
        order.setOrderNo(1234);
        order.setCustomerId(customer.getId());
        order.setOrderDesc("这是"+customer.getName()+"的订单");

        orderService.save(order);
        throw new RuntimeException("c测试回滚c");

//        return "success";

    }
}

验证结果发现使用Shardingsphere的垂直分库却是可以做到分布式事务的同样效果(Shardingsphere底层做了分布式事务),这样在正常场景下和异常场景下都可以保证数据的DML的正常入库和回滚了,具体的验证细节就不展示了,很容易就可以验证。

5.验证查询

新增能同时完成两个库那查询肯定是没有问题的了,这里主要是验证是否可以像一个库中这么写sql:

select order1.* from order1 where customer_id in (select id from customer)

我们知道两个独立的物理库,是无法写这种sql的,如果写必须是单个库中的不同逻辑库才可以这么写,现在很明显我们是两台不同服务器上的物理库,那可以支持吗,代码如下:

    @GetMapping("testJoinSelect")
    public String testJoinTest(){
        List<Order> orderList = orderService.list(new LambdaQueryWrapper<Order>().inSql(Order::getCustomerId, "select id from customer "));
        return orderList.toString();
    }

然后执行后结果如下:
在这里插入图片描述
可以看到这块目前这种写法是不行的,至于怎么改进如何进行支持后面继续探讨,普通的分表查询这里就不验证了,肯定是支持的。

注:仔细思考了下,数据库这么设计其实也是不合理的,类似且有关联的数据应该被拆分到同一个库,以此减少跨库带来的性能损耗。所以真实的业务场景中,我们其实应该避免这种设计,而不是将关联的数据拆分到多个库。如果必须拆,那也应该是分库分表时,同库的表相互关联,比如A库中的表1和表2关联,B库中的表1和表2关联,而不应该是A库中的表1和B库中的表2进行关联

四、ShardingSphere-JDBC-水平分表和水平分库

上面已经说了读写分离和垂直分库。还有最重要的一块水平拆分,阿里开发规范建议单表500w或者达到2G建议进行拆分,Mysql官方建议单表1000w进行拆分,通常单表2000w以上一般查询速度就会相较于之前有稍微 明显的下降。当然了最终多少数据量进行拆分还是需要依据自己的业务而定,业务要求1s内必须返回那就需要根据单库单表的查询性能来进行分析,不能满足1s的要去就需要进行拆分,所以不会有个完全的定论,一般因业务而定。这里说的拆分一般是指水平拆分,水平拆分又可以分为水平分表和水平分库。

水平分表:
根据一定规则将主键(其他列也行)分类,同一类型入一张表,比如常见的根据id的取模数进行拆分,水平分表不跨库,在单表的性能达到瓶颈,但是单库的性能仍有富余时是适用的。

水平分库:
水平分表时,若是单库的IO和CPU都已经成为了瓶颈,那么再单纯的水平分表已经没有了意义,此时就需要考虑水平分库了,水平分库就是将原本在单个库中水平拆分出来的表,存储到不同的库,以降低单库的IO和CPU的压力,提升系统响应速度。

这里需要介绍的场景自然是包含水平分表和水平分库的,需要先准备数据库环境,这里数据库环境使用两台服务器他们应该含有相同的库和表,分别是:

第一个数据库:
ip:192.168.150.138:3306
DB:testlevel
table: order0、order1

第二个数据库:
ip:192.168.150.138:3307
DB:testlevel
table: order0、order1

下面是库和表的创建sql:

create database testlevel;

CREATE TABLE `order0` (
  `id` bigint NOT NULL,
  `order_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `order_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;

CREATE TABLE `order1` (
  `id` bigint NOT NULL,
  `order_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `order_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;

注意表不要使用自增约束,这里必须手动设置表的id,使用自增会导致主键重复。做好以上的基础操作后就可以开始水平拆分的工作啦

Order1表实际分为四份:
192.168.150.138:3306库下:testlevel.order0、testlevel.order1
192.168.150.138:3307库下:testlevel.order0、testlevel.order1
customer表只有一个在:
192.168.150.140:3306库下:readwrite-db.customer

1.基础环境准备

这里还是需要一个可以访问并操作两个库的SpringBoot服务,不过这块内容就不细说了,需要自己进行准备,

2.配置水平分表分库

这里假设有如下配置(注意这里配置还不是分库的完整配置,这里只是单纯为了引出行表达式)

server:
  port: 6300

spring:
  application:
    name: ebbing-shardingsphere-chuizhifenku
  cloud:
    nacos:
      server-addr: 192.168.150.140:8848
      user-name: nacos
      password: nacos
      discovery:
        namespace: public

#spring:
  shardingsphere:
    # 属性配置模块
    props:
      sql-show: true # 展示sql:逻辑sql、实际sql
      # sql-simple: true # 展示简单sql,需要上一个配置的基础上配置

    mode:
      # 模式分为,内存、单机、集群,线上必须使用集群,才能保证高可用
      type: Memory
      # shardingsphere5.2.1 以后支持nacos
#      repository:
#        type: Nacos
#        props:
#          namespace: public
#          server-lists: 192.168.150.140:8848


    # 数据源模块
    datasource:
      names: customerDB,orderDB1,orderDB2

      customerDB:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.140:3306/readwrite-db
        username: root
        password: super
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

      orderDB1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.138:3306/testlevel
        username: root
        password: super
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

      orderDB2:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.138:3307/testlevel
        username: root
        password: super
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

    # 规则配置
    rules:
      # 分片配置:  由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。
                # 缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)
                # 或只分库不分表且所有库的表结构完全一致的情况
      sharding:
        tables:
          # 这个填逻辑表名与实体的表明注解一致
          customer:
            # 这里注明这个表是哪个数据源的哪个表,实际表名
            actual-data-nodes: customerDB.customer
          # 这个填逻辑表名与实体的表明注解一致
          order1:
            actual-data-nodes: orderDB1.order0, orderDB1.order1, orderDB2.order0, orderDB2.order1

这里相比于垂直分库其实只是增加了数据源,然后更改了order1的真实表。如下所示这种写法无疑会很麻烦,而且阅读起来也很不方便,所以,sharding还提供了行表达式用以简化下面的这种写法。

actual-data-nodes: orderDB1.order0, orderDB1.order1, orderDB2.order0, orderDB2.order1

3.行表达式

行表达式的目的就是为了简化上面的写法,那如何简化呢,行表达式支持两种写法

  • 方式一:
    ${expression}
    
  • 方式二:
    $->{expression}
    

以上两种写法都是可以的,第二种只是多了个箭头其实效果上没有区别,所以下面举例都使用方式一进行举例。
注意虽然两种写法都是可以的,但是但是但是 ${} 这种写法与Spring配置文件的写法有冲突,所以在配置文件中只能用第二种写法

如果想要表达多个连续的值,可以如下写:
注意:表示范围时不支持字符串,只可以是数字类型且不能写00…09这种格式,必须是0…9这种

# 表达多个连续的值的写法
${begin..end}
# 例子1:表示1,2,3,4,5
${1..5}
# 例子2:表示order0,order1
order${0..1}

如果想要表达枚举,可以如下写:

${['unit1','unit2','unit3']}
# 例子1
${['orderDB1.order','orderDB2.order']}

注意上面两种写法是可以组合的,组合以后的结果就是两个表达式的笛卡尔积:

# 下面的表达式的意思是:A0,A1,A2,B0,B1,B2
${['A','B']}.${0..2}
# 如果是前缀用以区分可以这么写,表示:0table0,0table1,1table0,1table1,2table0,2table1
${0..2}table_${0..1}
  • 练习:使用行表达式来解决上面四个表的穷举写法:orderDB1.order0,orderDB1.order01,orderDB2.order0,orderDB2.order1
# 写法0:最常用的写法
orderDB${1..2}.order${0..1}
# 写法1:
${['orderDB1.order','orderDB2.order']}.${0..1}
# 写法2:
orderDB1.order${0..1},orderDB2.order${0..1}
# 写法3:
orderDB1.order${[0,1]},orderDB2.order${[0,1]}

4.水平分库-分库策略、分库算法配置

上面介绍行表达式,不仅在声明表时有用,在下面的算法配置其实也有行表达式这种,可以使用行表达式来声明分库的算法。需要说下这里先介绍分库的策略,也就是说只支持在不同库的相同的单个表(orderDB1.order0,orderDB2.order0)来进行拆分,所以这里就没有水平分表的动作,只是分库的动作了,这里需要关注两块:分库策略、分库算法,需要增加的配置在下面配置文件的最下面详见注释:

server:
  port: 6300

spring:
  application:
    name: ebbing-shardingsphere-chuizhifenku
  cloud:
    nacos:
      server-addr: 192.168.150.140:8848
      user-name: nacos
      password: nacos
      discovery:
        namespace: public

#spring:
  shardingsphere:
    # 属性配置模块
    props:
      sql-show: true # 展示sql:逻辑sql、实际sql
      # sql-simple: true # 展示简单sql,需要上一个配置的基础上配置

    mode:
      # 模式分为,内存、单机、集群,线上必须使用集群,才能保证高可用
      type: Memory
      # shardingsphere5.2.1 以后支持nacos
#      repository:
#        type: Nacos
#        props:
#          namespace: public
#          server-lists: 192.168.150.140:8848


    # 数据源模块
    datasource:
      names: customerDB,orderDB1,orderDB2

      customerDB:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.140:3306/readwrite-db
        username: root
        password: super
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

      orderDB1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.138:3306/testlevel
        username: root
        password: super
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

      orderDB2:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.150.138:3307/testlevel
        username: root
        password: super
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

    # 规则配置
    rules:
      # 分片配置:  由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。
                # 缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)
                # 或只分库不分表且所有库的表结构完全一致的情况
      sharding:
        tables:
          # 这个填逻辑表名与实体的表明注解一致
          customer:
            # 这里注明这个表是哪个数据源的哪个表,这个是实际表名
            actual-data-nodes: customerDB.customer
          # 这个填逻辑表名与实体的表明注解一致
          order1:
            # 行表达式写法表示:orderDB1.order0,orderDB1.order01,orderDB2.order0,orderDB2.order1
            actual-data-nodes: orderDB$->{1..2}.order0
            # 以下为分库的配置
            database-strategy:
              standard:
                # 分库的列配置:使用哪个列进行分库
                sharding-column: id
                # 分库的算法:需要自己在下面的配置定义算法名称,sharding-algorithm后面
                sharding-algorithm-name: inline-order-id-alg
        sharding-algorithms:
          inline-order-id-alg:
          	# 其他常见的分片方式:
          	# INLINE(自定义分片算法)
          	# MOD(直接根据字段取模)
          	# HASH_MOD(先hash后取模,适用于分片字段是字符串)
          	# CLASS_BASED(实现接口StandardShardingAlgorithm,自动调用方法实现取模,时间分片可用这种)
            type: INLINE
            props:
              # 配置分片算法:id>>22取模以后是0或者1,加1则是1或者2,对应了两个库:orderDB1,orderDB
              # 这里分库策略:id是雪花算法id,先对id右移22表示雪花算法的时间戳然后再进行取模+1
              algorithm-expression: orderDB$->{((id>>22) % 2)+1}

5.验证分库

下面是验证代码,就是一个简单的插入操作:

@GetMapping("/testInsert")
public String testInsert(){
    Order order = new Order();
    // 这是雪花算法生成的id,很难模仿1ms内两次的请求,这样id永远是偶数,因为只有同ms内末尾的序号才会递增,跨ms都会重置
    // 解决的方法是在分库策略时不直接使用id进行分库,而是使用id>>22 以后再取模分库,这样就是纯粹根据时间戳来分了
    SnowflakeIdGenerator snowflakeIdGenerator = new SnowflakeIdGenerator(1, 1);
    order.setId(snowflakeIdGenerator.generateId());
    order.setOrderNo(1101);
    order.setOrderDesc("这是测试水平分库插入");
    orderService.save(order);
    return "success";
}

下面是验证结果:
可以看到已经实现了插入的分库操作,也达到了预期目的
在这里插入图片描述

6.取模分片示例

注意这个文章因为写的时候跨度挺长的,中间换了电脑,后面都是后来补充的内容,上面使用的例子在后面变换了库名和数据库地址,其他完全一致,数据库名称由orderDB1,orderDB2 调整为了 db_practice0、db_practice1
注意这个文章因为写的时候跨度挺长的,中间换了电脑,后面都是后来补充的内容,上面使用的例子在后面变换了库名和数据库地址,其他完全一致,数据库名称由orderDB1,orderDB2 调整为了 db_practice0、db_practice1
注意这个文章因为写的时候跨度挺长的,中间换了电脑,后面都是后来补充的内容,上面使用的例子在后面变换了库名和数据库地址,其他完全一致,数据库名称由orderDB1,orderDB2 调整为了 db_practice0、db_practice1

后面的例子如果看到库名不一致请勿诧异 。
下面展示下取模的配置,分片数量,注意一定要和分库的数量保持一致:

spring:
  application:
    name: testsharding

  shardingsphere:
    # 属性配置模块
    props:
      sql-show: true # 展示sql:逻辑sql、实际sql
    mode:
      # 模式分为,内存、单机、集群,线上必须使用集群,才能保证高可用
      type: Memory

    # 数据源模块
    datasource:
      names: db_practice0,db_practice1

      db_practice0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://10.0.95.161:3306/db_practice0?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
        username: root
        password: gcspb0EhdBjsd7np
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

      db_practice1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://10.0.95.161:3306/db_practice1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
        username: root
        password: gcspb0EhdBjsd7np
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

    # 规则配置
    rules:
      # 分片配置:  由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。
      # 缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)
      # 或只分库不分表且所有库的表结构完全一致的情况
      sharding:
        tables:
          order1:
            # 行表达式写法表示:orderDB1.order0,orderDB1.order01,orderDB2.order0,orderDB2.order1
            actual-data-nodes: db_practice$->{0..1}.order0
            # 以下为分库的配置
            database-strategy:
              standard:
                # 分库的列配置:使用哪个列进行分库
                sharding-column: id
                # 分库的算法:需要自己在下面的配置定义算法名称,sharding-algorithm后面
                sharding-algorithm-name: mod-order-id-alg
        sharding-algorithms:
          mod-order-id-alg:
            # 其他常见的分片方式:
            # INLINE(自定义分片算法)
            # MOD(直接根据字段取模)
            # HASH_MOD(先hash后取模,适用于分片字段是字符串)
            # CLASS_BASED(实现接口StandardShardingAlgorithm,自动调用方法实现取模,时间分片可用这种)
            type: MOD
            props:
			  # 分片数量,注意一定要和分库的数量保持一致
              sharding-count: 2 

7.标准分片示例

这里需要使用java类来实现分片的具体算法,如果可以直接完成分片,不建议这么用,自己写逻辑大部分没有直接取模,行表达式的效率高。这里需要实现StandardShardingAlgorithm。
java示例代码(这里未做实现,只展示方法):

import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;

import java.time.LocalDateTime;

/**
 * @Author: pcc
 * @Description: 一句话描述该类
 */
public class AlgorithmAction implements StandardShardingAlgorithm<LocalDateTime> {
    @Override
    ...
}

这是配置:

spring:
  application:
    name: testsharding

  shardingsphere:
    # 属性配置模块
    props:
      sql-show: true # 展示sql:逻辑sql、实际sql
    mode:
      # 模式分为,内存、单机、集群,线上必须使用集群,才能保证高可用
      type: Memory

    # 数据源模块
    datasource:
      names: db_practice0,db_practice1

      db_practice0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://10.0.95.161:3306/db_practice0?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
        username: root
        password: gcspb0EhdBjsd7np
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

      db_practice1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://10.0.95.161:3306/db_practice1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
        username: root
        password: gcspb0EhdBjsd7np
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

    # 规则配置
    rules:
      # 分片配置:  由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。
      # 缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)
      # 或只分库不分表且所有库的表结构完全一致的情况
      sharding:
        tables:
          order1:
            # 行表达式写法表示:orderDB1.order0,orderDB1.order01,orderDB2.order0,orderDB2.order1
            actual-data-nodes: db_practice$->{0..1}.order0
            # 以下为分库的配置
            database-strategy:
              standard:
                # 分库的列配置:使用哪个列进行分库
                sharding-column: id
                # 分库的算法:需要自己在下面的配置定义算法名称,sharding-algorithm后面
                sharding-algorithm-name: class-order-id-alg
        sharding-algorithms:
          class-order-id-alg:
            # 其他常见的分片方式:
            # INLINE(自定义分片算法)
            # MOD(直接根据字段取模)
            # HASH_MOD(先hash后取模,适用于分片字段是字符串)
            # CLASS_BASED(实现接口StandardShardingAlgorithm,自动调用方法实现取模,时间分片可用这种)
            type: CLASS_BASED
            props:
              strategy: standard
              # 自定义标准分配算法
              algorithmClassName: cn.felord.generatecode.sharding.AlgorithmAction

8.分表配置:HASH_MOD分库、MOD分表策略配置

分表策略其实和分库没有任何区别,分库的配置项叫database-stategy而分表的配置项是table-stategy,其他都没区别了,包括算法的配置都是一样的,下面展示下分库分表的配置。这里分库使用orderNo进行hash取模来进行分库,分表使用id进行取模来进行分表,注意分库支持的策略,分表也都支持,这里不做重复举例了。
注意:在分库分表场景下,分库的sharding-count表示库的数量,所以该值和数据库的分库数量必须一致,而分表的sharding-count表示的是单库的分表数量,不是整体的数量,该值必须与单库的分表的数量保持一致。sharding先根据分库策略将当前数据属于哪个库确定好,然后再根据分表策略来确定当前谁属于哪个表

spring:
  application:
    name: testsharding

  shardingsphere:
    # 属性配置模块
    props:
      sql-show: true # 展示sql:逻辑sql、实际sql
    mode:
      # 模式分为,内存、单机、集群,线上必须使用集群,才能保证高可用
      type: Memory

    # 数据源模块
    datasource:
      names: db_practice0,db_practice1

      db_practice0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://10.0.95.161:3306/db_practice0?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
        username: root
        password: gcspb0EhdBjsd7np
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

      db_practice1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://10.0.95.161:3306/db_practice1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
        username: root
        password: gcspb0EhdBjsd7np
        # 数据源配置
        minIdle: 200
        maxPoolSize: 1000
        connectionTimeout: 25000
        idleTimeout: 50000
        maxLifetime: 1800000

    # 规则配置
    rules:
      # 分片配置:  由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。
      # 缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)
      # 或只分库不分表且所有库的表结构完全一致的情况
      sharding:
        tables:
          order1:
            # 行表达式写法表示:orderDB1.order0,orderDB1.order01,orderDB2.order0,orderDB2.order1
            actual-data-nodes: db_practice$->{0..1}.order$->{0..1}
            # 以下为分库的配置
            database-strategy:
              standard:
                # 分库的列配置:使用哪个列进行分库,使用数据库的原始列名
                sharding-column: order_no
                # 分库的算法:需要自己在下面的配置定义算法名称,sharding-algorithm后面
                sharding-algorithm-name: hash-mod-orderNo-alg
            table-strategy:
              standard:
                # 分库的列配置:使用哪个列进行分库
                sharding-column: id
                # 分库的算法:需要自己在下面的配置定义算法名称,sharding-algorithm后面
                sharding-algorithm-name: hash-id-alg
        sharding-algorithms:
          hash-mod-orderNo-alg:
            # 其他常见的分片方式:
            # INLINE(自定义分片算法)
            # MOD(直接根据字段取模)
            # HASH_MOD(先hash后取模,适用于分片字段是字符串)
            # CLASS_BASED(实现接口StandardShardingAlgorithm,自动调用方法实现取模,时间分片可用这种)
            type: HASH_MOD
            props:
              sharding-count: 2
          hash-id-alg:
            # 其他常见的分片方式:
            # INLINE(自定义分片算法)
            # MOD(直接根据字段取模)
            # HASH_MOD(先hash后取模,适用于分片字段是字符串)
            # CLASS_BASED(实现接口StandardShardingAlgorithm,自动调用方法实现取模,时间分片可用这种)
            type: MOD
            props:
              sharding-count: 2

注意上面配置中,标红的名称是对应的,要改需要同时改:
在这里插入图片描述

下面是测试代码:

import cn.felord.generatecode.entity.Order1;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.util.UUID;

/**
* @Author: pcc
* @Description: 一句话描述该类
* 
*/

@RunWith(SpringRunner.class)
@SpringBootTest
public class IOrder0ServiceTest {

    @Resource
    private IOrder0Service order0Service;

    @Test
    public void testFindByUsername() {
        for (int i = 0; i < 30; i++) {
            Order1 order1 = new Order1();
            order1.setId(IdWorker.getId());
            order1.setOrderNo("NO"+ UUID.randomUUID().toString().replace("-",""));
            order1.setOrderDesc("测试订单");
            order0Service.save(order1);
        }
    }
}

下面是测试结果,可以看到已经正常实现了分库分表的插入了,观察数据库数据发现也没有任何问题,这里就不展示数据了
在这里插入图片描述

五、可能会碰到的问题

1.分库分表场景下SQL如何执行

这个其实还是很重要的一点,关乎表数据量大时的sql优化问题,配置还是第四部分第8节的配置,这里是做下sql在分库分表时是如何执行的探究,测试代码如下:

@Test
public void testFindByUsername2() {
    List<Order1> allOrder = order0Service.list();
    System.out.println(allOrder.size());
}

显示执行的sql截图:
在这里插入图片描述
可以看到,sharding是帮我们把同库的表的结果做了union,然后再组合了多个库的结果进行了返回,这也就是查询的实际sql了。
当然了,这只是最简单的一种情况,那如果还有别的表需要和Order表进行关联呢,这该怎么办呢?继续往下看

2.分库分表场景下,不分库分表的表如何配置?

在sharding3/sharding4 时都是需要通过配置声明表的(在配置文件中声明只是不需要配置分库分表策略)或者配置一个默认的数据源来处理这些表,在sharding5时,这些已经可以自动处理了,也就是说如果一个表不需要做分库分表的配置,那么我们就无需关心他了。
注意:上面的场景有一种情况是不适用的,也就是当被分库分表的表与普通表进行join操作时就会异常,这是因为sharding-jdbc并不支持跨库的join操作,只允许在同库内进行join,所以如果一个表如果需要和被分库分表的表进行join那么大概率他也是需要分库分表的(数据量最少也是类似的,有可能跟多),如果真的不需要对这个关联join的表分表,可以先查数据,然后有了数据再查另一个表,这样分开操作就可以避免sharding的不支持操作了

3.关联表的分库分表时笛卡尔积问题 - 绑定表

假如有两个表需要分库分表,拆分如下:

  • db_practice0:
    • order0
    • order1
    • order_detail0
    • order_detail1
  • db_practice1
    • order0
    • order1
    • order_detail0
    • order_detail1

在实际业务中是需要order和order_detail一起进行关联查询结果的,我们知道sharding-jdbc不支持跨库的join所以说order与order_detail的连接,其实是只有同库的连接的。所以对于这种场景的表进行分库分表时,我们首先应该保证:
order、order_detail需要关联的数据一定得分在同库中,不能需要关联的数据中,order的数据在db_practice0,而order_detail的数据在db_practice1,这样sharding-jdbc是无法处理的。
在满足了上面的一个情况后,sharding会在两个库分表做笛卡尔积式的关联,如下:
order0 join order_detail0
order0 join order_detail1
order1 join order_detail0
order1 join order_detail1
所以两个库中实际sql就会有8个,也就是说sharding需要执行8个sql,然后拼接这个执行结果。这个说法不进行验证了,验证起来很好验证,这里说下优化方案。
很显然8个sql我们是可以避免的,并不需要执行8个sql,这样对于sql性能来说无疑是不必要的浪费。那如何避免的呢,首先是配置好分片策略

  • 第一点:
    配置分库时,注意将同类型的数据都分配在同一个库中,也就是说order和order_detail的分库策略要保持一致,使用同一个分库策略,比如他们既然有关联关系,那就用他们的关联健进行分库,这样可以保证同类型数据肯定在同一个库,这一步防止出现需要跨库join的问题
  • 第二点:
    保证同类型的数据分在同一个分片,也就是说需要保证order0 、order_detail0 分配的数据是同类的,关联时他们是在一起关联,而不用做order1、order_detail0的关联,这样就可以保证同库只需要做两次join就可以了,此时其实还是可以使用他们的关联健进行分表,这样可以保证相同的数据一定在同一个分片,当然需要保持他们的规则是一致的

通过以上两点就可以实现同类型的数据被分配在了同库、同分片,不过做到以上两点sharding依然不能做到只关联同分配的表,上面只是前提,我们依然需要为sharding指明两者是有绑定的,这样当sql执行时才会判断是绑定表从而来简化sql,需要增加的配置如下(省略了无关配置):

spring:
  shardingsphere:
    # 规则配置
    rules:
      sharding:
        binding-tables:
          # 注意都是逻辑表名
          - order1,order_detail

这样就可以减少一半的join,从而提升数据库的检索效率了。
不过,这种处理方式不可能解决100%的业务场景,假如有特殊的操作,兜底的方案就是多个表的join sql分开写,而不直接写join语句。

4.广播表

有一种表各个数据库都需要,但是数据变化量也不高,这种表其实可以做成广播表。如此就会方便这种表的查询与操作了。先看看一个表一旦被设置为广播表以后,他会有哪些特性:

  • 插入、更新会实时在所有节点执行,查询操作只会从配置的任意节点执行一次
  • 可以和任何表join,因为各个库都存在,就没有了跨库join的问题了

那如何让一个表成为广播表呢?只需要两步:

  • 第一步:配置表的声明
    这一步和配置分库分表时表的声明是一样的,这里假设有一个字典表是广播表:tb_dict

    spring:
      shardingsphere:
        # 规则配置
        rules:
          sharding:
            tables:
              # 广播表指定,多个使用逗号分隔
              tb_dict:
                actual-data-nodes: db_practice$->{0..1}.tb_dict,db_payment.tb_dict
    
  • 第二步:声明表为广播表
    将tb_dict 设置为广播表

    spring:
      shardingsphere:
        # 规则配置
        rules:
          sharding:
            broadcast-tables:
          	  - tb_	dict
    

这样就完成了一个广播表的配置,此时更新插入会同时操作指定的广播表的所有库的对应的表,而查询的实际sql只会有一个。

5.报错:Property ‘sqlSessionFactory’ or ‘sqlSessionTemplate’ are required

这个是因为项目里引入了Druid的数据源导致的,及时你引入了没有使用也会报错(其实是默认有一些配置),这里把包删除,直接使用默认的Hikari数据源就行了。

<exclusions>
    <exclusion>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
    </exclusion>
</exclusions>

6.Sql 失效-多个语句拼接的更新长sql

下面这样的sql,sharding不支持,会导致第一个sql以后的update都无法正常走到分表,解决的话就是将循环移动到业务代码处,不要写在mapper里就行了

    <update id="batchUpdateTrajectoryAuditPass" parameterType="java.util.List">
        <foreach collection="list" item="item" index="index" open="" close="" separator=";">
            update mytable
            <set>
                modifier_id = #{item.modifierId},
                modifier_name = #{item.modifierName}
            </set>
            where id = ${item.id}
        </foreach>
    </update>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

归去来 兮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值