ShardingSphere5

=ShardingSphere总览=

沙丁斯菲尔 - 分片秋

第一章、基础理论入门

1.是什么

image-20221022232449754

image-20221022232548618

2. 高性能架构模式

image-20221022233653879

① 读写分离架构

读写分离原理,将数据库的读写操作分布在不同的节点上,写操作在主机上,读操作在从机上,主机执行完写操作后会报数据同步到从机上,完成数据同步

image-20221022234043027

读写分离的基本实现:

  • 主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
  • 读写分离是根据 SQL 语义的分析将读操作和写操作分别路由至主库与从库
  • 通过一主多从的配置方式,可以将查询请求均匀的分散到多个数据副本,能够进一步的提升系统的处理能力。
  • 使用多主多从的方式,不但能够提升系统的吞吐量,还能够提升系统的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行。

读写分离会出现一个问题:读的时候可能会不能够及时的读取到最新的数据。因此引入 CAP理论

CAP理论

在一个分布式系统中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。

  • C 一致性(Consistency):对某个指定的客户端来说,读操作保证能够读取到主机上最新的写操作结果,数据保证是一致的。
  • A 可用性(Availability):非故障的节点在合理的时间(不能一直连接超时)内返回合理的响应(从机即使没有同步到最新数据也应该把旧数据返回)``
  • P 分区容忍性(Partition Tolerance):当出现网络分区(可能是丢包,也可能是连接中断,还可能是拥塞),系统能够继续“履行职责”(返回错误信息或者提示)

在实际设计过程中,每个系统不可能只处理一种数据,而是包含多种类型的数据,有的数据必须选择 CP,有的数据必须选择 AP,分布式系统理论上不可能选择 CA 架构。 因为C要求能够读取到最新的数据,因此从机在未同步主机数据之前是不允许访问的,这与A互相矛盾。并且在实际设计中,P是肯定要满足的,因此设计分布式系统,主打 CP 或者 AP。

image-20221023000756698

② 数据库分片架构

image-20221023000852467

垂直分片是按照业务将不同业务的表放在不同的数据库,一个数据库只放一种业务的表,比如订单数据库、积分数据库、用户信息数据库

水平分片是将同一个业务的全部数据,按照某个字段(通常是主键)来放在不同的表(或库)中,比如订单主键奇数表、订单主键偶数表

垂直分片

image-20221023120858898

img

垂直分表

如果特殊情况下,表内数据行并不多,但是依旧单表内存过大,可能考虑是一张表内字段过多且某些字段数据量巨大,可以再进行垂直分表,将一个表的字段拆分到两个表中存储,拆分出来的两个表拥有共同的主键。

image-20221023130211264

而如果单表内行数据过大,比如超过1KW行数据,就考虑继续水平分片,将同一个表中的数据根据主键的奇偶来分布到两个或者多个表中。

水平分片

image-20221023130924515

image-20221023131117412

image-20221023131450429

第二章、数据库环境搭建

MySql 主从同步

本案例使用windows可视化工具 Docker Desktop 来进行

1.安装Docker Desktop

2.安装MySql

① MySQL - 主从复制

一主两从,先看常见问题再进行操作

主服务器

setp1:下载镜像

安装后,windows的命令行就可以直接使用docker命令

直接搜索并下载镜像

image-20221022203132954

setp2:先创建挂载目录,配置目录

image-20221022203215520

conf 目录下先创建 配置文件 my.cnf

[client]
default_character_set=utf-8
[mysqld]
collation_server=utf8_general_ci
character_set_server=utf8
# 服务器唯一id,默认值1
server-id=1
# 设置日志格式,默认值ROW
binlog_format=STATEMENT

setp3:运行镜像,生成容器

命令:

docker run -p 3307:3306  # 因为本地已有3306,因此需要避开3306端口
--privileged=true 
--name mysql   # 取名
-v E:/Docker/mysql/log:/var/log/mysql   ## 挂载日志目录
-v E:/Docker/mysql/data:/var/lib/mysql    ## 挂载数据持久化目录
-v E:/Docker/mysql/conf:/etc/mysql/conf.d    ## 挂载配置文件
-e MYSQL_ROOT_PASSWORD=root  -d mysql    ## 配置密码并指定镜像

docker run -p 3307:3306 --privileged=true --name mysql-master -v //e/Docker/mysql/log:/var/log/mysql -v //e/Docker/mysql/data:/var/lib/mysql -v //e/Docker/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=root  -d mysql

启动成功,并使用可视化工具连接

image-20221022203545735

进入到命令行,将密码编码方式修改一下,否则无法使用连接工具连接上,如下的错误:

image-20221023160918476

使用如下命令:

# mysql -uroot -proot
mysql: [Warning] World-writable config file '/etc/mysql/conf.d/my.cnf' is ignored.
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 8.0.27 MySQL Community Server - GPL

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
## 更改密码编码
mysql> ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root';
Query OK, 0 rows affected (0.00 sec)

可以成功连接

image-20221023161025703

连接成功,主机可以正常使用;

setp4:主机中创建从机用户

-- 创建slave用户
CREATE USER 'chen_slave'@'%';
-- 设置密码
ALTER USER 'chen_slave'@'%' IDENTIFIED WITH mysql_native_password BY 'root';
-- 授予复制权限
GRANT REPLICATION SLAVE ON *.* TO 'chen_slave'@'%' ;
-- 刷新权限
FLUSH PRIVILEGES;
  • step5:主机中查询master状态:

执行完此步骤后不要再操作主服务器MYSQL,防止主服务器状态值变化

SHOW MASTER STATUS;

记下FilePosition的值。执行完此步骤后不要再操作主服务器MYSQL,防止主服务器状态值变化。

mysql> SHOW MASTER STATUS;
+---------------+----------+--------------+------------------+-------------------+
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+---------------+----------+--------------+------------------+-------------------+
| binlog.000002 |     1347 |              |                  |                   |
+---------------+----------+--------------+------------------+-------------------+
从服务器

setp1:准备

从服务器的目录、挂载、配置文件、启动命令、修改密码方式都一样。按照上述进行即可

my.cnf 配置文件不生效,但是内容就是这么个内容

[client]
default_character_set=utf-8
[mysqld]
collation_server=utf8_general_ci
character_set_server=utf8
# 服务器唯一id,每台服务器的id必须不同,如果配置其他从机,注意修改id
server-id=2
# 中继日志名,默认xxxxxxxxxxxx-relay-bin
#relay-log=relay-bin

启动命令

docker run -p 3308:3306 --privileged=true --name mysql-slave1 -v //e/Docker/mysql-slave1/log:/var/log/mysql -v //e/Docker/mysql-slave1/data:/var/lib/mysql -v //e/Docker/mysql-slave1/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=root  -d mysql
docker run -p 3309:3306 --privileged=true --name mysql-slave2 -v //e/Docker/mysql-slave2/log:/var/log/mysql -v //e/Docker/mysql-slave2/data:/var/lib/mysql -v //e/Docker/mysql-slave2/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=root  -d mysql

setp2:配置主从关系

进入从机命令行

# 这里使用主机创建的 从机账号密码来配置
CHANGE MASTER TO MASTER_HOST='172.27.48.1', 
MASTER_USER='chen_slave',MASTER_PASSWORD='root', MASTER_PORT=3307,
# 这里是主机显示的内容
MASTER_LOG_FILE='binlog.000002',MASTER_LOG_POS=1347; 


CHANGE MASTER TO MASTER_HOST='172.27.48.1', MASTER_USER='chen_slave',MASTER_PASSWORD='root', MASTER_PORT=3307,MASTER_LOG_FILE='binlog.000002',MASTER_LOG_POS=1347; 

-- 启动
START SLAVE;
-- 查看状态(不需要分号)
SHOW SLAVE STATUS\G

**两个关键进程:**下面两个参数都是Yes,则说明主从配置成功!

image-20221023162322281

常见问题

按照这个方法,必然会出问题,因为配置文件不生效

如果没有全部yes,可以删除掉配置关系从新配置

stop slave;  #停止主从配置
reset slave;  #删除原本的主从配置


# 问题一,连接不上主机,原因:docker上挂载的配置文件没有生效,两个都用的同一个服务id,出错
# 解决办法,每次手动修改服务id
 show variables like 'server_id'; ## 查看当前服务的id
 SET GLOBAL server_id=2; # 修改当前服务的id

 # 问题二 同一个主机,要用   IPv4 地址 . . . . . . . . . . . . : 172.27.48.1
 其他的ip不可以

重启后准备

主机:先查看主机的状态

mysql -uroot -proot

SHOW MASTER STATUS;

从机:然后改变从机的服务id,并重新删除掉原本的连接,再重新连接

 mysql -uroot -proot
 
 SET GLOBAL server_id=2;
 
 stop slave;  #停止主从配置
reset slave;  #删除原本的主从配置

CHANGE MASTER TO MASTER_HOST='172.27.48.1', MASTER_USER='chen_slave',MASTER_PASSWORD='root', MASTER_PORT=3307,MASTER_LOG_FILE='binlog.000007',MASTER_LOG_POS=1686; 
-- 启动
START SLAVE;
-- 查看状态(不需要分号)
SHOW SLAVE STATUS\G

=ShardingSphere-JDBC=

第一章 、ShardingSphere-JDBC 读写分离

1、创建SpringBoot程序

1.1.添加依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
            <version>5.1.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
    </dependencies>

1.2.创建实体类


@TableName("t_user")
@Data
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String uname;
}

1.3.配置读写分离

# 应用名称
spring.application.name=sharging-jdbc-demo
# 开发环境设置
spring.profiles.active=dev
# 内存模式
spring.shardingsphere.mode.type=Memory

# 配置真实数据源
spring.shardingsphere.datasource.names=master,slave1,slave2

# 配置第 1 个数据源
spring.shardingsphere.datasource.master.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.master.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.master.jdbc-url=jdbc:mysql://192.168.100.201:3306/db_user
spring.shardingsphere.datasource.master.username=root
spring.shardingsphere.datasource.master.password=123456

# 配置第 2 个数据源
spring.shardingsphere.datasource.slave1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.slave1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave1.jdbc-url=jdbc:mysql://192.168.100.201:3307/db_user
spring.shardingsphere.datasource.slave1.username=root
spring.shardingsphere.datasource.slave1.password=123456

# 配置第 3 个数据源
spring.shardingsphere.datasource.slave2.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.slave2.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.slave2.jdbc-url=jdbc:mysql://192.168.100.201:3308/db_user
spring.shardingsphere.datasource.slave2.username=root
spring.shardingsphere.datasource.slave2.password=123456

# 读写分离类型,如: Static,Dynamic
spring.shardingsphere.rules.readwrite-splitting.data-sources.myds.type=Static
# 写数据源名称
spring.shardingsphere.rules.readwrite-splitting.data-sources.myds.props.write-data-source-name=master
# 读数据源名称,多个从数据源用逗号分隔
spring.shardingsphere.rules.readwrite-splitting.data-sources.myds.props.read-data-source-names=slave1,slave2

# 负载均衡算法名称
spring.shardingsphere.rules.readwrite-splitting.data-sources.myds.load-balancer-name=alg_round

# 负载均衡算法配置
# 负载均衡算法类型
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_round.type=ROUND_ROBIN
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_random.type=RANDOM
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_weight.type=WEIGHT
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_weight.props.slave1=1
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_weight.props.slave2=2

# 打印SQl
spring.shardingsphere.props.sql-show=true

1.4.启动项目

注意会报错,提示命令行过长,需要做一些修改:

image-20221023212232828

然后就可以正常启动了

image-20221023213046531

1.5. 测试读写分离+负载均衡

写入数据:

    private final UserMapper userMapper;


    /**
     * 写入数据的测试
     */
    @GetMapping("/testInsert")
    public void testInsert() {
        User user = new User();
        user.setUname("张三丰");
        userMapper.insert(user);
    }

日志:

image-20221023213746812

日志解析:

## 逻辑sql,表明本条sql语句是向配置文件中的逻辑数据源myds插入数据,
##   具体向哪个表插入,就交给了shardingSphere 框架去依据配置文件中配置的情况进行
Logic SQL: INSERT INTO t_user  ( uname )  VALUES  ( ? )
## 
SQLStatement: MySQLInsertStatement(setAssignment=Optional.empty, onDuplicateKeyColumns=Optional.empty)
## 实际的sql语句,经过shardingSohere框架 判断,实际插入的表的信息
Actual SQL: master ::: INSERT INTO t_user  ( uname )  VALUES  ( ? ) ::: [张三丰]

数据库:

数据都同步了

image-20221023213918636

读取数据,负载均衡

    /**
     * 读取数据的测试
     */
    @GetMapping("/testRead")
    public void testRead() {
        for (int i = 0; i < 5; i++) {
            log.info("-第{}次-", i);
            List<User> users = userMapper.selectList(Wrappers.query());

        }
    }

日志

image-20221023224602310

因为是轮询,因此每次查询的数据库都不同,也就是做了负载均衡。

1.6.测试事务

为了保证主从库间的事务一致性,避免跨服务的分布式事务,ShardingSphere-JDBC的主从模型中,事务中的数据读写均用主库

不添加 @Transactional 注解,就还是正常的读写分离,添加了 @Transactional 注解意味着开启了事务,就会把读写都锁定在主机;

开启事务,进行读写

    /**
     * 事务测试,
     * //开启事务,测试读取
     */
    @Transactional(rollbackFor = Exception.class)
    @GetMapping("/testRollBack")
    public void testTrans() {
        User user = new User();
        user.setUname("铁锤");
        userMapper.insert(user);
        //查询三次
        for (int i = 0; i < 3; i++) {
            log.info("-第{}次-", i);
            List<User> users = userMapper.selectList(null);
            log.info("-结果{}-", users);
        }
    }

日志

image-20221023225459427

因为事务的开启,将读写两个操作都限定在了主库,而且一个线程内多次去查询相同的数据,会从缓存中拿取;

1.7.不同负载均衡算法的效果

读取数据时,想要采取不同的负载均衡算法,只需要在配置文件中进行设置即可

image-20221023230635362

轮询

跟上述例子一样

image-20221023224602310

随机

image-20221023230931615

权重

image-20221023231430579

一共访问15次:

slave1 权重配的是1 , 因此出现了 5 次

slave2 权重配的是2 , 因此出现了 10 次

第二章 ShardingSphere-JDBC 垂直分片

垂直分片就是把不同的业务的数据分到不同数据库的表中

1.修改配置文件为yaml

因为引入了多个规则,因此这里将配置文件改为 yaml 格式,以方便读

tips:我用的ShardingSphere版本为 5.1.1 不能根据官网文档去配,会报错,按照下面方法去配:

上面的配置文件改为yaml格式的话是这样的

spring:
  application:
    name: service-shardingsphere
  shardingsphere:
    mode: # ===一、模式配置===
      type: Memory
    datasource: # ===二、数据源配置===
      names: master_user,user_slave1,user_slave2,master_order
    dataSource:
      master_user:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3307/db_user
        username: root
        password: root
      user_slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3308/db_user
        username: root
        password: root
      user_slave2:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3309/db_user
        username: root
        password: root
    rules: # ===三、规则配置===
      readwrite-splitting:  # --- 读写分离配置 ---
        # 1.配置数据库
        dataSources:
          ds_user:
            type: Static
            props:
              write-data-source-name: master_user
              read-data-source-names: user_slave1,user_slave2
            loadBalancerName: alg_round # 为读写分离设定规则名,自定义
        load-balancers: # 给自定义规则名做解释
          alg_round:
            type: ROUND_ROBIN
          alg_random:
            type: RANDOM
          alg_weight:
            type: WEIGHT
            props:
              user_slave1: 1
              user_slave2: 2
    props: # ===四、配置日志===
      sql-show: true

2.创建实体类、表、配置

1.用户业务数据库堆,有一主两从 的读写分离库,有业务表 t_user

image-20221024235927838

CREATE DATABASE db_user;
USE db_user;
CREATE TABLE t_user (
 id BIGINT AUTO_INCREMENT,
 uname VARCHAR(30),
 PRIMARY KEY (id)
);
INSERT INTO t_user(uname) VALUES('zhang3');
INSERT INTO t_user(uname) VALUES(@@hostname);

2.订单业务数据库堆,有一主数据库没有从数据库,有订单业务表 t_order

image-20221025000023785

CREATE DATABASE db_order;
USE db_order;
CREATE TABLE t_order (
  id BIGINT AUTO_INCREMENT,
  order_no VARCHAR(30),
  user_id BIGINT,
  amount DECIMAL(10,2),
  PRIMARY KEY(id) 
);

实体

@TableName("t_order")
@Data
public class Order {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String orderNo;
    private Long userId;
    private BigDecimal amount;
}



@TableName("t_user")
@Data
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String uname;
}

修改增加垂直分库的配置

server:
  port: 7021


spring:
  application:
    name: service-shardingsphere
  shardingsphere:
    # ===一、模式配置===
    mode:
      type: Memory
    # ===二、数据源配置===
    datasource:
      names: master_user,user_slave1,user_slave2,master_order
    dataSource:
      master_user:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3307/db_user
        username: root
        password: root
      user_slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3308/db_user
        username: root
        password: root
      user_slave2:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3309/db_user
        username: root
        password: root
      master_order:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3310/db_order
        username: root
        password: root
    # ===三、规则配置===
    rules:
      # --- 3.1 读写分离配置 ---
      readwrite-splitting:
        # 1.给每个大数据堆设置各种的读写分离的从库主库
        dataSources:
          ds_user: # user业务数据库堆
            type: Static
            props:
              write-data-source-name: master_user
              read-data-source-names: user_slave1,user_slave2
            loadBalancerName: alg_round # 为读写分离设定规则名,自定义
          ds_order: # order业务数据库堆
            type: Static
            props:
              write-data-source-name: master_order
              read-data-source-names: master_order
            loadBalancerName: alg_round # 为读写分离设定规则名,自定义
        # 2.给自定义规则名做解释
        load-balancers:
          alg_round:
            type: ROUND_ROBIN
          alg_random:
            type: RANDOM
          alg_weight:
            type: WEIGHT
            props:
              user_slave1: 1
              user_slave2: 2
      # --- 3.2 垂直分片配置 ---
      sharding:
        tables:
          # 这里指定该表使用哪个数据堆的哪个表(数据堆用读写分离中配置的名称才可以)
          t_user:
            actualDataNodes: ds_user.t_user
          t_order:
            actualDataNodes: ds_order.t_order
    # ===四、配置日志===
    props:
      sql-show: true

准备工作完成,开始测试

3.测试

垂直分片写

    /**
     * 垂直分库的测试
     * 需要两个不同业务的数据插入到不同的数据堆中
     */
    @GetMapping("/testShardingVertical")
    public void testInsert() {
        User user = new User();
        user.setUname("强哥");
        userMapper.insert(user);

        Order order = new Order();
        order.setOrderNo("ATGUIGU001");
        order.setUserId(user.getId());
        order.setAmount(new BigDecimal(100));
        orderMapper.insert(order);
    }

测试结果:

用户数据插入了用户业务数据库堆中,依旧是主从同步,依旧是读写分离

image-20221025000609834

订单数据插入了订单表,数据正确

image-20221025000705835

日志

## 用户数据业务
Logic SQL: INSERT INTO t_user  ( uname )  VALUES  ( ? )
SQLStatement: MySQLInsertStatement(setAssignment=Optional.empty, onDuplicateKeyColumns=Optional.empty)
Actual SQL: master_user ::: INSERT INTO t_user  ( uname )  VALUES  (?) ::: [强哥]

## 用户数据业务
Logic SQL: INSERT INTO t_order  ( order_no,user_id,amount )  VALUES  ( ?,?,? )
SQLStatement: MySQLInsertStatement(setAssignment=Optional.empty, onDuplicateKeyColumns=Optional.empty)
Actual SQL: master_order ::: INSERT INTO t_order  ( order_no,user_id,amount )  VALUES  (?, ?, ?) ::: [ATGUIGU001, 8, 100]

显然,从日志看不出来什么特别的,z只能看到sql分发到了哪个数据库堆中,╮(╯▽╰)╭

垂直分片读

代码

    /**
     * 垂直分库的读测试
     */
    @GetMapping("/testReadVertical")
    public void testSelectFromOrderAndUser() {
        User user = userMapper.selectById(1L);
        Order order = orderMapper.selectById(1L);
    }

日志

## 用户数据业务
Logic SQL: SELECT id,uname FROM t_user WHERE id=? 
SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
Actual SQL: user_slave1 ::: SELECT id,uname FROM t_user WHERE id=?  ::: [1]

## 用户数据业务
Logic SQL: SELECT id,order_no,user_id,amount FROM t_order WHERE id=? 
SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
Actual SQL: master_order ::: SELECT id,order_no,user_id,amount FROM t_order WHERE id=?  ::: [1]

依旧看不出处理的底层逻辑。

第三章、 ShardingSphere-JDBC 水平分片

水平分片就是把同一个表中的数据分成两个表

水平分片又有水平分库+水平分表

1.水平分片 - 水平分库

1.新增加一个库

image-20221025222153573

2.配置文件

server:
  port: 7021


spring:
  application:
    name: service-shardingsphere
  shardingsphere:
    # ===一、模式配置===
    mode:
      type: Memory
    # ===二、数据源配置===
    datasource:
      names: master_user,user_slave1,user_slave2,master_order0,master_order1
    dataSource:
      master_user:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3307/db_user
        username: root
        password: root
      user_slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3308/db_user
        username: root
        password: root
      user_slave2:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3309/db_user
        username: root
        password: root
      master_order0:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3310/db_order # 订单库0
        username: root
        password: root
      master_order1:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3311/db_order  # 订单库1
        username: root
        password: root
    # ===三、规则配置===
    rules:
      # --- 3.1 读写分离配置 ---
      readwrite-splitting:
        # 1.给每个大数据堆设置各种的读写分离的从库主库
        dataSources:
          ds_user: # user业务数据库堆
            type: Static
            props:
              write-data-source-name: master_user
              read-data-source-names: user_slave1,user_slave2
            loadBalancerName: alg_round # 为读写分离设定规则名,自定义
          ds_order0: # order业务数据库堆
            type: Static
            props:
              write-data-source-name: master_order0
              read-data-source-names: master_order0
            loadBalancerName: alg_round # 为读写分离设定规则名,自定义
          ds_order1: # order业务数据库堆
            type: Static
            props:
              write-data-source-name: master_order1
              read-data-source-names: master_order1
            loadBalancerName: alg_round # 为读写分离设定规则名,自定义
        # 2.给自定义规则名做解释
        load-balancers:
          alg_round:
            type: ROUND_ROBIN
          alg_random:
            type: RANDOM
          alg_weight:
            type: WEIGHT
            props:
              user_slave1: 1
              user_slave2: 2
      # --- 3.2 垂直分片配置 ---
      sharding:
        tables:
          # 这里指定该表使用哪个数据堆的哪个表(数据堆用读写分离中配置的名称才可以)
          t_user:
            actualDataNodes: ds_user.t_user # 用户数据不做水平分库,保持原本配置即可
          t_order:
            # --- 4 垂直分片后,还可以针对进一步的水平分片配置 ---
            # 因为这里直接指定向那个库中插入,因此不能使用数据库堆,要使用数据库名称
            actualDataNodes: master_order${0..1}.t_order0 # 指定t_order相关的数据,需要在这些数据库的表中存储
            databaseStrategy:
              standard:
                shardingColumn: user_id  # 水平分片的依据字段
                shardingAlgorithmName: t_user_inline # 规则名
        sharding-algorithms:
          t_user_inline: # 规则解释
            type: INLINE
            props:
              algorithm-expression: master_order$->{user_id % 2}  ## 正好利用取模的出来值,确定是那个库
    # ===四、配置日志===
    props:
      sql-show: true

3.测试

代码

    /**
     * 水平分库的写测试
     */
    @GetMapping("/test22")
    public void testInsert() {
        for (long i = 0; i < 4; i++) {
            Order order = new Order();
            order.setOrderNo("ATGUIGU001");
            order.setUserId(i + 1);
            log.info("userId的:{}", order.getUserId());
            order.setAmount(new BigDecimal(100));
            orderMapper.insert(order);
        }
    }

结果

image-20221025222823209

根据 user_id 的对 2 取模后的值,决定放在那个库中

2.水平分片 - 水平分表

水平分库后,空中如果只有一个表的话,也会有存储读取压力,因此再把数据放在多张表中,达到水平分表的目的

1.每个库中都有两张表

image-20221025223430953

2.配置文件

server:
  port: 7021


spring:
  application:
    name: service-shardingsphere
  shardingsphere:
    # ===一、模式配置===
    mode:
      type: Memory
    # ===二、数据源配置===
    datasource:
      names: master_user,user_slave1,user_slave2,master_order0,master_order1
    dataSource:
      master_user:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3307/db_user
        username: root
        password: root
      user_slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3308/db_user
        username: root
        password: root
      user_slave2:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3309/db_user
        username: root
        password: root
      master_order0:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3310/db_order
        username: root
        password: root
      master_order1:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3311/db_order
        username: root
        password: root
    # ===三、规则配置===
    rules:
      # --- 3.1 读写分离配置 ---
      readwrite-splitting:
        # 1.给每个大数据堆设置各种的读写分离的从库主库
        dataSources:
          ds_user: # user业务数据库堆
            type: Static
            props:
              write-data-source-name: master_user
              read-data-source-names: user_slave1,user_slave2
            loadBalancerName: alg_round # 为读写分离设定规则名,自定义
          ds_order0: # order业务数据库堆
            type: Static
            props:
              write-data-source-name: master_order0
              read-data-source-names: master_order0
            loadBalancerName: alg_round # 为读写分离设定规则名,自定义
          ds_order1: # order业务数据库堆
            type: Static
            props:
              write-data-source-name: master_order1
              read-data-source-names: master_order1
            loadBalancerName: alg_round # 为读写分离设定规则名,自定义
        # 2.给自定义规则名做解释
        load-balancers:
          alg_round:
            type: ROUND_ROBIN
          alg_random:
            type: RANDOM
          alg_weight:
            type: WEIGHT
            props:
              user_slave1: 1
              user_slave2: 2
      # --- 3.2 垂直分片配置 ---
      sharding:
        tables:
          # 这里指定该表使用哪个数据堆的哪个表(数据堆用读写分离中配置的名称才可以)
          t_user:
            actualDataNodes: ds_user.t_user
          t_order:
            # --- 4 垂直分片后,还可以针对进一步的水平分片配置 ---
            actualDataNodes: master_order${0..1}.t_order${0..1} # 指定t_order相关的数据,需要在这些数据库堆、表中存储${0..1}
            databaseStrategy: #水平分库的配置
              standard:
                shardingColumn: user_id  # 水平分片的依据字段
                shardingAlgorithmName: inline # 规则名
            tableStrategy: #水平分表的配置
              standard:
                shardingColumn: order_no
                shardingAlgorithmName: hashmod
        sharding-algorithms:
          inline: # 规则解释,行表达式,适合字段是标准的数值的情况
            type: INLINE
            props:
              algorithm-expression: master_order$->{user_id % 2}
          hashmod: # 规则解释,哈希取模,针对非数值的字段,使用哈希取模比较合适
            type: HASH_MOD
            props:
              sharding-count: 2  # 这里有几个表就写几,并且表得以0序列号开始,比如 t_order0 这里表示字段进行hash运算后,对 2 进行取模,看落到那个表
    # ===四、配置日志===
    props:
      sql-show: true



3.测试

    /**
     * 水平分表的写测试
     */
    @GetMapping("/testTable")
    public void testTable() {
        for (long i = 100; i < 104; i++) {
            Order order = new Order();
            order.setOrderNo("ATGUIGU" + i);
            order.setUserId(1L);//这里写死,让它全落在一个库中,方便测试
            log.info("userId的:{}", order.getUserId());
            order.setAmount(new BigDecimal(100));
            orderMapper.insert(order);
        }
    }

结果:

image-20221025225144679

image-20221025225200534

对字段哈希计算并取模后,各自落在对应的表中;

3.水平分片 - 读取

水平分片的读取会把配置的该业务的全部数据都查出来

1.测试查全部

    /**
     * 水平分片的读测试
     */
    @GetMapping("/testRead")
    public void testRead() {
        //查全部,并打印
        List<Order> orders = orderMapper.selectList(null);
        orders.forEach(System.out::println);
    }

结果

image-20221025225632518

全部数据都查询出来了

日志

Logic SQL: SELECT  id,order_no,user_id,amount  FROM t_order
SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
Actual SQL: master_order0 ::: SELECT  id,order_no,user_id,amount  FROM t_order0 UNION ALL SELECT  id,order_no,user_id,amount  FROM t_order1
Actual SQL: master_order1 ::: SELECT  id,order_no,user_id,amount  FROM t_order0 UNION ALL SELECT  id,order_no,user_id,amount  FROM t_order1

UNION ALL

可以看到,查询其实是把水平分片的每个分库都查了出来,并且每个库内,每个分表都查出来,然后用 UNION ALL 将结果汇合起来

2.测试查分片的字段进行过滤

    /**
     * 水平分片的读测试
     * 分别测试 分库字段、分表字段、普通字段
     */
    @GetMapping("/testRead2")
    public void testRead2() {
        System.out.println("-------------查分库字段-------------");
        List<Order> orders = orderMapper.selectList(Wrappers.<Order>lambdaQuery()
                .eq(Order::getUserId, 1L));

        System.out.println("-------------查分表字段-------------");
        List<Order> orders2 = orderMapper.selectList(Wrappers.<Order>lambdaQuery()
                .eq(Order::getOrderNo, "ATGUIGU001"));

        System.out.println("-------------查分库字段+分表字段-------------");
        List<Order> orders3 = orderMapper.selectList(Wrappers.<Order>lambdaQuery()
                .eq(Order::getOrderNo, "ATGUIGU001")
                .eq(Order::getUserId, 1L));

        System.out.println("---------------查普通字段-----------");
        List<Order> orders4 = orderMapper.selectList(Wrappers.<Order>lambdaQuery()
                .eq(Order::getId, "01111382569209186813f97a4e63f77b"));
    }

结果


-------------查分库字段-------------
## 可以看出,因为查询的是 用来分库的关键字段,因此虽然查询了两个表,但是只查了一个库(因为库是通过该字段确定了)
Logic SQL: SELECT  id,order_no,user_id,amount  FROM t_order WHERE (user_id = ?)
SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)

Actual SQL: master_order1 ::: SELECT  id,order_no,user_id,amount  FROM t_order0 WHERE (user_id = ?) UNION ALL SELECT id,order_no,user_id,amount  FROM t_order1  WHERE (user_id = ?) ::: [1, 1]

-------------查分表字段-------------
## 可以看出,因为查询的是 用来分表的关键字段,因此虽然查了两个库,但是只查了一个表(因为表是通过该字段确定了)
Logic SQL: SELECT  id,order_no,user_id,amount  FROM t_order  WHERE (order_no = ?)
SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)

Actual SQL: master_order0 ::: SELECT  id,order_no,user_id,amount  FROM t_order1 WHERE (order_no = ?) ::: [ATGUIGU001]
Actual SQL: master_order1 ::: SELECT  id,order_no,user_id,amount  FROM t_order1  WHERE (order_no = ?) ::: [ATGUIGU001]

-------------查分库字段+分表字段-------------
## 可以看出,因为查询的是 分库字段+分表字段,因此查了一个库,并且只查了一个表(因为通过该字段确定了那个库那个表)
Logic SQL: SELECT  id,order_no,user_id,amount  FROM t_order  WHERE (order_no = ? AND user_id = ?)
SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)

Actual SQL: master_order1 ::: SELECT  id,order_no,user_id,amount  FROM t_order1 WHERE (order_no = ? AND user_id = ?) ::: [ATGUIGU001, 1]
---------------查普通字段-----------
## 可以看出,因为查询的是 普通字段,因此查了两个库,并且查了两个表(因为无法通过该字段确定了)
Logic SQL: SELECT  id,order_no,user_id,amount  FROM t_order   WHERE (id = ?)
SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)

Actual SQL: master_order0 ::: SELECT  id,order_no,user_id,amount  FROM t_order0  WHERE (id = ?) UNION ALL SELECT  id,order_no,user_id,amount  FROM t_order1  WHERE (id = ?) ::: [01111382569209186813f97a4e63f77b, 01111382569209186813f97a4e63f77b]

Actual SQL: master_order1 ::: SELECT  id,order_no,user_id,amount  FROM t_order0 WHERE (id = ?) UNION ALL SELECT  id,order_no,user_id,amount  FROM t_order1 
 WHERE (id = ?) ::: [01111382569209186813f97a4e63f77b, 01111382569209186813f97a4e63f77b]

雪花算法配置

水平分片需要关注全局序列,因为不能简单的使用基于数据库的主键自增。

雪花算法的原理

image-20221026203339395

这里有两种方案:一种是基于MyBatisPlus的id策略;一种是ShardingSphere-JDBC的全局序列配置。

基于MyBatisPlus的id策略:将Order类的id设置成如下形式

@TableId(type = IdType.ASSIGN_ID)
private Long id;

基于ShardingSphere-JDBC的全局序列配置:和前面的MyBatisPlus的策略二选一

      sharding:
        tables:
          # 这里指定该表使用哪个数据堆的哪个表(数据堆用读写分离中配置的名称才可以)
          t_user:
            actualDataNodes: ds_user.t_user
          t_order:
            # --- 4 垂直分片后,还可以针对进一步的水平分片配置 ---
            actualDataNodes: master_order${0..1}.t_order${0..1} # 指定t_order相关的数据,需要在这些数据库堆、表中存储${0..1}
            databaseStrategy: #水平分库的配置
              standard:
                shardingColumn: user_id  # 水平分片的依据字段
                shardingAlgorithmName: inline # 规则名
            tableStrategy: #水平分表的配置
              standard:
                shardingColumn: order_no
                shardingAlgorithmName: hashmod
                ## ---------------------雪花算法配置-----------------------------
            key-generate-strategy:
              column: id
              key-generator-name: alg_snowflake
                ## --------------------------------------------------
        sharding-algorithms:
          inline: # 规则解释,行表达式,适合字段是标准的数值的情况
            type: INLINE
            props:
              algorithm-expression: master_order$->{user_id % 2}
          hashmod: # 规则解释,哈希取模,针对非数值的字段,使用哈希取模比较合适
            type: HASH_MOD
            props:
              sharding-count: 2  # 这里有几个表就写几,并且表得以0序列号开始,比如 t_order0 这里表示字段进行hash运算后,对 2 进行取模看落到那个表
                ## --------------------------------------------------
       key-generators:
          alg_snowflake:
            type: SNOWFLAKE
                ## --------------------------------------------------

此时,需要将实体类中的id策略修改成以下形式:

//当配置了shardingsphere-jdbc的分布式序列时,自动使用shardingsphere-jdbc的分布式序列
//当没有配置shardingsphere-jdbc的分布式序列时,自动依赖数据库的主键自增策略
@TableId(type = IdType.AUTO)

不推荐使用shardingSphere的分布式序列,建议使用mybatis-plus的。

4.多表关联

​ 业务中,会有两个表相关联的情况出现,比如订单主单 t_order 跟细单 t_item ,我们希望这种关联的数据,都存放在同一个数据源中,最好是同一个库中。像之前那种配置,就会有可能把一条关联的订单主细单数据水平分片到两个表甚至是两个库里面,为了避免这种情况,我们可以使用多表关联,这样关联的两条数据,会优先放在同一个数据源中;

​ 多表关联没有额外的配置,只能通过代码实现,具体思路是:将分库关键字和分表关键字设置为主细单关联的外键,然后主单表跟细单表的分库分表配置都设成一样的,这样因为主细单的分库分表键都是相同的值,因此就会分配到同一个数据源中

准备工作

## 订单详情表
CREATE TABLE t_order_item0(
    id BIGINT,
    order_no VARCHAR(30),
    user_id BIGINT,
    price DECIMAL(10,2),
    `count` INT,
    PRIMARY KEY(id)
);

CREATE TABLE t_order_item1(
    id BIGINT,
    order_no VARCHAR(30),
    user_id BIGINT,
    price DECIMAL(10,2),
    `count` INT,
    PRIMARY KEY(id)
);

实体类

@TableName("t_order_item")
@Data
public class OrderItem {
    //当配置了shardingsphere-jdbc的分布式序列时,自动使用shardingsphere-jdbc的分布式序列
    @TableId(type = IdType.AUTO)
    private Long id;
    private String orderNo;
    private Long userId;
    private BigDecimal price;
    private Integer count;
}

配置文件

      # --- 3.2 垂直分片配置 ---
      sharding:
        tables:
          # 这里指定该表使用哪个数据堆的哪个表(数据堆用读写分离中配置的名称才可以)
          t_user: # 这个是实体上面 #TableName("值") 里面的值,使用到该实体,就会出现在这里
            actualDataNodes: ds_user.t_user
          t_order:
            # --- 4 垂直分片后,还可以针对进一步的水平分片配置 ---
            actualDataNodes: master_order${0..1}.t_order${0..1} # 指定t_order相关的数据,需要在这些数据库堆、表中存储${0..1}
            databaseStrategy: #水平分库的配置
              standard:
                shardingColumn: user_id  # 水平分片的依据字段
                shardingAlgorithmName: inline # 规则名
            tableStrategy: #水平分表的配置
              standard:
                shardingColumn: order_no
                shardingAlgorithmName: hashmod
          t_order_item:
            # --- 4 垂直分片后,还可以针对进一步的水平分片配置 ---
            actualDataNodes: master_order${0..1}.t_order_item${0..1} # 指定t_order相关的数据,需要在这些数据库堆、表中存储${0..1}
            databaseStrategy: #水平分库的配置
              standard:
                shardingColumn: user_id  # 水平分片的依据字段
                shardingAlgorithmName: inline # 规则名
            tableStrategy: #水平分表的配置
              standard:
                shardingColumn: order_no
                shardingAlgorithmName: hashmod
        sharding-algorithms:
          inline: # 规则解释,行表达式,适合字段是标准的数值的情况
            type: INLINE
            props:
              algorithm-expression: master_order$->{user_id % 2}
          hashmod: # 规则解释,哈希取模,针对非数值的字段,使用哈希取模比较合适
            type: HASH_MOD
            props:
              sharding-count: 2  # 这里有几个表就写几,并且表得以0序列号开始,比如 t_order0 这里表示字段进行hash运算后,对 2 进行取模,看落到那个表

测试

代码

/**
     * 关联表数据的写测试
     */
    @GetMapping("/testConnect")
    public void testDatabase() {
        for (long i = 100; i < 103; i++) {

            Order order = new Order();
            //分库分片键做关联
            order.setOrderNo("ATGUIGU" + i);
            order.setUserId(1L);
            orderMapper.insert(order);

            for (long j = 1; j < 3; j++) {
                OrderItem orderItem = new OrderItem();
                //分库分片键做关联
                orderItem.setOrderNo(order.getOrderNo());
                orderItem.setUserId(order.getUserId());
                orderItem.setPrice(new BigDecimal(10));
                orderItem.setCount(2);
                orderItemMapper.insert(orderItem);
            }
        }

        for (long i = 5; i < 7; i++) {

            Order order = new Order();
            order.setOrderNo("ATGUIGU" + i);
            order.setUserId(2L);
            orderMapper.insert(order);

            for (long j = 1; j < 3; j++) {
                OrderItem orderItem = new OrderItem();
                orderItem.setOrderNo(order.getOrderNo());
                orderItem.setUserId(order.getUserId());
                orderItem.setPrice(new BigDecimal(1));
                orderItem.setCount(3);
                orderItemMapper.insert(orderItem);
            }
        }
    }

结果

订单用户相同的都被分到同一个库中,因为分库字段 user_id 是相同的,符合预期。

这里只测试了插入,没有测试查询。

5.绑定表

查询会涉及到主表去关联那个库中的那个细单表的问题

​ (查询会先去选择两个库中的两个主单表中的其中一个,然后利用这个主单表去查本库的两个细单表(查两次),然后去查另一个库的两个细单表(查两次),然后两个主单表中的另外一个也同样来一遍,一共是8次)

​ 如果指定表的后缀一样的是绑定在一起的,那么查询速率就会提高,只要查4次;

这里就需要做一下特殊配置了

配置文件,配置绑定关系

      # --- 3.2 垂直分片配置 ---
      sharding:
        tables:
          # 这里指定该表使用哪个数据堆的哪个表(数据堆用读写分离中配置的名称才可以)
          t_user: # 这个是实体上面 #TableName("值") 里面的值,使用到该实体,就会出现在这里
            actualDataNodes: ds_user.t_user
          t_order:
            # --- 4 垂直分片后,还可以针对进一步的水平分片配置 ---
            actualDataNodes: master_order${0..1}.t_order${0..1} # 指定t_order相关的数据,需要在这些数据库堆、表中存储${0..1}
            databaseStrategy: #水平分库的配置
              standard:
                shardingColumn: user_id  # 水平分片的依据字段
                shardingAlgorithmName: inline # 规则名
            tableStrategy: #水平分表的配置
              standard:
                shardingColumn: order_no
                shardingAlgorithmName: hashmod
          t_order_item:
            # --- 4 垂直分片后,还可以针对进一步的水平分片配置 ---
            actualDataNodes: master_order${0..1}.t_order_item${0..1} # 指定t_order相关的数据,需要在这些数据库堆、表中存储${0..1}
            databaseStrategy: #水平分库的配置
              standard:
                shardingColumn: user_id  # 水平分片的依据字段
                shardingAlgorithmName: inline # 规则名
            tableStrategy: #水平分表的配置
              standard:
                shardingColumn: order_no
                shardingAlgorithmName: hashmod
        sharding-algorithms:
          inline: # 规则解释,行表达式,适合字段是标准的数值的情况
            type: INLINE
            props:
              algorithm-expression: master_order$->{user_id % 2}
          hashmod: # 规则解释,哈希取模,针对非数值的字段,使用哈希取模比较合适
            type: HASH_MOD
            props:
              sharding-count: 2  # 这里有几个表就写几,并且表得以0序列号开始,比如 t_order0 这里表示字段进行hash运算后,对 2 进行取模,看落到那个表
        binding-tables[0]: # 这里 [] 内的序号,代表绑定的组数,可以累加
          t_order,t_order_item
        binding-tables[1]:
          t_user,t_address

测试

    /**
     * 关联表数据的读测试
     * (会测试绑定)
     */
    @GetMapping("/testGetOrderAmount")
    public void testGetOrderAmount() {
        List<OrderVo> orderAmountList = orderItemMapper.getOrderAmount();
        orderAmountList.forEach(System.out::println);
    }

    <select id="getOrderAmount" resultType="com.springSharding.vo.OrderVo">
        SELECT o.order_no, SUM(i.price * i.count) AS amount
        FROM t_order o JOIN t_order_item i ON o.order_no = i.order_no
        GROUP BY o.order_no
    </select>

结果

未配置:

image-20221026221908062

可以看到,查询了八次

配置绑定

image-20221026222923889

可以看到,只查询了4次

注意事项

image-20221026223048788

6.广播表

什么是广播表

当系统中需要用到一些基础的数据,且对每个业务模块都是相同的数据的适合,就需要让这些数据在所有的分片数据源中都存在;表结构及其数据在每个数据库中均完全一致。 适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。

广播具有以下特性:

(1)插入、更新操作会实时在所有节点上执行,保持各个分片的数据一致性

(2)查询操作,只从一个节点获取

(3)可以跟任何一个表进行 JOIN 操作

准备工作

建表

需要在系统中每个库中都建表

CREATE TABLE t_dict(
    id BIGINT,
    dict_type VARCHAR(200),
    PRIMARY KEY(id)
);

实体类

@TableName("t_dict")
@Data
public class Dict {
    //可以使用MyBatisPlus的雪花算法
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    private String dictType;
}

配置文件

       ## 。。。。##
       ## 上面的省略
       sharding-algorithms:
          inline: # 规则解释,行表达式,适合字段是标准的数值的情况
            type: INLINE
            props:
              algorithm-expression: master_order$->{user_id % 2}
          hashmod: # 规则解释,哈希取模,针对非数值的字段,使用哈希取模比较合适
            type: HASH_MOD
            props:
              sharding-count: 2  # 这里有几个表就写几,并且表得以0序列号开始,比如 t_order0 这里表示字段进行hash运算后,对 2 进行取模,看落到那个表
        # --- 5 垂直分片后,还可以进行绑定表配置 ---
        binding-tables[0]:
          t_order,t_order_item
        # --- 6 垂直分片后,还可以进行广播表的配置 ---
        broadcast-tables[0]: # 配置广播表,因为是广播,所以可以不用配置数据节点指向,只配置这里就可以
          t_dict

测试

代码


    /**
     * 测试广播表 插入
     */
    @GetMapping("/testBroadcast")
    public void testBroadcast() {
        Dict dict = new Dict();
        dict.setDictType("type1");
        dictMapper.insert(dict);
    }

    /**
     * 测试广播表 读取
     */
    @GetMapping("/testSelectBroadcast")
    public void testSelectBroadcast() {

        List<Dict> dicts = dictMapper.selectList(null);
        dicts.forEach(System.out::println);
    }

结果

每个表中都插入了

需要注意的是,查询的时候,会随机从其中一个数据节点中获取

## 只查询一次
Logic SQL: SELECT  id,dict_type  FROM t_dict
SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
Actual SQL: master_order1 ::: SELECT  id,dict_type  FROM t_dict Dict(id=1585282641090666497, dictType=type1)

完整的配置文件 yaml

server:
  port: 7021


spring:
  application:
    name: service-shardingsphere
  shardingsphere:
    # ===一、模式配置===
    mode:
      type: Memory
    # ===二、数据源配置===
    datasource:
      # 这里是对应下面的数据库名
      names: master_user,user_slave1,user_slave2,master_order0,master_order1
    dataSource:
      master_user:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3307/db_user
        username: root
        password: root
      user_slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3308/db_user
        username: root
        password: root
      user_slave2:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3309/db_user
        username: root
        password: root
      master_order0:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3310/db_order
        username: root
        password: root
      master_order1:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3311/db_order
        username: root
        password: root
    # ===三、规则配置===
    rules:
      # --- 3.1 读写分离配置 ---
      readwrite-splitting:
        # 1.给每个大数据堆设置各种的读写分离的从库主库
        dataSources:
          ds_user: # user业务数据库堆
            type: Static
            props:
              write-data-source-name: master_user
              read-data-source-names: user_slave1,user_slave2
            loadBalancerName: alg_round # 为读写分离设定规则名,自定义
          ds_order0: # order业务数据库堆
            type: Static
            props:
              write-data-source-name: master_order0
              read-data-source-names: master_order0
            loadBalancerName: alg_round # 为读写分离设定规则名,自定义
          ds_order1: # order业务数据库堆
            type: Static
            props:
              write-data-source-name: master_order1
              read-data-source-names: master_order1
            loadBalancerName: alg_round # 为读写分离设定规则名,自定义
        # 2.给自定义规则名做解释
        load-balancers:
          alg_round:
            type: ROUND_ROBIN
          alg_random:
            type: RANDOM
          alg_weight:
            type: WEIGHT
            props:
              user_slave1: 1
              user_slave2: 2
      # --- 3.2 垂直分片配置 ---
      sharding:
        tables:
          # 这里指定该表使用哪个数据堆的哪个表(数据堆用读写分离中配置的名称才可以)
          t_user: # 这个是实体上面 #TableName("值") 里面的值,使用到该实体,就会出现在这里
            actualDataNodes: ds_user.t_user
          t_order:
            # --- 4 垂直分片后,还可以针对进一步的水平分片配置 ---
            actualDataNodes: master_order${0..1}.t_order${0..1} # 指定t_order相关的数据,需要在这些数据库堆、表中存储${0..1}
            databaseStrategy: #水平分库的配置
              standard:
                shardingColumn: user_id  # 水平分片的依据字段
                shardingAlgorithmName: inline # 规则名
            tableStrategy: #水平分表的配置
              standard:
                shardingColumn: order_no
                shardingAlgorithmName: hashmod
          t_order_item:
            # --- 4 垂直分片后,还可以针对进一步的水平分片配置 ---
            actualDataNodes: master_order${0..1}.t_order_item${0..1} # 指定t_order相关的数据,需要在这些数据库堆、表中存储${0..1}
            databaseStrategy: #水平分库的配置
              standard:
                shardingColumn: user_id  # 水平分片的依据字段
                shardingAlgorithmName: inline # 规则名
            tableStrategy: #水平分表的配置
              standard:
                shardingColumn: order_no
                shardingAlgorithmName: hashmod
        sharding-algorithms:
          inline: # 规则解释,行表达式,适合字段是标准的数值的情况
            type: INLINE
            props:
              algorithm-expression: master_order$->{user_id % 2}
          hashmod: # 规则解释,哈希取模,针对非数值的字段,使用哈希取模比较合适
            type: HASH_MOD
            props:
              sharding-count: 2  # 这里有几个表就写几,并且表得以0序列号开始,比如 t_order0 这里表示字段进行hash运算后,对 2 进行取模,看落到那个表
        # --- 5 垂直分片后,还可以进行绑定表配置 ---
        binding-tables[0]:
          t_order,t_order_item
        # --- 6 垂直分片后,还可以进行广播表的配置 ---
        broadcast-tables[0]: # 配置广播表,因为是广播,所以可以不用配置数据节点指向,只配置这里就可以
          t_dict


    # ===四、配置日志===
    props:
      sql-show: true


JDBC 部分 完结

=ShardingSphere-Proxy=

实际中,建议使用代码的方式,防止过多的引入三方应用程序

因此以下作为了解

第一章、Proxy的安装

1、获取

目前 ShardingSphere-Proxy 提供了 3 种获取方式:

2、使用二进制发布包安装

二进制包既可以Linux系统运行,又可以在windows系统运行

step1:解压二进制包

apache-shardingsphere-5.1.1-shardingsphere-proxy-bin.tar.gz

windows:使用解压软件解压文件

Linux:将文件上传至/opt目录,并解压

tar -zxvf apache-shardingsphere-5.1.1-shardingsphere-proxy-bin.tar.gz

step2:MySQL驱动

mysql-connector-java-8.0.22.jar

将MySQl驱动放至解压目录中的ext-lib目录

spte3:修改配置conf/server.yaml

rules:
  - !AUTHORITY
    users:
      - root@%:root
    provider:
      type: ALL_PRIVILEGES_PERMITTED

props:
  sql-show: true

spte4:启动ShardingSphere-Proxy

Linux 操作系统请运行 bin/start.sh

Windows 操作系统请运行 bin/start.bat

指定端口号和配置文件目录:bin/start.bat ${proxy_port} ${proxy_conf_directory}

step5:远程连接ShardingSphere-Proxy

远程访问

mysql -h192.168.100.1 -P3307 -uroot -p

step6:访问测试

show databases;

image-20220819152009158

3、使用Docker安装

step1:启动Docker容器

docker run -d \
-v /atguigu/server/proxy-a/conf:/opt/shardingsphere-proxy/conf \
-v /atguigu/server/proxy-a/ext-lib:/opt/shardingsphere-proxy/ext-lib \
-e ES_JAVA_OPTS="-Xmx256m -Xms256m -Xmn128m" \
-p 3321:3307 \
--name server-proxy-a \
apache/shardingsphere-proxy:5.1.1


-----------------------------------------
docker run -d -v //e/server/proxy-a/conf:/opt/shardingsphere-proxy/conf -v //e/server/proxy-a/ext-lib:/opt/shardingsphere-proxy/ext-lib -e ES_JAVA_OPTS="-Xmx256m -Xms256m -Xmn128m" -p 3321:3307 --name server-proxy-a apache/shardingsphere-proxy:5.1.1

step2:上传MySQL驱动

将MySQl驱动上传至e/server/proxy-a/ext-lib目录

spte3:修改配置server.yaml

rules:
  - !AUTHORITY
    users:
      - root@%:root
    provider:
      type: ALL_PRIVILEGES_PERMITTED

props:
  sql-show: true

将配置文件上传至/atguigu/server/proxy-a/conf目录

spte4:重启容器

docker restart server-proxy-a

step5:远程连接ShardingSphere-Proxy

ShardingSphere-Proxy容器中默认情况下没有mysql命令行客户端的安装,因此需要远程访问

mysql -h192.168.100.201 -P3321 -uroot -p

step6:访问测试

show databases;

image-20220819152009158

常见问题:docker容器无法远程连接

容器可以成功的创建并启动,但是无法远程连接。排除防火墙和网络等问题后,看看是不是因为容器内存不足导致。

原因:容器可分配内存不足

查看办法:进入容器后查看ShardingSphere-Proxy的日志,如有有cannot allocate memory,则说明容器内存不足

docker exec -it server-proxy-a env LANG=C.UTF-8 /bin/bash
cd /opt/shardingsphere-proxy/logs
tail stdout.log 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bz4jx909-1666882122131)(C:\Users\Administrator\Desktop\尚硅谷ShardingSphere5教程\笔记\笔记\assets\image-20220819151154763.png)]

解决方案:创建容器的时候使用JVM参数

-e ES_JAVA_OPTS="-Xmx256m -Xms256m -Xmn128m"

第二章、Proxy的读写分离

Proxy就相当于是一个数据库中间件,应用程序连接上它就可以在直接进行存储数据,中间件会自动将数据放在相应的数据库中。因此连接数据库、配置主从、分片,等之类的配置,就放在proxy中进行

image-20221027221405923

1、修改配置文件

修改配置config-readwrite-splitting.yaml

schemaName: readwrite_splitting_db

dataSources:
  write_ds:
    url: jdbc:mysql://192.168.100.201:3306/db_user?serverTimezone=UTC&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1
  read_ds_0:
    url: jdbc:mysql://192.168.100.201:3307/db_user?serverTimezone=UTC&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1
  read_ds_1:
    url: jdbc:mysql://192.168.100.201:3308/db_user?serverTimezone=UTC&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1

rules:
- !READWRITE_SPLITTING
  dataSources:
    readwrite_ds:
      type: Static
      props:
        write-data-source-name: write_ds
        read-data-source-names: read_ds_0,read_ds_1

将配置文件上传至/atguigu/server/proxy-a/conf目录

重启容器

docker restart server-proxy-a

2、实时查看日志

可以通过这种方式查看服务器中输出的SQL语句

docker exec -it server-proxy-a env LANG=C.UTF-8 /bin/bash
tail -f /opt/shardingsphere-proxy/logs/stdout.log 

3、远程访问测试

mysql> show databases;
mysql> use readwrite_splitting_db;
mysql> show tables;
mysql> select * from t_user;
mysql> select * from t_user;
mysql> insert into t_user(uname) values('wang5');

4、应用程序访问Proxy

4.1、创建项目

项目类型:Spring Initializr

SpringBoot脚手架:http://start.aliyun.com

项目名:sharding-proxy-demo

SpringBoot版本:2.3.7.RELEASE

4.2、添加依赖

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

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.1</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

4.3、创建实体类

package com.atguigu.shardingproxydemo.entity;

@TableName("t_user")
@Data
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String uname;
}

4.4、创建Mapper

package com.atguigu.shardingproxydemo.mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

4.5、配置数据源

# 应用名称
spring.application.name=sharding-proxy-demo
# 开发环境设置
spring.profiles.active=dev

#mysql数据库连接(proxy)
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.100.201:3321/readwrite_splitting_db?serverTimezone=GMT%2B8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root

#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

4.6、测试

package com.atguigu.shardingproxydemo;

@SpringBootTest
class ShardingProxyDemoApplicationTests {

    @Autowired
    private UserMapper userMapper;

    /**
     * 读数据测试
     */
    @Test
    public void testSelectAll(){
        List<User> users = userMapper.selectList(null);
        users.forEach(System.out::println);
    }
}

第三章、Proxy的垂直分片

1、修改配置文件

修改配置config-sharding.yaml

schemaName: sharding_db

dataSources:
  ds_0:
    url: jdbc:mysql://192.168.100.201:3301/db_user?serverTimezone=UTC&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1
  ds_1:
    url: jdbc:mysql://192.168.100.201:3302/db_order?serverTimezone=UTC&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1

rules:
- !SHARDING
  tables:
    t_user:
      actualDataNodes: ds_0.t_user
    t_order:
      actualDataNodes: ds_1.t_order

2、实时查看日志

可以通过这种方式查看服务器中输出的SQL语句

docker exec -it server-proxy-a env LANG=C.UTF-8 /bin/bash
tail -f /opt/shardingsphere-proxy/logs/stdout.log 

3、远程访问测试

mysql> show databases;
mysql> use sharding_db;
mysql> show tables;
mysql> select * from t_order;
mysql> select * from t_user;

第四章、Proxy的水平分片

1、修改配置文件

修改配置config-sharding.yaml

schemaName: sharding_db

dataSources:
  ds_user:
    url: jdbc:mysql://192.168.100.201:3301/db_user?serverTimezone=UTC&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1
  ds_order0:
    url: jdbc:mysql://192.168.100.201:3310/db_order?serverTimezone=UTC&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1
  ds_order1:
    url: jdbc:mysql://192.168.100.201:3311/db_order?serverTimezone=UTC&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1

rules:
- !SHARDING
  tables:
    t_user:
      actualDataNodes: ds_user.t_user

    t_order:
      actualDataNodes: ds_order${0..1}.t_order${0..1}
      databaseStrategy:
        standard:
          shardingColumn: user_id
          shardingAlgorithmName: alg_mod
      tableStrategy:
        standard:
          shardingColumn: order_no
          shardingAlgorithmName: alg_hash_mod
      keyGenerateStrategy:
        column: id
        keyGeneratorName: snowflake
    t_order_item:
      actualDataNodes: ds_order${0..1}.t_order_item${0..1}
      databaseStrategy:
        standard:
          shardingColumn: user_id
          shardingAlgorithmName: alg_mod
      tableStrategy:
        standard:
          shardingColumn: order_no
          shardingAlgorithmName: alg_hash_mod
      keyGenerateStrategy:
        column: id
        keyGeneratorName: snowflake

  bindingTables:
    - t_order,t_order_item


  broadcastTables:
    - t_dict

  shardingAlgorithms:
    alg_inline_userid:
      type: INLINE
      props:
        algorithm-expression: server-order$->{user_id % 2}
    alg_mod:
      type: MOD
      props:
        sharding-count: 2
    alg_hash_mod:
      type: HASH_MOD
      props:
        sharding-count: 2
  
  keyGenerators:
    snowflake:
      type: SNOWFLAKE

2、实时查看日志

可以通过这种方式查看服务器中输出的SQL语句

docker exec -it server-proxy-a env LANG=C.UTF-8 /bin/bash
tail -f /opt/shardingsphere-proxy/logs/stdout.log 

3、远程访问测试

mysql> show databases;
mysql> use sharding_db;
mysql> show tables;
mysql> select * from t_order; --测试水平分片
mysql> select * from t_dict; --测试广播表

--------------------------------------------------- 完结 ------------------------------------------
参考视频链接

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
shardingsphere5是指ShardingSphere的第5个版本。ShardingSphere是一个开源的分布式数据库中间件,用于实现数据库的分片和分库功能。根据引用\[1\]中的配置信息,可以看出ShardingSphere的分片规则和分表策略。对于t_order表,根据order_no的哈希值的奇偶性来确定数据插入的服务器和表。如果order_no的哈希值为偶数,则数据插入对应服务器的t_order0表;如果order_no的哈希值为奇数,则数据插入对应服务器的t_order1表。而对于t_user表,根据user_id的奇偶性来确定数据插入的服务器。如果user_id为偶数,则数据插入server-order0服务器;如果user_id为奇数,则数据插入server-order1服务器。这样的分片规则可以提高查询效率,因为同一个用户的订单数据会被插入到同一台服务器上。\[2\]\[3\] #### 引用[.reference_title] - *1* *2* [尚硅谷_ShardingSphere5](https://blog.csdn.net/m0_45432976/article/details/128389257)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [ShardingSphere5](https://blog.csdn.net/qq_44981526/article/details/127193650)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值