ShardingSphere 5 核心实践

ShardingSphere 官网

高性能架构模式

互联网业务兴起之后,海量用户加上海量数据的特点,单个数据库服务器已经难以满足业务需要,必须考虑数据库集群的方式来提升性能。高性能数据库集群的第一种方式是读写分离,第二种方式是数据库分片

  • 访问量大
  • 数据量大(阿里巴巴规定单表超过 500 条记录,或者超过 2GB 就要考虑分库分表了)

读写分离架构

读写分离原理: 读写分离的基本原理是将数据库读写操作分散到不同的节点上,下面是其基本架构图

读写分离的基本实现:

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

下图展示了根据业务需要,将用户表的写操作和读操路由到不同的数据库的方案:

CAP

CAP 理论:

CAP 定理(CAP theorem)又被称作布鲁尔定理(Brewer's theorem),是加州大学伯克利分校的计算机科学家埃里克·布鲁尔(Eric Brewer)在 2000 年的 ACM PODC 上提出的一个猜想。对于设计分布式系统的架构师来说,CAP 是必须掌握的理论

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

  • C 一致性(Consistency):对某个指定的客户端来说,读操作保证能够返回最新的写操作结果
  • A 可用性(Availability):非故障的节点在合理的时间内返回合理的响应 (不是错误和超时的响应)
  • P 分区容忍性(Partition Tolerance):当出现网络分区后 (可能是丢包,也可能是连接中断,还可能是拥塞),系统能够继续"履行职责"

CAP特点:

在实际设计过程中,每个系统不可能只处理一种数据,而是包含多种类型的数据,有的数据必须选择 CP,有的数据必须选择 AP,分布式系统理论上不可能选择 CA 架构

{% note info %}

如果主从库之间采用异步同步,那么为了高可用和高性能,系统允许从库短暂落后主库,因此不能保证强一致性;如果要保证强一致性,就需要等待同步完成或限制读从库,这会牺牲部分可用性和性能。

{% endnote %}

  • CP:如下图所示,为了保证一致性,当发生分区现象后,N1 节点上的数据已经更新到 y,但由于 N1 和 N2 之间的复制通道中断,数据 y 无法同步到 N2,N2 节点上的数据还是 x。这时客户端 C 访问 N2 时,N2 需要返回 Error,提示客户端 C"系统现在发生了错误",这种处理方式违背了可用性(Availability)的要求,因此 CAP 三者只能满足 CP。
  • AP:如下图所示,为了保证可用性,当发生分区现象后,N1 节点上的数据已经更新到 y,但由于 N1 和 N2 之间的复制通道中断,数据 y 无法同步到 N2,N2 节点上的数据还是 x。这时客户端 C 访问 N2 时,N2 将当前自己拥有的数据 x 返回给客户端 C 了,而实际上当前最新的数据已经是 y 了,这就不满足一致性(Consistency)的要求了,因此 CAP 三者只能满足 AP。注意:这里 N2 节点返回 x,虽然不是一个"正确"的结果,但是一个"合理"的结果,因为 x 是旧的数据,并不是一个错乱的值,只是不是最新的数据而已。(比如视频播放量、点赞数、朋友圈这种)

CAP 理论中的 C 在实践中是不可能完美实现的,在数据复制的过程中,一定是有时间消耗的,此时节点N1 和节点 N2 的数据并不一致(强一致性)。即使无法做到强一致性,但应用可以采用适合的方式达到最终一致性。具有如下特点:

  • 基本可用(Basically Available):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。--> 比如下单高峰可以把系统资源大部分都给下单功能,而让修改收货地址这种功能可用的资源少,当然也会牺牲修改收货地址模块功能的使用,比如卡顿,延迟高
  • 软状态(Soft State):允许系统存在中间状态,而该中间状态不会影响系统整体可用性。这里的中间状态就是 CAP 理论中的数据不一致。--> 比如你发了朋友圈,但是你的朋友两分钟后才看到你新发的朋友圈

最终一致性(Eventual Consistency):系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。

数据库分片架构

读写分离的问题:

读写分离分散了数据库读写操作的压力,但没有分散存储压力,为了满足业务数据存储的需求,就需要将存储分散到多台数据库服务器上

数据分片:

将存放在单一数据库中的数据分散地存放至多个数据库或表中,以达到提升性能瓶颈以及可用性的效果。 数据分片的有效手段是对关系型数据库进行分库和分表。数据分片的拆分方式又分为垂直分片和水平分片

垂直分片

垂直分库

按照业务拆分的方式称为垂直分片,又称为纵向拆分,它的核心理念是专库专用。 在拆分之前,一个数据库由多个数据表构成,每个表对应着不同的业务。而拆分之后,则是按照业务将表进行归类,分布到不同的数据库中,从而将压力分散至不同的数据库。

下图展示了根据业务需要,将用户表和订单表垂直分片到不同的数据库的方案

垂直拆分可以缓解数据量和访问量带来的问题,但无法根治。如果垂直拆分之后,表中的数据量依然超过单节点所能承载的阈值(单表记录过多,比如超过 500 万或者单表数据大小超过 2GB),则需要水平分片来进一步处理。

{% note info %}

这种拆法的要求是 不同业务之间耦合度要低 ,比如订单和支付关系很紧,但用户信息和商品信息相对独立,就适合拆开。如果两个业务经常需要强事务、频繁 join,那就不太适合拆得太开,否则会带来分布式事务、跨库查询的问题。

{% endnote %}

{% note info %}

单表过大可能有两种情况

  • 表记录过多,适合水平拆分
  • 单个记录比较大,适合垂直拆分
    {% endnote %}

垂直分表

垂直分表适合将表中某些不常用的列,或者是占了大量空间的列拆分出去。

假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。(建立的新表也要有 ID 字段,分表后两个表的 ID 字段是 1对1 对关系)

垂直分表引入的复杂性主要体现在表操作的数量要增加。例如,原来只要一次查询就可以获取 name、age、sex、nickname、description,现在需要两次查询,一次查询获取 name、age、sex,另外一次查询获取 nickname、description。

水平拆分就是水平分表,水平分表适合表行数特别大的表,水平分表属于水平分片

水平分片

水平分片又称为横向拆分。相对于垂直分片,它不再将数据根据业务逻辑分类,而是通过某个字段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个分片仅包含数据的一部分。 例如:根据主键分片,偶数主键的记录放入 0 库(或表),奇数主键的记录放入 1 库(或表),如下图所示。

查询和插入都根据这个规则来进行

单表进行切分后,是否将多个表分散在不同的数据库服务器中,可以根据实际的切分效果来确定。刚刚这个图里面就是分表后放到了不同的数据库。

  • 水平分表:单表切分为多表后,新的表即使在同一个数据库服务器中,也可能带来可观的性能提升,如果性能能够满足业务要求,可以不拆分到多台数据库服务器,毕竟业务分库也会引入很多复杂性,比如分布式事务、跨库关联问题,此外还有数据库成本的问题,需要💰;
  • 水平分库:如果单表拆分为多表后,单台服务器依然无法满足性能要求,那就需要将多个表分散在不同的数据库服务器中。

阿里巴巴Java开发手册:

【推荐】单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。

说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表

读写分离和数据分片架构

下图展现了将数据分片与读写分离一同使用时,应用程序与数据库集群之间的复杂拓扑关系。

我们只关注其中一个来看,比如 Application 1

  • 下面三台数据库负责写,上面三台负责读,涉及数据同步问题
  • 也涉及分表,这里应该是水平分表+水平分库,因为是 t_order_0t_order_1,如果是垂直分表,应该是 t_order_baset_order_detail 这样写

水平分库分表 + 主从复制 + 读写分离

实现方式

读写分离和数据分片具体的实现方式一般有两种: 程序代码封装中间件封装

程序代码封装

程序代码封装指在代码中抽象一个数据访问层(或中间层封装),实现读写操作分离和数据库服务器连接的管理。

其基本架构是:以读写分离为例

用代码来控制访问哪个数据库

中间件封装

中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。对于业务服务器来说,访问中间件和访问数据库没有区别,在业务服务器看来,中间件就是一个数据库服务器。

基本架构是:以读写分离为例

找一个专门的哥们来做,我们只负责发送 SQL 命令,具体给谁让数据库中间件来做

常用解决方案

Apache ShardingSphere(提供程序级别和中间件级别两种方式)

MyCat(数据库中间件) --> 比较老了,没有维护,经常出现问题

ShardingSphere

简介

官网:https://shardingsphere.apache.org/index_zh.html

文档:https://shardingsphere.apache.org/document/5.5.3/cn/overview/ --> 最新版 5.5.3

Apache ShardingSphere 由 ShardingSphere-JDBC 和 ShardingSphere-Proxy 两种接入端组成,二者既可以独立部署,也可以在混合架构中配合使用。

ShardingSphere-JDBC

程序代码封装

定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。

  • 适用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC;
  • 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, HikariCP 等;
  • 支持任意实现 JDBC 规范的数据库,目前支持 MySQL,PostgreSQL,Oracle,SQLServer 以及任何可使用 JDBC 访问的数据库。

{% note info %}

这个图有两个 Java Application,含义是如果我们的应用服务程序本身也是集群形式,我们可以引入服务治理组件,比如 Nacos、Zookeeper

{% endnote %}

ShardingSphere-Proxy

中间件封装

定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持(可以不是 Java 程序,但是 ShardingSphere-JDBC 只支持 Java)。 目前提供 MySQL 和 PostgreSQL版本,它可以使用任何兼容 MySQL/PostgreSQL 协议的访问客户端(如:MySQL Command Client, MySQL Workbench, Navicat 等)操作数据,对 DBA 更加友好。

  • 向应用程序完全透明,可直接当做 MySQL/PostgreSQL 使用;
  • 兼容 MariaDB 等基于 MySQL 协议的数据库,以及 openGauss 等基于 PostgreSQL 协议的数据库;
  • 适用于任何兼容 MySQL/PostgreSQL 协议的的客户端,如:MySQL Command Client, MySQL Workbench, Navicat 等。

MySQL 主从同步

MySQL 主从同步原理

基本原理:

slave 会从 master 读取 binlo 来进行数据同步

具体步骤:

  • step1:master 将数据改变记录到 二进制日志(binary log) 中。
  • step2: 当slave上执行 start slave 命令之后,slave会创建一个 IO 线程用来连接master,请求master中的binlog。
  • step3:当slave连接master时,master会创建一个 log dump 线程,用于发送 binlog 的内容。在读取 binlog 的内容的操作中,会对主节点上的 binlog 加锁,当读取完成并发送给从服务器后解锁。
  • step4:IO 线程接收主节点 binlog dump 进程发来的更新之后,保存到 中继日志(relay log) 中。
  • step5:slave的SQL线程,读取relay log日志,并解析成具体操作,从而实现主从操作一致,最终数据一致。

一主多从配置

服务器规划:使用 docker 方式创建,主从服务器 IP 一致,端口号不一致

  • 主服务器:容器名ss-mysql-master,端口3306
  • 从服务器:容器名ss-mysql-slave1,端口3307
  • 从服务器:容器名ss-mysql-slave2,端口3308 (ss 含义是 ShardingSphere)

{% note info %}

这里我是在 macOS 系统下,已经安装了 Docker。

{% endnote %}

准备主服务器

sh 复制代码
mkdir -p /Users/ice/Desktop/cola/environment/mysql/master/conf
mkdir -p /Users/ice/Desktop/cola/environment/mysql/master/data

step1:在 docker 中创建并启动 MySQL 主服务器 端口 3307 (因为我本机已经启动了 mysql 服务了)

shell 复制代码
docker run -d \
-p 3307:3306 \
-v /Users/ice/Desktop/cola/environment/mysql/master/conf:/etc/mysql/conf.d \
-v /Users/ice/Desktop/cola/environment/mysql/master/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--name ss-mysql-master \
mysql:8.0.44

step2:创建 MySQL 主服务器配置文件

默认情况下 MySQL 的 binlog 日志是自动开启的,可以通过如下配置定义一些可选配置

conf 目录下新建 my.cnf,配置如下内容

properties 复制代码
[mysqld]
# 服务器唯一id,默认值 1
server-id=1
# 设置日志格式,默认值 ROW
binlog_format=ROW
# 二进制日志名,默认 binlog
# log-bin=binlog
# 设置需要复制的数据库,默认复制全部数据库
#binlog-do-db=mytestdb
# 设置不需要复制的数据库
#binlog-ignore-db=mysql
#binlog-ignore-db=infomation_schema

重启MySQL容器

shell 复制代码
docker restart ss-mysql-master

可以通过 docker ps 查看状态

binlog 格式说明

  • binlog_format=STATEMENT:日志记录的是主机数据库的写指令,性能高,但是 now() 之类的函数以及获取系统参数的操作会出现主从数据不同步的问题。
  • binlog_format=ROW(默认):日志记录的是主机数据库的写后的数据,批量操作时性能较差,解决 now() 或者 user() 或者 @@hostname 等操作在主从机器上不一致的问题。
  • binlog_format=MIXED:是以上两种 level 的混合使用,有函数用 ROW,没函数用 STATEMENT,但是无法识别系统变量

{% note info %}

以这三个容器的 ID 为例,后续我们测试主从同步,在主机上执行命令,插入一条数据 INSERT INTO t_user(uname) VALUES(@@hostname);,如果我们是用的 STATEMENT,那记录的是写指令,结果如下

  • master:uname: 17375537e83f
  • slave1:uname: 2444bd995d2b
  • slave2:uname: 836fcb7ab2f9

如果用的是 ROW,那么是记录的写入的数据,那么结果如下

  • master:uname: 17375537e83f
  • slave1:uname: 17375537e83f
  • slave2:uname: 17375537e83f
    {% endnote %}

binlog-ignore-db 和 binlog-do-db 的优先级问题

不配置过滤规则就全部记录;配置 binlog-do-db 就只记录指定库,忽略 binlog-ignore-db 配置;如果没配置 binlog-do-db,配置了 binlog-ignore-db 就排除指定库,其余都记录。

step3:使用命令行登录MySQL主服务器

shell 复制代码
# 进入容器:env LANG=C.UTF-8 避免容器中显示中文乱码
docker exec -it ss-mysql-master env LANG=C.UTF-8 /bin/bash
# 进入容器内的mysql命令行
mysql -uroot -p
# 修改默认密码校验方式,兼容新老版本
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';

用 Navicat 可以正确连接上即可。

{% note info %}

每次都做这个修改默认密码检验方式,主要是为了兼容 MySQL8 的登陆认证方式,MySQL8 默认认证方式是 caching_sha2_password,很多旧驱动、Navicat老版本、ShardingSphere旧版本可能连不上,所以就改为 mysql_native_password 认证方式,兼容性更好

{% endnote %}

step4:主机中创建 slave 用户

{% note info %}

从机要从主机同步数据,也就是从机开启一个 IO 线程读取主机 binlog 日志,那么就需要主机开设一个账号给从机连接用

{% endnote %}

sql 复制代码
-- 创建slave用户
CREATE USER 'ss_slave'@'%';
-- 设置密码
ALTER USER 'ss_slave'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
-- 授予复制权限
GRANT REPLICATION SLAVE ON *.* TO 'ss_slave'@'%';
-- 刷新权限
FLUSH PRIVILEGES;

ss_master 的 mysql 数据库下的 user 表,该用户就被创建出来了(只开启了远程复制的权限)

step5:主机中查询master状态

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

sql 复制代码
SHOW MASTER STATUS;

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

{% note warning %}

为什么不让操作主服务器 MYSQL 了?

因为 File + Position 是准确的复制起点,配置从库时需要写这个信息。如果你操作主库,比如建表,插入、修改,主库的 binlog 位置就会往后走,FilePosition 也会变化,这就不是主库最新状态了。

那新加入的数据在 File + Position 后面,从库不也是要同步吗?感觉没有冲突啊?

这是配置在从库的,标记为一个分界点,Position 之前是从库已经有了这部分数据,之后是从库需要同步的。

{% endnote %}

准备从服务器

可以配置多台从机 slave1、slave2...,这里以配置 slave1 为例

shell 复制代码
mkdir -p /Users/ice/Desktop/cola/environment/mysql/slave1/conf
mkdir -p /Users/ice/Desktop/cola/environment/mysql/slave1/data

step1:在 docker 中创建并启动 MySQL 从服务器端口3308

shell 复制代码
docker run -d \
-p 3308:3306 \
-v /Users/ice/Desktop/cola/environment/mysql/slave1/conf:/etc/mysql/conf.d \
-v /Users/ice/Desktop/cola/environment/mysql/slave1/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--name ss-mysql-slave1 \
mysql:8.0.44

step2:创建 MySQL 从服务器配置文件

也是创建 my.conf,配置如下内容

properties 复制代码
[mysqld]
# 服务器唯一id,每台服务器的id必须不同,如果配置其他从机,注意修改id
server-id=2
# 中继日志名,默认xxxxxxxxxxxx-relay-bin
#relay-log=relay-bin

重启 MySQL 容器

shell 复制代码
docker restart ss-mysql-slave1

step3:使用命令行登录 MySQL 从服务器

shell 复制代码
#进入容器:
docker exec -it ss-mysql-slave1 env LANG=C.UTF-8 /bin/bash
#进入容器内的mysql命令行
mysql -uroot -p
#修改默认密码校验方式
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';

测试连接,成功!

step4:在从机上配置主从关系

{% note info %}

因为我们是在 docker 容器上,没有 IP 什么的,有两种做法,一种是主机和从机创建网络来互联,这样 HOST 写容器名就可以(但是真实场景下不会三个 mysql 都在一个 docker 容器里面)

另一种做法是我们拿到本机 IP 就可以了,但是这个本机 IP 可能会变,所以我用 host.docker.internal 这个指代宿主机的方式来做。

生产环境下 HOST 肯定是固定的,替换就可以了

{% endnote %}

从机上执行以下 SQL 操作

sql 复制代码
CHANGE MASTER TO MASTER_HOST='host.docker.internal', 
MASTER_USER='ss_slave',MASTER_PASSWORD='123456', MASTER_PORT=3307,
MASTER_LOG_FILE='binlog.000003',MASTER_LOG_POS=1342; 

{% note info %}

注意这个里面的 MASTER_LOG_FILEMASTER_LOG_POS 要替换到前面说的 FilePosition

{% endnote %}

参考上面步骤创建第二个从服务器 slave2

{% hideToggle 创建第二个从服务器 %}

shell 复制代码
mkdir -p /Users/ice/Desktop/cola/environment/mysql/slave2/conf
mkdir -p /Users/ice/Desktop/cola/environment/mysql/slave2/data

step1:在 docker 中创建并启动 MySQL 从服务器端口3309

shell 复制代码
docker run -d \
-p 3309:3306 \
-v /Users/ice/Desktop/cola/environment/mysql/slave2/conf:/etc/mysql/conf.d \
-v /Users/ice/Desktop/cola/environment/mysql/slave2/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--name ss-mysql-slave2 \
mysql:8.0.44

step2:创建 MySQL 从服务器配置文件

也是创建 my.conf,配置如下内容

properties 复制代码
[mysqld]
# 服务器唯一id,每台服务器的id必须不同,如果配置其他从机,注意修改id
server-id=3
# 中继日志名,默认xxxxxxxxxxxx-relay-bin
#relay-log=relay-bin

重启 MySQL 容器

shell 复制代码
docker restart ss-mysql-slave2

step3:使用命令行登录 MySQL 从服务器

shell 复制代码
#进入容器:
docker exec -it ss-mysql-slave2 env LANG=C.UTF-8 /bin/bash
#进入容器内的mysql命令行
mysql -uroot -p
#修改默认密码校验方式
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';

step4:在从机上配置主从关系

从机上执行以下 SQL 操作(配置的主机信息,所以都是一样的)

sql 复制代码
CHANGE MASTER TO MASTER_HOST='host.docker.internal', 
MASTER_USER='ss_slave',MASTER_PASSWORD='123456', MASTER_PORT=3307,
MASTER_LOG_FILE='binlog.000003',MASTER_LOG_POS=1342; 

{% endhideToggle %}

启动主从同步

启动从机的复制功能,两个从机执行 SQL

sql 复制代码
START SLAVE;
-- 查看状态(不需要分号)
SHOW SLAVE STATUS\G

下面两个参数都是 Yes,则说明主从配置成功!

实现主从同步

在主机中执行以下SQL,在从机中查看数据库、表和数据是否已经被同步(可以从 Navicat 上执行)

sql 复制代码
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);

执行完后查看两个从机,数据库、表、表数据都有了

停止和重置

需要的时候,可以使用如下 SQL 语句

sql 复制代码
-- 在从机上执行。功能说明:停止 I/O 线程和 SQL 线程的操作。
stop slave; 

-- 在从机上执行。功能说明:用于删除 SLAVE 数据库的 relaylog 日志文件,并重新启用新的 relaylog 文件。
reset slave;

-- 在主机上执行。功能说明:删除所有的 binglog 日志文件,并将日志索引文件清空,重新开始所有新的日志文件。
-- 用于第一次进行搭建主从库时,进行主库 binlog 初始化工作;
reset master;

{% note warning %}

用在主从复制出问题、重新配置、重新初始化时用的。用到的时候再学习

{% endnote %}

常见问题

问题1

启动主从同步后,常见错误是 Slave_IO_Running: No 或者 Slave_IO_Running: Connecting 的情况,此时查看下方的 Last_IO_ERROR 错误日志,根据日志中显示的错误信息在网上搜索解决方案即可

典型的错误例如: Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'Client requested master to start replication from position > file size'

解决方案

sql 复制代码
-- 在从机停止 slave
SLAVE STOP;

-- 在主机查看 mater 状态
SHOW MASTER STATUS;
-- 在主机刷新日志
FLUSH LOGS;
-- 再次在主机查看 mater 状态(会发现 File 和 Position 发生了变化)
SHOW MASTER STATUS;
-- 修改从机连接主机的 SQL,并重新连接即可
问题2

{% note info %}

这种错误出现是因为部署在 Linux 服务器上的时候,没有先停止防火墙,直接 docker run 了。

{% endnote %}

启动 docker 容器后提示 WARNING: IPv4 forwarding is disabled. Networking will not work.

此错误,虽然不影响主从同步的搭建,但是如果想从远程客户端通过以下方式连接 docker 中的 MySQL 则没法连接

shell 复制代码
C:\Users\administrator>mysql -h 192.168.100.201 -P 3306 -u root -p

在 Linux 服务器上解决方案如下

shell 复制代码
#修改配置文件:
vim /usr/lib/sysctl.d/00-system.conf
#追加
net.ipv4.ip_forward=1
#接着重启网络
systemctl restart network

ShardingSphere-JDBC 读写分离

创建 SpringBoot 程序

项目名:sharding-jdbc-demo,我们用 JDK17,SpringBoot3。这里 ShardingSphere-JDBC 也用 5.5.2 版本。

添加依赖+配置读写分离

{% note info %}

这里版本有点乱,从 5.3.0 开始,shardingsphere-jdbc-core-spring-boot-starter 就废弃了,所以推荐用官网上的 shardingsphere-jdbc

{% endnote %}

我给出两个配置,第一种是 shardingsphere-jdbc-core-spring-boot-starter + SpringBoot2(SpringBoot3 好像兼容有问题) + snakeyaml,这是老版本,在官网上会有关于 Spring Boot Starter | 5.2.1 的教程

如上图官网在老版本还有 Spring Boot Starter 的教程

但是这种在最新版已经不支持这个 spring-boot-starter 了,网上说是废弃了,所以本节还有第两种配置方式。SpringBoot3 + ShardingSphere 5.5.2

如上图官网都没有 Spring Boot Starter 的教程了

{% note danger %}

版本变一点,配置、依赖都有变化!!!所以要紧跟官网去看配置,5.5.3 版本太多问题了,我就不配置了,推荐一个博客关于 5.5.2 的配置 SpringBoot3 + ShardingSphere 5.5.2

{% endnote %}

{% tabs 前置准备, 2 %}

SpringBoot 2.x 版本还需要手动引入 SnakeYAML,参看这里

xml 复制代码
<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.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.yaml</groupId>
        <artifactId>snakeyaml</artifactId>
        <version>2.2</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.16</version>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </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>
    </dependency>
</dependencies>

配置文件还是

properties 复制代码
# 应用名称
spring.application.name=sharging-jdbc-demo
# 开发环境设置
spring.profiles.active=dev
# 内存模式
spring.shardingsphere.mode.type=Standalone
spring.shardingsphere.mode.repository.type=JDBC

# 配置真实数据源
spring.shardingsphere.datasource.names=master,slave1,slave2
# 配置第 1 个数据源
# spring.shardingsphere.datasource.<actual-data-source-name>.type=
# 使用的默认的数据库连接池
spring.shardingsphere.datasource.master.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.master.jdbc-url=jdbc:mysql://localhost:3307/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.cj.jdbc.Driver
spring.shardingsphere.datasource.slave1.jdbc-url=jdbc:mysql://localhost:3308/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.cj.jdbc.Driver
spring.shardingsphere.datasource.slave2.jdbc-url=jdbc:mysql://localhost:3309/db_user
spring.shardingsphere.datasource.slave2.username=root
spring.shardingsphere.datasource.slave2.password=123456

# 这里 spring.shardingsphere.rules.readwrite-splitting.data-sources.<readwrite-splitting-data-source-name>
# readwrite_ds 是逻辑数据源名,可以随便起,这里用来告诉 ShardingSphere readwrite_ds = master + slave1 + slave2
# 写数据源名称
spring.shardingsphere.rules.readwrite-splitting.data-sources.readwrite_ds.static-strategy.write-data-source-name=master
# 读数据源名称,多个从数据源用逗号分隔
spring.shardingsphere.rules.readwrite-splitting.data-sources.readwrite_ds.static-strategy.read-data-source-names=slave1,slave2

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

# 负载均衡算法配置
# 负载均衡算法类型
spring.shardingsphere.rules.readwrite-splitting.load-balancers.load_balance_alg.type=ROUND_ROBIN

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

导入依赖,这里是 JDK 17 + SpringBoot3

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>shardingsphere-jdbc</artifactId>
        <version>5.5.2</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>3.5.16</version>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </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>
    </dependency>
</dependencies>

配置文件如下

properties 复制代码
# 配置 DataSource Driver
spring.datasource.driver-class-name=org.apache.shardingsphere.driver.ShardingSphereDriver
# 指定 YAML 配置文件
spring.datasource.url=jdbc:shardingsphere:classpath:config.yaml

在 resource 目录下创建 config.yaml

yaml 复制代码
# 模式配置,生产环境下一般是集群
mode:
  type: Standalone

# 配置真实数据源
dataSources:
  master: # 数据源名
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3307/db_user
    username: root
    password: 123456
  slave1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3308/db_user
    username: root
    password: 123456
  slave2:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3309/db_user
    username: root
    password: 123456
rules:
  - !SINGLE # 单表规则,不做分库分表的表
    tables:
      - readwrite_ds.t_user # t_user 表属于 readwrite_ds 这个数据源
    defaultDataSource: readwrite_ds # 如果某张表没指名数据源就去这里
  - !READWRITE_SPLITTING
    dataSourceGroups:
      readwrite_ds: # 逻辑数据源名 = master + slave1 + slave2
        writeDataSourceName: master # 写数据库
        readDataSourceNames: # 读数据库
          - slave1
          - slave2
        transactionalReadQueryStrategy: PRIMARY
        loadBalancerName: load_balance_alg
    loadBalancers: # 负载规则
      load_balance_alg:
        type: ROUND_ROBIN
# 开启日志
props:
  sql-show: true

{% endtabs %}

创建实体类

java 复制代码
package com.lh.shardingjdbc.entity;

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

创建 Mapper

java 复制代码
package com.lh.shardingjdbc.mapper;

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

测试

读写分离测试

java 复制代码
@SpringBootTest
public class ReadWriteTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testInsert() {
        User user = new User();
        user.setUname("二狗");
        userMapper.insert(user);
    }
}

实际插入的位置 Actual SQL: master。插入之后,三个数据库都有数据代表成功!

事务测试

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

  • 不添加 @Transactionalinsert 对主库操作,select 对从库操作
  • 添加 @Transactional:则 insertselect 均对主库操作
  • 注意 :在 JUnit 环境下的 @Transactional 注解,默认情况下,因为是测试,所以不管成功还是失败,都会对事务进行回滚(即使在没加注解 @Rollback,也会对事务回滚)
java 复制代码
/**
 * 事务测试
 */
@Transactional // 开启事务
@Test
public void testTrans(){

    User user = new User();
    user.setUname("二狗");
    userMapper.insert(user);

    List<User> users = userMapper.selectList(null);
}

开启事务,写和读都是走主库

java 复制代码
@Test
public void testTrans(){

    User user = new User();
    user.setUname("二狗");
    userMapper.insert(user);

    List<User> users = userMapper.selectList(null);
}

关上事务之后,写走主库,读走从库

如上效果是需要我们配置上 transactionalReadQueryStrategy: PRIMARY,代表事务内读走哪里,5.5.2 版本默认值是 DYNAMIC。官方解释如下

transactionalReadQueryStrategy 是事务内读请求的路由策略,可选值:PRIMARY(路由至主库)、FIXED(同一事务内路由至固定数据源)、DYNAMIC(同一事务内路由至非固定数据源)。默认值:DYNAMIC

真无语,5.5.3 是默认 PRIMARY,前面 5.1 版本都没有这个字段,5.5.2 又是 DYNAMIC,默认值来回变,所以显式配置上最安全

负载均衡测试

java 复制代码
/**
 * 读数据测试
 */
@Test
public void testSelectAll() {
    List<User> users1 = userMapper.selectList(null);
    List<User> users2 = userMapper.selectList(null);
    List<User> users3 = userMapper.selectList(null);
    List<User> users4 = userMapper.selectList(null);
}

是 1,2,1,2 这种轮询的,和我们的配置 ROUND_ROBIN 是一致的

负载均衡算法

官方提供了三种方式

  • ROUND_ROBIN 轮询负载均衡算法
  • RANDOM 随机负载均衡算法
  • WEIGHT 权重负载均衡算法(通过属性 props 配置权重)

比如权重负载均衡算法配置如下

yaml 复制代码
rules:
  - !READWRITE_SPLITTING
    dataSourceGroups:
      readwrite_ds: # 逻辑数据源名 = master + slave1 + slave2
        writeDataSourceName: master # 写数据库
        readDataSourceNames: # 读数据库
          - slave1
          - slave2
        transactionalReadQueryStrategy: PRIMARY
        loadBalancerName: load_balance_alg
    loadBalancers: # 负载规则
      load_balance_alg:
        type: WEIGHT
        props:
          slave1: 1
          slave2: 3

重新跑一下试试

四次请求,三次 slave2,一次 slave1

{% note danger %}

只有一个写库,所以 loadBalancer 配置的是读请求的负载均衡,不是写请求的。(如果有多个写库呢?官方也没说)

{% endnote %}

{% note success %}

本章介绍读写分离,目的是我们在操作业务的时候不考虑数据库读写具体在哪个上面,配置文件去配置读和写去哪个数据库,也有负载均衡规则处理

{% endnote %}

ShardingSphere-JDBC 垂直分片

一般情况下,垂直分表后的两张表还是在同一个数据库,因为毕竟两个表还是一张表,关联性强,查询的时候很可能多表查询,所以一般不进行分库,避免跨服务。并且这种垂直分片后就和多表查询一样,所以 ShardingSphere 不提供这种,只提供垂直分库。

{% note info %}

这是已经垂直分库了,然后 ShardingSphere-JDBC 如何处理。

{% endnote %}

准备服务器

服务器规划:使用 docker 方式创建如下容器

  • 服务器:容器名 server-user,端口 3301
  • 服务器:容器名 server-order,端口 3302

创建 server-user 容器

step1:创建容器

shell 复制代码
docker run -d \
-p 3301:3306 \
-v /Users/ice/Desktop/cola/environment/mysql/server/user/conf:/etc/mysql/conf.d \
-v /Users/ice/Desktop/cola/environment/mysql/server/user/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--name server-user \
mysql:8.0.44

step2:登录 MySQL 服务器

shell 复制代码
#进入容器:
docker exec -it server-user env LANG=C.UTF-8 /bin/bash
#进入容器内的mysql命令行
mysql -uroot -p
#修改默认密码插件
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';

step3:创建数据库

sql 复制代码
CREATE DATABASE db_user;
USE db_user;
CREATE TABLE t_user (
 id BIGINT AUTO_INCREMENT,
 uname VARCHAR(30),
 PRIMARY KEY (id)
);

创建 server-order 容器

step1:创建容器

shell 复制代码
docker run -d \
-p 3302:3306 \
-v /Users/ice/Desktop/cola/environment/mysql/server/order/conf:/etc/mysql/conf.d \
-v /Users/ice/Desktop/cola/environment/mysql/server/order/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--name server-order \
mysql:8.0.44

step2:登录 MySQL 服务器

shell 复制代码
#进入容器:
docker exec -it server-order env LANG=C.UTF-8 /bin/bash
#进入容器内的mysql命令行
mysql -uroot -p
#修改默认密码插件
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';

step3:创建数据库

sql 复制代码
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) 
);

程序实现

创建实体类

java 复制代码
package com.lh.shardingjdbc.entity;

@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_order") 是逻辑表名,因为没有指定是哪个数据库呢,只说了表名

创建Mapper

java 复制代码
package com.lh.shardingjdbc.mapper;

@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}

{% note warning %}

如果没有 User 类要创建上,就是前面已经创建过的 User

{% endnote %}

配置垂直分片

yaml 复制代码
# 模式配置,生产环境下一般是集群
mode:
  type: Standalone

# 配置真实数据源
dataSources:
  server-user: # 数据源名
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3301/db_user
    username: root
    password: 123456
  server-order:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3302/db_order
    username: root
    password: 123456
rules:
  - !SHARDING # 配置垂直分库
    tables:
      t_user: # 逻辑表名,对应 @TableName("t_user")
        actualDataNodes: server-user.t_user # 真实数据源和真实表
        databaseStrategy:
          none: # 还得加冒号 :
        tableStrategy:
          none:
      t_order:
        actualDataNodes: server-order.t_order
        databaseStrategy:
          none:
        tableStrategy:
          none:
# 开启日志
props:
  sql-show: true

{% note warning %}

none: 注意还要加冒号

{% endnote %}

测试垂直分片

java 复制代码
package com.atguigu.shardingjdbcdemo;

@SpringBootTest
public class ShardingTest {


    @Autowired
    private UserMapper userMapper;

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 垂直分片:插入数据测试
     */
    @Test
    public void testInsertOrderAndUser(){

        User user = new User();
        user.setUname("二狗");
        userMapper.insert(user);

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

    }

    /**
     * 垂直分片:查询数据测试
     */
    @Test
    public void testSelectFromOrderAndUser(){
        User user = userMapper.selectById(1L);
        Order order = orderMapper.selectById(1L);
    }
}

插入测试结果

没毛,插入用户路由到了 server-user,插入订单路由到了 server-order

查询测试结果

查询也没问题

{% note success %}

本章介绍垂直分库,目的是我们在操作业务的时候不考虑某张表具体在哪个数据库上面,配置文件去配置哪个表去哪个数据库(如果加上读写分离,就是哪个表去哪些数据库上,在这些数据库里,哪个数据库负责写,哪些数据库负责读,配置就是 actualDataNodes: readwrite_ds.t_user,剩下都不变)

{% hideToggle 配置如下 %}

yaml 复制代码
rules:
  - !READWRITE_SPLITTING
    dataSourceGroups:
      user_rw:
        writeDataSourceName: user_master
        readDataSourceNames:
          - user_slave1
          - user_slave2
        transactionalReadQueryStrategy: PRIMARY
        loadBalancerName: user_lb

      order_rw:
        writeDataSourceName: order_master
        readDataSourceNames:
          - order_slave1
          - order_slave2
        transactionalReadQueryStrategy: PRIMARY
        loadBalancerName: order_lb

    loadBalancers:
      user_lb:
        type: ROUND_ROBIN

      order_lb:
        type: ROUND_ROBIN

  - !SHARDING
    tables:
      t_user:
        actualDataNodes: user_rw.t_user
        databaseStrategy:
          none:
        tableStrategy:
          none:

      t_order:
        actualDataNodes: order_rw.t_order
        databaseStrategy:
          none:
        tableStrategy:
          none:

    defaultDatabaseStrategy:
      none:
    defaultTableStrategy:
      none:

{% endhideToggle %}

{% endnote %}

ShardingSphere-JDBC 水平分片

准备服务器

服务器规划:使用 docker 方式创建如下容器

  • 服务器:容器名 server-order0,端口 3310
  • 服务器:容器名 server-order1,端口 3311

创建 server-order0 容器

step1:创建容器

shell 复制代码
docker run -d \
-p 3310:3306 \
-v /Users/ice/Desktop/cola/environment/mysql/server/order0/conf:/etc/mysql/conf.d \
-v /Users/ice/Desktop/cola/environment/mysql/server/order0/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--name server-order0 \
mysql:8.0.44

step2:登录 MySQL 服务器

shell 复制代码
#进入容器:
docker exec -it server-order0 env LANG=C.UTF-8 /bin/bash
#进入容器内的mysql命令行
mysql -uroot -p
#修改默认密码插件
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';

step3:创建数据库

注意 水平分片的 id 需要在业务层实现,不能依赖数据库的主键自增,所以没有在创建表的时候带主键自增

{% note info %}

原因很简单,因为不同的 order 表有自己独立的自增序列,多张表的 id 就会重复。

{% endnote %}

sql 复制代码
CREATE DATABASE db_order;
USE db_order;
CREATE TABLE t_order0 (
  id BIGINT,
  order_no VARCHAR(30),
  user_id BIGINT,
  amount DECIMAL(10,2),
  PRIMARY KEY(id) 
);
CREATE TABLE t_order1 (
  id BIGINT,
  order_no VARCHAR(30),
  user_id BIGINT,
  amount DECIMAL(10,2),
  PRIMARY KEY(id) 
);

创建 server-order1 容器

step1:创建容器

shell 复制代码
docker run -d \
-p 3311:3306 \
-v /Users/ice/Desktop/cola/environment/mysql/server/order1/conf:/etc/mysql/conf.d \
-v /Users/ice/Desktop/cola/environment/mysql/server/order1/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--name server-order1 \
mysql:8.0.44

step2:登录MySQL服务器

shell 复制代码
#进入容器:
docker exec -it server-order1 env LANG=C.UTF-8 /bin/bash
#进入容器内的mysql命令行
mysql -uroot -p
#修改默认密码插件
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';

step3:创建数据库和 server-order0 相同

注意 水平分片的 id 需要在业务层实现,不能依赖数据库的主键自增

sql 复制代码
CREATE DATABASE db_order;
USE db_order;
CREATE TABLE t_order0 (
  id BIGINT,
  order_no VARCHAR(30),
  user_id BIGINT,
  amount DECIMAL(10,2),
  PRIMARY KEY(id) 
);
CREATE TABLE t_order1 (
  id BIGINT,
  order_no VARCHAR(30),
  user_id BIGINT,
  amount DECIMAL(10,2),
  PRIMARY KEY(id) 
);

基本水平分片

基本配置

yaml 复制代码
# 模式配置,生产环境下一般是集群
mode:
  type: Standalone
# 开启日志
props:
  sql-show: true

数据源配置

yaml 复制代码
# 配置真实数据源
dataSources:
  server-user: # 数据源名
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3301/db_user
    username: root
    password: 123456
  server-order0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3310/db_order
    username: root
    password: 123456
  server-order1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3311/db_order
    username: root
    password: 123456

标椎分片表配置

yaml 复制代码
# 值由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。
rules:
  - !SHARDING
    tables:
      t_user: # 逻辑表名,对应 @TableName("t_user")
        actualDataNodes: server-user.t_user # 真实数据源和真实表
        databaseStrategy:
          none:
        tableStrategy:
          none:
      t_order:
        actualDataNodes: server-order0.t_order0,server-order0.t_order1,server-order1.t_order0,server-order1.t_order1
        databaseStrategy:
          none:
        tableStrategy:
          none:

先做个简单测试看看

java 复制代码
/**
 * 水平分片:插入数据测试
 */
@Test
public void testInsertOrder(){

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

报错了?原因是因为我们把 t_order 配成了分片表,但是这条 insert SQL 里没有带上 ShardingSphere 需要的分片键,所以它不知道这个数据要往哪个数据库里的哪个表插入。

那我们只留一个试试

yaml 复制代码
actualDataNodes: server-order0.t_order_0

再插入还是报错,报错原因 Cause: java.sql.SQLException: Field 'id' doesn't have a default value,因为我们的 t_order 表设置如下

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

这里配置主键是自增的,但是实际上数据库 t_order 表中 id 是主键,但是没有自增策略,所以必须手动插入

{% note info %}

这里的 @TableId(type = IdType.AUTO) 的作用是告诉 ORM,数据库表的 id 字段是自增的,我们生成插入语句的时候就不用管 id 字段了,所以相当于帮我们生成的语句是

sql 复制代码
INSERT INTO t_order  ( order_no, user_id, amount )  VALUES (  ATGUIGU001, 1, 100  )

没有 id,但是 id 又是主键,数据库又没有自增策略,插入的时候不知道 id 的值,就报错了

{% endnote %}

修改 Order 实体类的主键策略

java 复制代码
// @TableId(type = IdType.AUTO) //依赖数据库的主键自增策略
@TableId(type = IdType.ASSIGN_ID) // 分布式 id,默认的雪花算法

再跑一下就插入成功了

行表达式

优化上一步的分片表配置

yaml 复制代码
rules:
  - !SHARDING
    tables:
      t_user: # 逻辑表名,对应 @TableName("t_user")
        actualDataNodes: server-user.t_user # 真实数据源和真实表
        databaseStrategy:
          none:
        tableStrategy:
          none:
      t_order:
        actualDataNodes: server-order$->{0..1}.t_order$->{0..1}
        databaseStrategy:
          none:
        tableStrategy:
          none:

上面这种形式就等价于

yaml 复制代码
actualDataNodes: server-order0.t_order0,server-order0.t_order1,server-order1.t_order0,server-order1.t_order1

{% note warning %}

其实可以用 ${ expr } 或者 $->{ expr } 的形式,更推荐后者,因为前者在 .properties 文件中 ${} 容易被 Spring 当成占位符处理,容易冲突

{% endnote %}

详细可以参考 ShardingSphere | 行表达式

{% hideToggle 小例子 %}

text 复制代码
db0
  ├── t_order0
  └── t_order1
db1
  ├── t_order0
  └── t_order1

可以写为

groovy 复制代码
db$->{0..1}.t_order$->{0..1}
text 复制代码
db0
  ├── t_order0
  └── t_order1
db1
  ├── t_order2
  ├── t_order3
  └── t_order4

可以写为

groovy 复制代码
db0.t_order$->{0..1},db1.t_order$->{2..4}
text 复制代码
db0
  ├── t_order01
  ├── t_order02
  ├── t_order03
  ├── t_order04
  ├── t_order05
  ├── t_order06
  ├── t_order07
  ├── t_order08
  ├── t_order09
  ├── t_order10
  .........
  └── t_order20

可以写为

groovy 复制代码
db0.t_order0$->{0..9}, db0.t_order$->{10..20}

{% endhideToggle %}

分片算法配置

水平分库

分片规则:order 表中 user_id 为偶数时,数据插入 server-order0 服务器,user_id 为奇数时,数据插入 server-order1 服务器。这样分片的好处是,同一个用户的订单数据,一定会被插入到同一台服务器上,查询一个用户的订单时效率较高。

参考

yaml 复制代码
rules:
  - !SHARDING
    tables:
      t_user: # 逻辑表名,对应 @TableName("t_user")
        actualDataNodes: server-user.t_user # 真实数据源和真实表
        databaseStrategy:
          none:
        tableStrategy:
          none:
      t_order:
        actualDataNodes: server-order$->{0..1}.t_order$->{0..1}
        databaseStrategy: # 分库策略
          standard: # 用于单分片键的标准分片场景
            shardingColumn: user_id # 分片列名称
            shardingAlgorithmName: alg_inline_userid # 分片算法名称
        tableStrategy: # 分表策略
          none:
    shardingAlgorithms:
      alg_inline_userid: # 分片算法名称
        type: INLINE # 分片算法类型
        props: # 分片算法属性配置
          algorithm-expression: server-order$->{user_id % 2}

为了方便测试,先设置只在 t_order0 表上进行测试

properties 复制代码
actualDataNodes: server-order$->{0..1}.t_order0

{% note info %}

因为我们目前还只是在分库,还没具体分表,所以如果就这样运行还会报错

{% endnote %}

测试

java 复制代码
/**
 * 水平分片:分库插入数据测试
 */
@Test
public void testInsertOrderDatabaseStrategy(){

    for (long i = 0; i < 4; i++) {
        Order order = new Order();
        order.setOrderNo("ATGUIGU001");
        order.setUserId(i + 1);
        order.setAmount(new BigDecimal(100));
        orderMapper.insert(order);
    }

}

可以看到,奇数 userId 插入 server-order1,偶数插入 server-order0

{% note info %}

看官网还有非常多的分片算法,比如我们还可以配置取模分片算法

yaml 复制代码
rules:
  - !SHARDING
    tables:
      # ...	
      t_order:
        actualDataNodes: server-order$->{0..1}.t_order$->{0..1}
        databaseStrategy: # 分库策略
          standard: # 用于单分片键的标准分片场景
            shardingColumn: user_id # 分片列名称
            shardingAlgorithmName: alg_mod # 分片算法名称
        tableStrategy: # 分表策略
          none:
    shardingAlgorithms:
      alg_mod: # 分片算法名称
        type: MOD # 分片算法类型
        props: # 分片算法属性配置
          sharding-count: 2

具体是余 0 的到哪个库,余 1 到到哪个库,跟我们列举库的顺序有关

但是这种写法实际上不可以,后面会说

{% endnote %}

{% note danger %}

但是这种取模的方式不利于后续数据库扩展,比如新加一个数据库,然后 sharding-count 变为 3,那原来旧数据位置不等于新路由的位置,除非你把所有数据重新分布迁移一下

生产上几种做法如下

  1. 一开始就多一些表,比如刚开始2个库,4张表,当扩展到4个库的时候,让每个库两个表,把原来部分表迁移到新库(比原来好一点了,是一部分数据迁移)

  2. 固定分片数,比如 user_id % 1024,然后按照逻辑分片编号,映射到具体数据库

    text 复制代码
    逻辑分片 0~511   → server-order0
    逻辑分片 512~1023 → server-order1

    这样扩展到时候,只要调整逻辑分片到物理库的映射关系,并且再迁移部分就可以了

  3. 按照时间范围分片(适合订单、日志类数据) --> 有时间段分片算法
    比如,t_order_202501 t_order_202502,缺点是按用户查询所有订单可能跨表,当然,如果查用户订单不带时间范围,就很有可能也跨表
    {% endnote %}

水平分表

分片规则:order 表中 order_no 的哈希值为偶数时,数据插入对应服务器的 t_order0 表,order_no 的哈希值为奇数时,数据插入对应服务器的 t_order1 表。因为 order_no 是字符串形式,因此不能直接取模。

diff 复制代码
rules:
  - !SHARDING
    tables:
      # ...
      t_order:
        actualDataNodes: server-order$->{0..1}.t_order$->{0..1}
        databaseStrategy: # 分库策略
          standard: # 用于单分片键的标准分片场景
            shardingColumn: user_id # 分片列名称
            shardingAlgorithmName: alg_inline_userid # 分片算法名称
        tableStrategy: # 分表策略
+         standard:
+           shardingColumn: order_no # 分片列名称
+           shardingAlgorithmName: alg_hash_mod # 分片算法名称
    shardingAlgorithms:
      alg_inline_userid: # 分片算法名称
        type: INLINE # 分片算法类型
        props: # 分片算法属性配置
          algorithm-expression: server-order$->{user_id % 2}
+     alg_hash_mod:
+       type: INLINE
+       props:
+         algorithm-expression: t_order$->{Math.abs(order_no.hashCode()) % 2}

{% note info %}

这里用 INLINE 而不是 HASH_MOD,因为 INLINE 更灵活。原因在于,如果用 tables 可以分别配置分库分表策略,会更灵活,缺点是只能使用标准分片、复合分片、Hint 分片,没办法使用自动分片。

自动分片需要在 autoTables 中配置,但是这种方式不能把分库策略和分表策略分开配置,而是用同一个自动分片策略。官网也说了

自动分片算法的分片逻辑由 ShardingSphere 自动管理,需要通过配置 autoTables 分片规则进行使用。

yaml 复制代码
  autoTables: # 自动分片表规则配置
    t_order_auto: # 逻辑表名称
      actualDataSources (?): # 数据源名称
      shardingStrategy: # 切分策略
        standard: # 用于单分片键的标准分片场景
          shardingColumn: # 分片列名称
          shardingAlgorithmName: # 自动分片算法名称

真是无数次想吐槽这个官网,,,,

{% endnote %}

注意 actualDataNodes 可以用正式的了

yaml 复制代码
actualDataNodes: server-order$->{0..1}.t_order$->{0..1}

测试

java 复制代码
/**
 * 水平分片:分表插入数据测试
 */
@Test
public void testInsertOrderTableStrategy(){

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

        Order order = new Order();
        order.setOrderNo("ATGUIGU" + i);
        order.setUserId(1L);
        order.setAmount(new BigDecimal(100));
        orderMapper.insert(order);
    }

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

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

查询测试

java 复制代码
/**
 * 水平分片:查询所有记录
 * 查询了两个数据源,每个数据源中使用 UNION ALL 连接两个表
 */
@Test
public void testShardingSelectAll(){

    List<Order> orders = orderMapper.selectList(null);
    orders.forEach(System.out::println);
}
  • 查 server-order0 中的 t_order0 数据并且 UNION ALL t_order1 数据
  • 查 server-order1 中的 t_order0 数据并且 UNION ALL t_order1 数据
  • 合并查询结果返回
java 复制代码
/**
 * 水平分片:根据 user_id 查询记录
 * 查询了一个数据源,每个数据源中使用 UNION ALL 连接两个表
 */
@Test
public void testShardingSelectByUserId(){

    QueryWrapper<Order> orderQueryWrapper = new QueryWrapper<>();
    orderQueryWrapper.eq("user_id", 1L);
    List<Order> orders = orderMapper.selectList(orderQueryWrapper);
    orders.forEach(System.out::println);
}
  • 查 server-order1 中的 t_order0 数据并且 UNION ALL t_order1 数据
  • 合并查询结果返回

{% note warning %}

考虑一下范围、分页、分组、排序?

只要 SQL 不能根据分片键精准定位到某个库/表,就可能跨库跨表查询(也可能只跨库或者只跨表);跨库跨表后,排序、分组、聚合、分页都需要 ShardingSphere 对多个结果集做归并处理。

{% endnote %}

分布式序列算法

雪花算法

yaml 配置

分布式序列算法

{% note warning %}

也提供了 UUID 的,但是 UUID 不适合做 MySQL Innodb 存储引擎主键,插入性能比较差

{% endnote %}

背景:数据分片后,不同数据节点生成全局唯一主键是非常棘手的问题。同一个逻辑表内的不同实际表之间的自增键由于无法互相感知而产生重复主键。 虽然可通过约束自增主键初始值和步长的方式避免碰撞,但需引入额外的运维规则,使解决方案缺乏完整性和可扩展性。

水平分片需要关注全局序列,因为不能简单的使用基于数据库的主键自增。这里有两种方案,选择其中一个就可以(如果没引入 MyBatis-Plus,就只能用第二个了)

基于 MyBatisPlus
java 复制代码
@TableId(type = IdType.ASSIGN_ID)
private Long id;
基于 ShardingSphere-JDBC
diff 复制代码
rules:
  - !SHARDING
    tables:
      # ...
      t_order:
        # ...
+	keyGenerateStrategy: # 分布式序列策略
+	   column: id # 自增列名称,缺省表示不使用自增主键生成器
+	   keyGeneratorName: alg_snowflake # 分布式序列算法名称
   # 分布式序列算法配置
+  keyGenerators:
+    alg_snowflake: # 分布式序列算法名称
+      type: SNOWFLAKE # 分布式序列算法类型
+      # props: # 分布式序列算法属性配置

{% note danger %}

这里是用的 id 属性来生成分布式主键,id 没有作为分片值(前面用的是 user_idorder_no 做的分片值)。如果它是分片值,还需要配置属性 max-vibration-offset,详细信息看官网 分布式序列算法

{% endnote %}

此时,需要将实体类中的 id 策略修改成以下形式(因为 MyBatis-Plus 的雪花算法优先级更高,如果不注释掉我们的配置白费)

我们替换为 @TableId(type = IdType.AUTO)

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

然后我们再测试是否能插入成功

java 复制代码
@Test
public void testInsertOrderTableStrategy(){

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

        Order order = new Order();
        order.setOrderNo("ATGUIGU" + i);
        order.setUserId(1L);
        order.setAmount(new BigDecimal(100));
        orderMapper.insert(order);
    }

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

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

之前用 @TableId(type = IdType.AUTO) 因为数据库没有主键自增所以报错,之后我们使用 @TableId(type = IdType.ASSIGN_ID) 来解决主键问题。这里又设置回 @TableId(type = IdType.AUTO),但是使用 ShardingSphere 配置了 id 的分布式序列,能够插入成功,说明我们的生效了

多表关联

创建关联表

server-order0、server-order1 服务器中分别创建两张订单详情表 t_order_item0、t_order_item1

我们希望同一个用户的订单表和订单详情表中的数据都在同一个数据源中,避免跨库关联,因此这两张表我们使用相同的分片策略。

那么在 t_order_item 中我们也需要创建 order_nouser_id 这两个分片键

sql 复制代码
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)
);

{% note info %}

一个订单里面可能有多个商品,所以一个订单可以关联多个订单详情表,用 order_no 作为关联,id 用雪花算法生成

{% endnote %}

创建实体类

java 复制代码
package com.lh.shardingjdbc.entity;

@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;
}

创建Mapper

java 复制代码
package com.lh.shardingjdbc.mapper;

@Mapper
public interface OrderItemMapper extends BaseMapper<OrderItem> {

}

配置关联表

t_order_item 的分片表、分片策略、分布式序列策略和 t_order 一致,只要在原来的基础上加上下面的配置就可以,和 t_order 配置一样的

diff 复制代码
rules:
  - !SHARDING
    tables:
      t_user: # 逻辑表名,对应 @TableName("t_user")
        actualDataNodes: server-user.t_user # 真实数据源和真实表
        databaseStrategy:
          none:
        tableStrategy:
          none:
      t_order:
        actualDataNodes: server-order$->{0..1}.t_order$->{0..1}
        databaseStrategy: # 分库策略
          standard: # 用于单分片键的标准分片场景
            shardingColumn: user_id # 分片列名称
            shardingAlgorithmName: alg_inline_userid # 分片算法名称
        tableStrategy: # 分表策略
          standard:
            shardingColumn: order_no # 分片列名称
-           shardingAlgorithmName: alg_hash_mod # 分片算法名称
+           shardingAlgorithmName: alg_order_hash_mod # 分片算法名称
        keyGenerateStrategy: # 分布式序列策略
          column: id # 自增列名称,缺省表示不使用自增主键生成器
          keyGeneratorName: alg_snowflake # 分布式序列算法名称
+     t_order_item:
+       actualDataNodes: server-order$->{0..1}.t_order_item$->{0..1}
+       databaseStrategy: # 分库策略
+         standard: # 用于单分片键的标准分片场景
+           shardingColumn: user_id # 分片列名称
+           shardingAlgorithmName: alg_inline_userid # 分片算法名称
+       tableStrategy: # 分表策略
+         standard:
+           shardingColumn: order_no # 分片列名称
+           shardingAlgorithmName: alg_order_item_hash_mod # 分片算法名称
+       keyGenerateStrategy: # 分布式序列策略
+         column: id # 自增列名称,缺省表示不使用自增主键生成器
+         keyGeneratorName: alg_snowflake # 分布式序列算法名称
    shardingAlgorithms:
      alg_inline_userid: # 分片算法名称
        type: INLINE # 分片算法类型
        props: # 分片算法属性配置
          algorithm-expression: server-order$->{user_id % 2}
-     alg_hash_mod:          
+     alg_order_hash_mod:
        type: INLINE
        props:
          algorithm-expression: t_order$->{Math.abs(order_no.hashCode()) % 2}
+     alg_order_item_hash_mod:
+       type: INLINE
+       props:
+         algorithm-expression: t_order_item$->{Math.abs(order_no.hashCode()) % 2}
    # 分布式序列算法配置
    keyGenerators:
      alg_snowflake: # 分布式序列算法名称
        type: SNOWFLAKE # 分布式序列算法类型

测试插入数据

同一个用户的订单表和订单详情表中的数据都在同一个数据源中,避免跨库关联

java 复制代码
/**
 * 测试关联表插入
 */
@Test
public void testInsertOrderAndOrderItem(){

    for (long i = 1; i < 3; 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("ATGUIGU" + i);
            orderItem.setUserId(1L);
            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("ATGUIGU" + i);
            orderItem.setUserId(2L);
            orderItem.setPrice(new BigDecimal(1));
            orderItem.setCount(3);
            orderItemMapper.insert(orderItem);
        }
    }

}

按照我们的规则,如果订单插入 server-orderX.t_orderY,那么对应的订单详情表也会插入 server-orderX.t_order_itemY

绑定表

需求 查询每个订单的订单号和总订单金额

创建 VO 对象

java 复制代码
package com.lh.shardingjdbc.entity;

@Data
public class OrderVo {
    private String orderNo;
    private BigDecimal amount;
}

添加 Mapper 方法

java 复制代码
package com.lh.shardingjdbc.mapper;

@Mapper
public interface OrderMapper extends BaseMapper<Order> {

    @Select({"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"})
    List<OrderVo> getOrderAmount();

}

测试关联查询

java 复制代码
/**
 * 测试关联表查询
 */
@Test
public void testGetOrderAmount(){

    List<OrderVo> orderAmountList = orderMapper.getOrderAmount();
    orderAmountList.forEach(System.out::println);
}

我们可以看到查询结果是没有问题的,我们再看看查询过程

有 8 个实际的 SQL,但是实际上是不是 4 个就够了?因为我们 order 表和 order_item 表的分库分表规则是一样的,所以是不需要 t_order0 JOIN t_order_item1t_order1 JOIN t_order_item0 的,解决办法就是下面的配置绑定表

配置绑定表

绑定表配置规则

在原来水平分片配置的基础上添加如下配置

yaml 复制代码
rules:
  - !SHARDING
    bindingTables:
      - t_order, t_order_item

配置完绑定表后再次进行关联查询的测试

  • 如果不配置绑定表:测试的结果为 8 个 SQL。多表关联查询会出现笛卡尔积关联。
  • 如果配置绑定表:测试的结果为 4 个 SQL。多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。

ShardingSphere | 绑定表 指分片规则一致的一组分片表。 使用绑定表进行多表关联查询时,必须使用分片键进行关联,否则会出现笛卡尔积关联或跨库关联,从而影响查询效率。

{% note info %}

绑定表要求多张表的分库分表规则在逻辑上保持一致,不仅要分表数量一致,还要数据库路由位置一致、分片后缀对应一致。也就是说,server-order0.t_order0 应该对应 server-order0.t_order_item0,不能跨库对应,也不能 0 对 1。只有这样,ShardingSphere 才能在 JOIN 时根据主表路由结果推导出从表路由结果,避免笛卡尔积路由。

我们这个 SQL 语句

sql 复制代码
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

因为没有 WHERE user_id = ?,所以不能定位到某个数据库;也没有 WHERE order_no = ?,所以不能定位到某张分表。因此它仍然会查所有相关分片。重点是:绑定表不是让它只查一个分片,而是避免无意义的笛卡尔积 JOIN。

没有配置绑定表时,ShardingSphere 不敢假设 t_order0 一定对应 t_order_item0,因为两张表分片规则可能不一样,比如一张表按照 order_no % 2,另一个按照 item_id % 2,即使都是两个库两个表,也不一定一一对应。而配置了 bindingTables: t_order,t_order_item 绑定表,ShardingSphere 会认为 t_order 和 t_order_item 的数据分布位置一致,于是按照相同数据库、相同分片后缀进行一一对应 JOIN,避免全分片之间的笛卡尔积 JOIN。

{% endnote %}

广播表

什么是广播表

指所有的分片数据源中都存在的表,表结构及其数据在每个数据库中均完全一致。 适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。

广播具有以下特性

  1. 插入、更新操作会实时在所有节点上执行,保持各个分片的数据一致性
  2. 查询操作,只从一个节点获取
  3. 可以跟任何一个表进行 JOIN 操作

创建广播表

{% note info %}

因为字典表每个表查询都可能用到,所以一般每个节点都有一份字典表,数据一致。这样避免某个表在某些库里面没有,但是又频繁用到,导致每次都还需要跨库关联

{% endnote %}

在 server-order0、server-order1 和 server-user 服务器中分别创建 t_dict 表

sql 复制代码
CREATE TABLE t_dict(
    id BIGINT,
    dict_type VARCHAR(200),
    PRIMARY KEY(id)
);

程序实现

创建实体类
java 复制代码
package com.lh.shardingjdbc.entity;

@TableName("t_dict")
@Data
public class Dict {
    //可以使用MyBatisPlus的雪花算法
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    private String dictType;
}
创建 Mapper
java 复制代码
package com.lh.shardingjdbc.mapper;

@Mapper
public interface DictMapper extends BaseMapper<Dict> {
}
配置广播表
yaml 复制代码
dataSources:
  server-user: # 数据源名
    # ...
  server-order0:
    # ...
  server-order1:
    # ...
rules:
  - !BROADCAST
    tables: # 广播表规则列表
      - t_dict

{% note info %}

这种配置是说所有我们配置的数据源全部都有这个 t_dict,不能说某些数据库有 t_dict,某些没有,那不符合广播表的定义。

广播表的含义是这张表在 ShardingSphere 管理的所有真实数据源里都存在一份,广播表写的时候同步写所有库,读的时候一般只读其中一个库;如果和分片表 JOIN,就读分片表所在库里的那份广播表。

{% endnote %}

测试广播表

java 复制代码
@Autowired
private DictMapper dictMapper;

/**
 * 广播表:每个服务器中的t_dict同时添加了新数据
 */
@Test
public void testBroadcast(){

    Dict dict = new Dict();
    dict.setDictType("type1");
    dictMapper.insert(dict);
}

三个都插入了

java 复制代码
/**
 * 查询操作,只从一个节点获取数据
 * 随机负载均衡规则
 */
@Test
public void testSelectBroadcast(){

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

{% note danger %}

yaml 复制代码
rules:
  - !SHARDING
	tables:
      t_dict:
        actualDataNodes: server-order$->{0..1}.t_dict
        databaseStrategy:
          none:
        tableStrategy:
          none:

你可能会想着我这样配置呢,设定数据节点,这样不就可以自定义了?注意,这里配置的是分片规则,如果你这么配置,在插入的时候会报错 avoid same record in table 't_dict' routing to multiple data nodes. 因为我们没设置分库分表的规则,插入时不知道路由到哪里,查询不会报错,但是会把两个库里面两个表的数据都查出来并且合并,这样是重复的,也就是说如果你加上这个,就覆盖了广播表规则,你在设置 t_dict 的分片规则,不再是一个广播表了!!!二者只能选其一去配置,因为定义上不呢了就冲突,所以要么分片要么广播

{% endnote %}

启动 ShardingSphere-Proxy

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

我们学习前两种

安装

使用二进制发布包安装

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

step1:解压二进制包

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

  • Windows:使用解压软件解压文件

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

    shell 复制代码
    tar -zxvf apache-shardingsphere-5.5.2-shardingsphere-proxy-bin.tar.gz

解压后 bin 文件夹下有三个文件

  • start.bat Windows 启动
  • start.sh Linux 启动
  • stop.sh 关闭

step2:MySQL 驱动

下载解压取 jar 包 mysql-connector-j-9.7.0.jar,将 MySQl 驱动放至解压目录中的 ext-lib 目录(新建这个目录)

spte3:修改配置 conf/global.yaml

二进制发布包配置

认证和授权

yaml 复制代码
authority:
  users:
    - user: root@%
      password: root
      admin: true
  privilege:
    type: ALL_PERMITTED

props:
  sql-show: true

spte4:启动 ShardingSphere-Proxy

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

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

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

默认端口号是 3307,比如我想改运行端口号就执行命令

sh 复制代码
bin/start.bat 3307

step5:远程连接 ShardingSphere-Proxy

shell 复制代码
mysql -h127.0.0.1 -P3307 -uroot -p

这里别忘了使用 ./stop.sh 关闭它,我还是喜欢用 Docker 方式。

使用 Docker 安装

step1:启动 Docker 容器

先创建一个临时容器(让默认配置先搞到本地,当然如果本地文件夹里面已经有配置了,就不需要这么做了)

shell 复制代码
docker create --name temp-ss-proxy apache/shardingsphere-proxy:5.5.2

把容器里的默认配置复制到我们本机目录

shell 复制代码
docker cp temp-ss-proxy:/opt/shardingsphere-proxy/conf /Users/ice/Desktop/cola/environment/ShardingSphere/proxy-a/

然后删掉临时容器

shell 复制代码
docker rm temp-ss-proxy

再用下面的命令启动(如果我们之前已经成功启动过,前面的步骤可以跳过)

shell 复制代码
docker run -d \
-v /Users/ice/Desktop/cola/environment/ShardingSphere/proxy-a/conf:/opt/shardingsphere-proxy/conf \
-v /Users/ice/Desktop/cola/environment/ShardingSphere/proxy-a/ext-lib:/opt/shardingsphere-proxy/ext-lib \
-p 3321:3307 \
--name ss-proxy-a \
apache/shardingsphere-proxy:5.5.2

{% note info %}

可以添加参数减少启动需要的内存

shell 复制代码
-e ES_JAVA_OPTS="-Xmx256m -Xms256m -Xmn128m"

{% endnote %}

step2:上传 MySQL 驱动

将 MySQl 驱动上传至 /Users/ice/Desktop/cola/environment/ShardingSphere/proxy-a/ext-lib 目录

spte3:修改配置 global.yaml

修改 /Users/ice/Desktop/cola/environment/ShardingSphere/proxy-a/conf 中的配置文件

yaml 复制代码
authority:
  users:
    - user: root@%
      password: root
      admin: true
  privilege:
    type: ALL_PERMITTED

props:
  sql-show: true

spte4:重启容器

shell 复制代码
docker restart ss-proxy-a

step5:远程连接 ShardingSphere-Proxy

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

shell 复制代码
mysql -h127.0.0.1 -P3321 -uroot -p

{% note info %}

这里面的 readwrite_splitting_db 是我后面配好了读写分离,所以这里显示了,这个名字就是后面配置中的 schemaName

{% endnote %}

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

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

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

shell 复制代码
docker exec -it ss-proxy-a env LANG=C.UTF-8 /bin/bash
cd /opt/shardingsphere-proxy/logs
tail stdout.log 

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

shell 复制代码
-e ES_JAVA_OPTS="-Xmx256m -Xms256m -Xmn128m"

ShardingSphere-Proxy 读写分离

修改配置文件

修改配置 database-readwrite-splitting.yaml

{% note info %}

配置文件中给了两个示例,一个是 Postgresql 的,一个是 MySQL 的,改动后面的

{% endnote %}

yaml 复制代码
databaseName: readwrite_splitting_db

dataSources:
  write_ds:
    url: jdbc:mysql://host.docker.internal:3307/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://host.docker.internal:3308/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://host.docker.internal:3309/db_user?serverTimezone=UTC&useSSL=false
    username: root
    password: 123456
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    maxLifetimeMilliseconds: 1800000
    maxPoolSize: 50
    minPoolSize: 1

rules:
- !READWRITE_SPLITTING
  dataSourceGroups:
    readwrite_ds:
      writeDataSourceName: write_ds
      readDataSourceNames:
        - read_ds_0
        - read_ds_1
      transactionalReadQueryStrategy: PRIMARY
      loadBalancerName: random
  loadBalancers:
    random:
      type: RANDOM
- !SINGLE
  tables:
    - readwrite_ds.*
  defaultDataSource: readwrite_ds

{% note danger %}

一定要加上这个 - !SINGLE 下面这些内容,含义是 readwrite_ds 里的普通表都交给 ShardingSphere 管理。

{% endnote %}

和之前读写配置是一样的,就是多了个 databaseName,之后将配置文件上传至 /Users/ice/Desktop/cola/environment/ShardingSphere/proxy-a/conf 目录

重启容器

shell 复制代码
docker restart ss-proxy-a

实时查看日志

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

shell 复制代码
docker logs -f ss-proxy-a

远程访问测试

shell 复制代码
mysql -h127.0.0.1 -P3321 -uroot -p
sql 复制代码
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');

应用程序访问 Proxy

创建项目

sharding-proxy-demo

添加依赖

{% note success %}

把 ShardingSphere 完全当成 MySQL 来用就可以,我们实际连接的是 ShardingSphere,但是当成 MySQL 用就可以。

都不需要引入有关 ShardingSphere 有关的依赖

{% endnote %}

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>3.5.16</version>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </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>
    </dependency>
</dependencies>

创建实体类

java 复制代码
package com.lh.shardingproxydemo.entity;

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

创建 Mapper

java 复制代码
package com.lh.shardingproxydemo.mapper;

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

配置数据源

yaml 复制代码
spring:
  application:
    name: sharding-proxy-demo
  profiles:
    active: dev
  # 连接的实际上是 shardingsphere-proxy
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3321/readwrite_splitting_db?serverTimezone=UTC&useSSL=false
    username: root
    password: root
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

测试

java 复制代码
package com.lh.shardingproxydemo;

@SpringBootTest
class ShardingProxyDemoApplicationTests {

    @Autowired
    private UserMapper userMapper;

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

这里打印的就是逻辑 SQL 了,真实 SQL 还要去看日志文件。

{% note success %}

无感配置分库分表、读写分离,和原来程序编写没有任何区别

{% endnote %}

ShardingSphere-Proxy 垂直分片

修改配置文件

修改配置 database-sharding.yaml

yaml 复制代码
databaseName: sharding_db
#
dataSources:
  ds_0:
    url: jdbc:mysql://host.docker.internal: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://host.docker.internal: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

实时查看日志

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

shell 复制代码
docker logs -f ss-proxy-a

远程访问测试

shell 复制代码
mysql -h127.0.0.1 -P3321 -uroot -p
sql 复制代码
mysql> show databases;
mysql> use sharding_db;
mysql> show tables;
mysql> select * from t_order;
mysql> select * from t_user;

用 SpringBoot 连接和前面的一样,只不过把连接数据库的名字改成 sharding_db 就可以了

{% note info %}

我们把 database-readwrite-splitting.yamldatabase-sharding.yaml 都保留下来的话,就相当于有两个数据库,但是数据库一个只有读写分离,一个只有分片,所以如果你想同时用分片和读写分离,还是要都配置到一个文件(只要是 database- 开头的 yaml 文件就可以),用一个名字

并且要注意,如果你把这些配置文件保留下来了,那么里面所有涉及的数据源都必须全部正常运行,proxy 才能正常启动,所以如果某个数据库停用了或者某些配置你不用,最好先注释掉,不要影响其它配置的正常运行。

{% endnote %}

ShardingSphere-Proxy 水平分片

修改配置文件

修改配置 database-sharding.yaml

yaml 复制代码
databaseName: sharding_db

dataSources:
  ds_user:
    url: jdbc:mysql://host.docker.internal: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://host.docker.internal: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://host.docker.internal: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_inline_userid
      tableStrategy:
        standard:
          shardingColumn: order_no
          shardingAlgorithmName: alg_order_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_inline_userid
      tableStrategy:
        standard:
          shardingColumn: order_no
          shardingAlgorithmName: alg_order_item_hash_mod
      keyGenerateStrategy:
        column: id
        keyGeneratorName: snowflake
  bindingTables:
    - t_order, t_order_item
  shardingAlgorithms:
    alg_inline_userid:
      type: INLINE
      props:
        algorithm-expression: ds_order$->{user_id % 2}
    alg_order_hash_mod:
      type: INLINE
      props:
        algorithm-expression: t_order$->{Math.abs(order_no.hashCode()) % 2}
    alg_order_item_hash_mod:
      type: INLINE
      props:
        algorithm-expression: t_order_item$->{Math.abs(order_no.hashCode()) % 2}
  keyGenerators:
    snowflake:
      type: SNOWFLAKE

- !BROADCAST
  tables:
    - t_dict

实时查看日志

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

shell 复制代码
docker logs -f ss-proxy-a

远程访问测试

shell 复制代码
mysql -h127.0.0.1 -P3321 -uroot -p
sql 复制代码
mysql> show databases;
mysql> use sharding_db;
mysql> show tables;
mysql> select * from t_order; --测试水平分片
mysql> select * from t_dict; --测试广播表