数据库开发标题-mysql 高可用
主从复制
主库配置
修改配置文件 /etc/my.cnf
[mysqld] # mysql 服务 ID,保证整个集群环境每个节点不重复,取值范围 1 ~ 2^32-1 server-id=1 # 1 只读,0 读写 read-only=0 #启用二进制日志;log-bin=binlog和下面配置等效,只是二进制日志文件的前缀不同,建议使用下面的配置 log-bin=mysql-bin # 不要复制的数据库(可设置多个) binlog-ignore-db=mysql binlog-ignore-db=information_schema binlog-ignore-db=performation_schema binlog-ignore-db=sys #设置需要复制的数据库 binlog-do-db=testmycat #设置 logbin 格式,有如下三种格式: # 默认值ROW,表示从机数据完全和主机一致;性能较差; # STATEMENT,日志记录的是 SQL 语句,而不是每一行的数据,减少了数据传输(如now()之类的函数,会导致数据不一致),性能较高; # MIXED,表示混合模式,有函数用ROW,无函数用STATEMENT,可以减少数据复制,但是数据不一致的风险更高。 binlog_format=STATEMENTsystemctl restart mysqld systemctl status mysqld # 查看是否重启成功提示
可以不用指定 binlog-ignore-db 和 binlog-do-db,让所有数据都被复制。
从库配置
修改配置文件 /etc/my.cnf
[mysqld] # mysql 服务 ID,保证整个集群环境每个节点不重复,取值范围 1 ~ 2^32-1 server-id=2 # 1 只读,0 读写 read-only=1 #启用中继日志 relay-log=mysql-relaysystemctl restart mysqld systemctl status mysqld # 查看是否重启成功在 Master 中建立帐户并授权 Slave
# 创建一个用户并分配主从复制权限 CREATE USER '用户名'@'%' IDENTIFIED BY '密码'; GRANT REPLICATION SLAVE ON *.* TO '用户名'@'%'; # 这一条命令和上面两条命令等效 GRANT REPLICATION SLAVE ON *.* TO '用户名'@'%' IDENTIFIED BY '密码';在 Slave 关联 Master
MySQL8+CHANGE REPLICATION SOURCE TO SOURCE_HOST='Master IP', SOURCE_USER='用户名', SOURCE_PASSWORD='密码', SOURCE_LOG_FILE='mysql-bin. 具体数字', SOURCE_LOG_POS=具体行位置; start replica; show replica status\G; # 下面两个参数都是 Yes,则说明主从配置成功! # Slave_IO_Running: Yes # Slave_SQL_Running: YesMySQL8 之前CHANGE MASTER TO MASTER_HOST='Master IP', MASTER_USER='用户名', MASTER_PASSWORD='密码', MASTER_LOG_FILE='mysql-bin. 具体数字', MASTER_LOG_POS=具体行位置; start slave; show slave status\G;提示
上面配置中的 LOG_FILE 和 LOG_POS 两项可通过如下指令在 Master 上查看:
# 查询 master 状态 show master status;若启动失败,可以在 Slave 中运行如下指令并重新执行上面的关联操作
stop slave; reset master;
分库分表
当单台数据库由于业务压力处于瓶颈状态,需要考虑对数据库进行分库分表操作,MySQL 单表数据达到 500 万 或单表容量超过 2GB 就会影响查询效率。
通过主从复制实现读写分离,对于应用程序而言,需要自己维护多个节点的访问地址,在读写的时候选择合适的数据库。分库分表同样面对着多节点如何访问的问题。
比较流行的分库分表策略是:将某一个库中的表拆分到不同数据库中(垂直分库),再将某张表的数据拆分到不同数据库(水平分表)。
以下是几种数据库的拆分策略:
| 拆分策略 | 描述 |
|---|---|
| 垂直分表 | 将表中字段拆分到其他数据库(或当前库),适合将某些不常用的或占用空间大的列拆分出去,拆分的表主键和原表保持一致 |
| 水平分表 | 将表中数据拆分到其他数据库(或当前库),适合表行数特别大的场景(通常为超过 500 万条) |
| 垂直分库 | 将库中的表拆分到其他数据库,每个库表结构都不一样,通过主键或外键关联 |
| 水平分库 | 将库中的多个表数据拆分到其他数据库,每个库表结构一样 |
Mycat
注意
Mycat 在 2022 年就已经停更,推荐 Java 程序使用 Apache ShardingSphere 作为分库分表解决方案
Mycat 帮助我们实现了读写分离、分库分表、主从故障切换等多种功能,对开发人员而言,他能屏蔽掉底层的繁杂操作,让我们不必关心程序和多个 MySQL 服务之间的通信问题,使用它就相当于只连接了一台 MySQL 一样。
安装 Mycat
目前 MyCat 官网下载地址用不了,直接 下载源码 ,在 mycat2\src\main\resources\datasources目录中修改数据库连接地址信息(这个数据源只是为了让 Mycat 能成功启动,具体怎么分库分表见下文),mvn 打包一下得到一个名为mycat2-xxx-release-jar-with-dependencies.jar的 jar 包。
打包
mvn clean package -DskipTests=true运行
配置文件位于源码
mycat2\src\main\resources目录中,将文件中的一坨配置复制到服务器/usr/local/mycat/resources目录中,并执行如下指令:dev# /usr/local/mycat/resources 为配置所在的路径 java -Dfile.encoding=UTF-8 \ -DMYCAT_HOME=/usr/local/mycat/resources \ -jar mycat2-1.22-jar-with-dependencies.jarprod# /usr/local/mycat/resources 为配置所在的路径 java -server -Xms10240m -Xmx10240m \ -XX:MaxMetaspaceSize=1024m -XX:MetaspaceSize=1024m \ -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 \ -XX:+CMSClassUnloadingEnabled -XX:+UseCMSInitiatingOccupancyOnly \ -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark \ -XX:+ExplicitGCInvokesConcurrent -XX:ErrorFile=/dump/hs_err.log \ -XX:OnError=/home/xxxxx/server/mycat/jvmOnError.sh \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/home/wq/server/mycat/dump.hprof \ -XX:OnOutOfMemoryError=/home/wq/server/mycat/dump.sh \ -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xdebug \ -Xrunjdwp:server=y,transport=dt_socket,address=35456,suspend=n \ -DMYCAT_HOME=/usr/local/mycat/resources \ -Dcom.sun.management.jmxremote \ -Dcom.sun.management.jmxremote.authenticate=false \ -Dcom.sun.management.jmxremote.ssl=false \ -Dcom.sun.management.jmxremote.port=6375 \ -Dlogback.configurationFile=/mnt/wq/server/mycat/resources/logback.xml \ -jar /home/xxxxx/mycat2-1.22-jar-with-dependencies.jar运行成功后,使用 navicat 连接,端口 8066,账号密码:root/123456
注意
MySQL8 需要给用户设置
XA_RECOVER_ADMIN权限(包括 root,用于管理分布式事务);可以为 Mycat 创建一个数据库用户,在配置中使用这个用户:# 在 MySQL 中创建用户并添加权限 CREATE USER '用户名'@'%' IDENTIFIED BY '密码'; GRANT XA_RECOVER_ADMIN ON *.* TO '用户名'@'%'; # MySQL8 必须要加的权限 (包括 root) GRANT ALL PRIVILEGES ON *.* TO '用户名'@'%'; # 按需添加其他权限其他安装
官网提供的制作 tar 包安装主要针对于不懂 Java 的小伙伴,使用 Java Service Wrapper 制作了一个壳子,使 Mycat 可以在 Linux/Windows 中运行,了解一下即可。
Mycat 概述
名词解释
名称 描述 逻辑库 Mycat 中定义的库,在物理 MySQL 中并不存在,多个物理 MySQL 数据库共同组成一个逻辑库 逻辑表 Mycat 中定义的表,多个物理 MySQL 数据库中的表共同组成一张逻辑表 拆分键 如订单表按用户 id 拆分,用户 id 就是拆分键 物理分表 多个物理分表的数据并集就是逻辑表 物理分库 参与数据分片的实际物理数据库 ER 表 主要针对父子关系的表,如订单表和订单详情表,将有关联的数据拆分到同一数据库
读写分离
Mycat 需要配合 MySQL 主从复制功能,实现读写分离。以下通过 一主一从 、双主双从演示读写分离的实现。
客户端通过连接 Mycat,由 Mycat 将读写请求转发对应物理 MySQL。
一主一从
通过完成以下配置,实现仅 Master 可写,Master 和 Slave 都可读的功能。
指定集群名称
在 Mycat 中创建一个名为 my-test-db-1 的逻辑库,打开 Mycat 配置文件目录,找到 schemas 文件夹,会发现一个名为
my-test-db-1.schema.json的配置文件,在配置中加入 targetName 指定集群名称,名字随便取,如下:{ "customTables": {}, "globalTables": {}, "normalProcedures": {}, "normalTables": {}, "schemaName": "my-test-db-1", "targetName": "my-test-01", "shardingTables": {}, "views": {} }添加数据源
可以手动在 datasources 目录中配置数据源,但比较繁琐。Mycat 支持通过注解添加数据源,只需执行 SQL 注释,Mycat 会在 datasources 目录中自动创建两个名为
myTestForXXX.datasource.json的配置。/*+ mycat:createDataSource{ "name":"myTestForWrite", "url":"jdbc:mysql://主机 ip:3306/my-test-db-1?useSSL=false&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true", "user":"root", "password":"root" } */; /*+ mycat:createDataSource{ "name":"myTestForRead", "url":"jdbc:mysql://从机 ip:3306/my-test-db-1?useSSL=false&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true", "user":"root", "password":"root" } */; # 查询数据源列表 /*+ mycat:showDataSources{} */;更新集群信息
Mycat 将若干个数据源关联起来,相当于构建了一个集群。如下为创建集群示例,需注意集群名称和上文中的 targetName 保持一致。执行成功后,Mycat 会在 clusters 目录中自动创建一个名为
my-test-01.cluster.json的配置。/*! mycat:createCluster{ "name":"my-test-01", "masters":["myTestForWrite"], "replicas":["myTestForRead"]} */; # 查看集群列表 /*+ mycat:showClusters{} */;
注意
配置完成后,需要重启 Mycat 使配置生效!
测试是否生效
主从复制中,可以使用内置函数(如时间函数)、系统变量(如 hostname)等使主从数据不一致,便于测试读写分离是否生效。连接 Mycat 并执行如下 SQL:
create table users ( id int not null, username varchar(50) not null, primary key (id) ); insert into user values(100,@@hostname); select * from user;
双主双从
MySQL 双主双从大致实现为:两个主节点互相为主从关系,每个主节点分别有一个从节点。MySQL 具体配置自行查阅资料,这里懒得写了。
假设已经存在了四台 MySQL 服务,masterA、masterB、slaveA、slaveB,且他们已经配置好双主双从关系。
在 Mycat 中完成如下配置,可实现在 masterA 中写,在 masterB、slaveA、slaveB 中读的功能,且当 masterA 服务不可用时,自动将 masterB 切换为可写状态,masterB 相当于一台备机。
指定集群名称
在 Mycat 中创建一个名为 my-test-db-2 的逻辑库,打开 Mycat 配置文件目录,找到 schemas 文件夹,会发现一个名为 my-test-db-2.schema.json 的配置文件,在配置中加入 targetName 指定集群名称,名字随便取,如下:
{ "customTables": {}, "globalTables": {}, "normalProcedures": {}, "normalTables": {}, "schemaName": "my-test-db-2", "targetName": "my-test-02", "shardingTables": {}, "views": {} }添加数据源
在 Mycat 中执行如下 SQL 注释:
/*+ mycat:createDataSource{ "name":"masterAForWrite", "url":"jdbc:mysql://masterA:3306/my-test-db-2?useSSL=false&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true", "user":"root", "password":"root" } */; /*+ mycat:createDataSource{ "name":"masterBForWrite", "url":"jdbc:mysql://masterB:3306/my-test-db-2?useSSL=false&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true", "user":"root", "password":"root" } */; /*+ mycat:createDataSource{ "name":"slaveAForRead", "url":"jdbc:mysql://slaveA:3306/my-test-db-2?useSSL=false&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true", "user":"root", "password":"root" } */; /*+ mycat:createDataSource{ "name":"slaveBForRead", "url":"jdbc:mysql://slaveB:3306/my-test-db-2?useSSL=false&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true", "user":"root", "password":"root" } */; # 查询数据源列表 /*+ mycat:showDataSources{} */;更新集群信息
/*! mycat:createCluster{ "name":"my-test-02", "masters":["masterAForWrite","masterBForWrite"], "replicas":["masterBForWrite","slaveAForRead","slaveBForRead"]} */; # 查看集群列表 /*+ mycat:showClusters{} */;完成配置后需要重启 Mycat!
分库分表
分库原则:有紧密关系(例如用到 join 查询)的表不应该被分离,表在不同的数据库里面不能做关联查询。在编写程序时尽量不要用 join 在数据库中联表查询,而是通过程序将数据关联。
分表原则:要根据具体业务场景决定怎么分表。例如:根据创建时间分片,不适合订单表,因为越早的订单被查询的频次越少,如果根据用户 id 去分,就比较合理。
假设已经存在了四台 MySQL 服务,其中 masterA 是 slaveA 的主机,其中 masterB 是 slaveB 的主机,即两组独立的主从关系数据库。
添加数据源
/*+ mycat:createDataSource{ "name":"masterAForWrite", "url":"jdbc:mysql://masterA:3306/my-test-db-2?useSSL=false&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true", "user":"root", "password":"root" } */; /*+ mycat:createDataSource{ "name":"slaveAForRead", "url":"jdbc:mysql://slaveA:3306/my-test-db-2?useSSL=false&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true", "user":"root", "password":"root" } */; /*+ mycat:createDataSource{ "name":"masterBForWrite", "url":"jdbc:mysql://masterB:3306/my-test-db-2?useSSL=false&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true", "user":"root", "password":"root" } */; /*+ mycat:createDataSource{ "name":"slaveBForRead", "url":"jdbc:mysql://slaveB:3306/my-test-db-2?useSSL=false&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true", "user":"root", "password":"root" } */; # 查询数据源列表 /*+ mycat:showDataSources{} */;配置集群
注意,如下配置采用的自动分片规则,自动分片默认要求集群名称以 c 为前缀,数字为后缀,如 c0 为第一个分片表,c1 为第二个。
/*! mycat:createCluster{ "name":"c0", "masters":["masterAForWrite"], "replicas":["slaveAForRead"]} */; /*! mycat:createCluster{ "name":"c1", "masters":["masterBForWrite"], "replicas":["slaveBForRead"]} */; # 查看集群列表 /*+ mycat:showClusters{} */;配置全局表
全局表(广播表)是所有数据库都会用到的通用的一张表,如数据字典、状态字典、省市字典表等,他们数据量不会很大,通常将他们保存到每个数据库中。
Mycat 中在建表语句末尾加上
broadcast关键字,就能指定这张表为全局表。在 Mycat 中新建一个名为 my-test-db-3 的数据库,并执行如下 sql:create table sys_dict ( id int not null, name varchar(50) not null, value int not null, primary key (id) ) broadcast; insert into sys_dict values(1,'男',1);执行完上面的 sql 后, 观察 schemas 文件夹中的
my-test-db-3.schema.json配置,Mycat 自动会添加上 targetName 属性(分别为上面配置的 c0、c1)。观察每台物理数据库,每个库中都新建了 my-test-db-3 数据库,且库中存在 sys_dict 这张表。
配置分片表
在 Mycat 中执行如下 sql ,将订单表根据用户 id 哈希取模进行分片:
create table order ( id int not null, user_id int not null, amount int not null, primary key (id) ) dbpartition by mod_hash(user_id) tbpartition by mod_hash(user_id) tbpartitions 1 dbpartitions 2; insert into order values(1,1,100); insert into order values(2,2,200); insert into order values(3,3,300); insert into order values(3,4,400);【dbpartition、tbpartition】 表示将数据库、表根据 user_id 分片;
【tbpartitions、 dbpartitions】 表示向 2 个数据库中分配 order 表的数据(因为有两个集群),且每个库有 1 个 order 表的分区。观察 MasterA 和 MasterB,他们各自新建了一张名为 my-test-db-3_0 和 my-test-db-3_1 的库,且库中分别有名为 order_0 和 order_1 两张表。观察表中数据,在 Mycat 插入的两条数据,每个库中各占 2 条。
配置 ER 表
对于两张有关联关系的表,如订单表和详情表,他们通过订单 id 相互关联。
在 Mycat1.6 版本中,需要把有关联的数据放在同一个库中,例如 A 库订单表包含了订单 id 为 1~100 的数据,则 A 库中的订单详情数据需要与订单对应。
Mycat2 版本中,支持有关联关系的数据放在不同库中,即 A 库订单表包含了订单 id 为 1~100 的数据,但 A 库中订单详情关联的订单 id 可能不在 1~100 内。
在 Mycat 中执行如下 sql,根据 order_id 将订单详情数据分片,2 个数据库分配 1 个表分区。
create table order_detail ( id int not null, detail varchar(100), order_id int not null, primary key (id) ) dbpartition by mod_hash(order_id) tbpartition by mod_hash(order_id) tbpartitions 1 dbpartitions 2; insert into order_detail values(1,'黑丝',1); insert into order_detail values(2,'红丝',1); insert into order_detail values(3,'橙丝',2); insert into order_detail values(4,'黄丝',3); insert into order_detail values(5,'绿丝',3); insert into order_detail values(6,'白丝',4);观察 MasterA 和 MasterB,在 my-test-db-3_0 和 my-test-db-3_1 库中,分别新建了名为 order_detail_0 和 order_detail_1 两张表。
再观察每个数据库中订单和详情表,他们可能没有对应关系,因为数据分散到了两个库中。在 Mycat 中执行如下 sql:
select * from order o left join order_detail d on o.id=d.order_id;令人惊奇的是,Mycat 居然能查出所有的数据项,原因是因为 Mycat 会分析 join 语句,自动去找到对应的库帮我们完成数据的组装,且有关联关系的表会被 Mycat 加入到同一个 ER 组中。
# 查看 ER 表 /*+ mycat:showErGroup{} */;
分片算法
上节中,配置数据分片使用了 mod_hash(字段名) 来指定分片规则,它表示根据指定字段值进行哈希取模运算,使用取模结果将数据分区到不同数据库。Mycat 支持 多种分区算法,实际上根据年月和取模用的最多。
按日期分片示例:在两个数据库中分配表数据,每个库有 12 个表分区。
create table xxx ...
dbpartition by YYYYDD(create_time) tbpartition by YYYYDD(create_time)
tbpartitions 12 dbpartitions 2;注意
创建表不要让主键自增,这是个非常 low 的做法,不但在数据迁移时会产生各种问题,而且在 Mycat 中,由于数据分区,id 自增会导致多个节点产生重复的 id。
ShardingSphere
Apache ShardingSphere 是一款分布式 SQL 事务和查询引擎,可通过数据分片、弹性伸缩、加密等能力对任意数据库进行增强。
Apache ShardingSphere 由 ShardingSphere-JDBC 和 ShardingSphere-Proxy 这 2 款既能够独立部署,又支持混合部署配合使用的产品组成。 它们均提供标准化的基于数据库作为存储节点的增量功能。
ShardingSphere-JDBC 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架,但是只能在 Java 语言中使用。
ShardingSphere-Proxy 定位为透明化的数据库代理端, 目前提供 MySQL 和 PostgreSQL 协议,可直接当做 MySQL/PostgreSQL 使用,和 Mycat 功能类似,任何语言都适用。
JDBC - 读写分离
如果程序为多实例部署(微服务 / 多副本)且希望配置中心化、动态下发,生产环境建议使用集群模式部署,在 ShardingSphere-JDBC 的集群模式下,可使用 ZooKeeper 或 Nacos 保存数据源、读写分离/分库分表等规则,并通过 ZooKeeper 或 Nacos 管理集群的动态拓扑和配置变更。
假设已经存在了三台 MySQL 服务,ds_master、ds_slave1、ds_slave2,且他们已经配置好一主双从关系,以下为使用 ShardingSphere-JDBC + Zookepper 配置读写分离的示例( ds_master 负责写,ds_slave1、ds_slave2 负责读)。
<dependencies>
<!-- ShardingSphere JDBC Spring Boot Starter -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>${shardingsphere.version}</version>
</dependency>
<!-- ZooKeeper 模式支持(可选) -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-mode-repository-zookeeper</artifactId>
<version>${shardingsphere.version}</version>
</dependency>
<!-- Nacos 模式支持(可选) -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-mode-repository-nacos</artifactId>
<version>${shardingsphere.version}</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>spring:
profiles:
active: dev
application:
name: sharding-jdbc-demo
shardingsphere:
props:
sql-show: true # 控制台打印 SQL,便于调试
mode:
type: Cluster # 集群模式(生产推荐);其他模式参考官网
repository:
type: ZooKeeper
props:
server-lists: 127.0.0.1:2181 # ZooKeeper 地址,可配多个
# namespace 可用于区分多个环境(开发、测试、生产)
namespace: shardingsphere-jdbc
datasource:
# 数据源名称
names: ds_master, ds_slave1, ds_slave2
ds_master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/db_master?serverTimezone=UTC&useSSL=false
username: root
password: root123
ds_slave1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3307/db_slave1?serverTimezone=UTC&useSSL=false
username: root
password: root123
ds_slave2:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3308/db_slave2?serverTimezone=UTC&useSSL=false
username: root
password: root123
rules:
readwrite-splitting:
data-sources:
# my_readwrite_ds 为自定义读写分离数据源数据源名称,有点类似逻辑库,应用程序无需关系哪个库该读或哪个库该写,这个由 ShardingSphere 帮助我们实现了
my_readwrite_ds:
# Static 类型表示静态数据源,即上面配置的几个数据源都是写死的。若需要动态加入或移除数据源,可使用 Dynamic 类型
type: Static
props:
write-data-source-name: ds_master
read-data-source-names: ds_slave1, ds_slave2
# 定义负载均衡算法名称(此处为轮询)
load-balancer-name: my_round_robin
load-balancers:
# ShardingSphere 提供了三种负载均衡算法类型;轮询 ROUND_ROBIN、随机 RANDOM、权重 WEIGHT
my_round_robin:
type: ROUND_ROBIN
my_random:
type: RANDOM
my_weight:
type: WEIGHT
props:
ds_slave1: 1.0
ds_slave2: 2.0@Slf4j
@ComponentScan(basePackages = "com.zjx")
@SpringBootTest
public class DemoUnitTest {
private final IUserService userService;
@Autowired
public ServiceTest(IUserService userService) {
this.userService = userService;
}
// 执行测试代码,观察日志输出,查看执行的三条SQL语句是否来着不同节点
@Test
@Transactional // 注意,使用 ShardingSphere-JDBC 只要加上了事务注解,读写都会走主库,因为避免了分布式事务的复杂性
public void testTest() {
User user = new User();
user.setName("zjx");
user.setAge(18);
userService.save(user); // 插入一条数据
userService.list(); // 查询
userService.list(); // 再次查询,用于在日志中观察两次查询是否来自不同节点
}
}提示
小细节!!!在单元测试中使用 @Transactional 注解,即使程序没有发生异常,也会自动回滚事务。
JDBC - 垂直分库
假设原有的单台 MySQL 已接近性能瓶颈,现在根据业务将用户和订单模块分别拆分到两台 MySQL 实例中,两个数据库分别命名为 ds_user 和 ds_order,且他们各自有若干张数据表。
垂直分库适用于优化单体应用,无法解决单表数据量过大的性能问题。
spring:
profiles:
active: dev
application:
name: sharding-jdbc-demo
shardingsphere:
props:
sql-show: true # 控制台打印 SQL,便于调试
mode:
type: Cluster # 集群模式(生产推荐);其他模式参考官网
repository:
type: ZooKeeper
props:
server-lists: 127.0.0.1:2181 # ZooKeeper 地址,可配多个
# namespace 可用于区分多个环境(开发、测试、生产)
namespace: shardingsphere-jdbc
datasource:
# 数据源名称
names: ds_user, ds_order
ds_user:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/user_db?serverTimezone=UTC&useSSL=false
username: root
password: root123
ds_order:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.2:3306/order_db?serverTimezone=UTC&useSSL=false
username: root
password: root123
rules:
sharding:
tables:
# 定义逻辑表和真实表的映射关系;
# 逻辑表 user 和 user_profile 会映射到 ds_user.user 和 ds_user.user_profile 两个真实表
# 逻辑表 t_order 和 t_order_item 会映射到 ds_order.t_order 和 ds_order.t_order_item 两个真实表
# 在实体类中,POJO 的表名映射和逻辑表名一致即可
user:
actual-data-nodes: ds_user.user
user_profile:
actual-data-nodes: ds_user.user_profile
# 注意,逻辑表名称需要避免使用关键字,例如 order
t_order:
actual-data-nodes: ds_order.t_order
t_order_item:
actual-data-nodes: ds_order.t_order_item@Slf4j
@ComponentScan(basePackages = "com.zjx")
@SpringBootTest
public class DemoUnitTest {
private final IUserService userService;
private final IOrderService orderService;
@Autowired
public ServiceTest(IUserService userService, IOrderService orderService) {
this.userService = userService;
this.orderService = orderService;
}
// 执行测试代码,观察日志输出,并查看两个数据源是否有数据插入
@Test
// 注意,垂直分库后,由于 user 和 order 使用的两个数据源不同,所以事务始终不会回滚
// 可以启用 ShardingSphere 的 XA 事务模式,保证事务的一致性,但是性能会下降(涉及两阶段提交),详见下文
@Transactional
public void testTest() {
User user = new User();
user.setName("zjx");
user.setAge(18);
userService.save(user); // 插入一条数据
Order order = new Order();
order.setUserId("001");
order.setOrderNo("123456");
orderService.save(order); // 插入一条数据
}
}在上文测试代码中提到事务无效问题,可通过启用 ShardingSphere 的 XA 事务解决
# 修改 application.yml 增加如下配置
spring:
shardingsphere:
# ...
props:
sql-show: true
transaction:
enabled: true
type: XA # 支持 XA 事务,也可以用 BASE(柔性事务)@SpringBootApplication
// 启动类添加 @EnableTransactionManagement
@EnableTransactionManagement
public class YourApp {
public static void main(String[] args) {
SpringApplication.run(YourApp.class, args);
}
}JDBC - 分库分表
将单表数据量拆分到不同表、不同库中(水平分库 + 水平分表),可以有效缓解单表数据量过大问题。
假设 t_order 表的数据量已经非常庞大了,需要其拆分为 6 张表,且用 2 个 MySQL 实例存储,每个实例存3张表(ds_0、 ds_1 两个数据源,他们都包含三张名为 t_order_0、t_order_1 和 t_order_2 三张表);分片规则为根据用户ID取模,示例配置如下:
提示
- 分库分表命名时,一定要使用数值作为后缀,例如 ds_0、ds_1、order_0、order_1 等,便于使用分片算法路由到对应的数据源。
- 建议分库分表时,分库策略和分表策略选取不同的字段,以避免冲突(如订单表,按 user_id 分库、 order_id 分表)。
- 分库分表时,解决多表联查常见做法是让这类存在强关联关系的表分配到同一个库中(种策略叫做绑定表策略,如订单表 t_order 与订单详情表 t_order_item,同一个用户的数据应存入相同库中)
- ShardingSphere 在任何情况下都不会自动创建不存在的数据库和数据表,所以必须先手动创建好表。
重要
ShardingSphere 最理想的分库分表策略,就是将同一个用户的相关数据,始终路由到同一个分片(同一个库 + 同一张表)!这是最理想、性能最优、查询设计最简单的方案,对联表、分页查询也非常友好。但是,理想终究是理想,如本例中(按 user_id 分库、 order_id 分表),同一个用户的订单虽然在同一个库中,但被分散到了多张表,这会直接导致查用户订单列表时,传统分页不再能生效(跨表无法分页),但它的优点也很显著,逻辑清晰、更大扩展量、更均衡的分布...
我们要在业务特性、访问模式、分片性能之间做权衡和选择。
ShardingSphere 这类分库分表系统中,传统的分页(例如 limit 10 offset 1000)方式已经不再推荐使用,原因不仅是技术限制,更涉及架构设计的权衡。更推荐的做法是游标式分页(基于 create_time 或 snowflake_id,他们很容易被排序,例如 create_time < '2025-08-01 12:00:00' order by create_time desc limit 10,ShardingSphere 内部会对每个表都执行这条语句,然后取并集,再排序返回最终的10条记录),前端以滚动加载方式代替分页。
spring:
profiles:
active: dev
application:
name: sharding-jdbc-demo
shardingsphere:
props:
sql-show: true # 控制台打印 SQL,便于调试
mode:
type: Cluster # 集群模式(生产推荐);其他模式参考官网
repository:
type: ZooKeeper
props:
server-lists: 127.0.0.1:2181 # ZooKeeper 地址,可配多个
# namespace 可用于区分多个环境(开发、测试、生产)
namespace: shardingsphere-jdbc
datasource:
names: ds_0, ds_1
ds_0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db_0?serverTimezone=UTC&useSSL=false
username: root
password: root123
ds_1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.2:3306/order_db_1?serverTimezone=UTC&useSSL=false
username: root
password: root123
rules:
sharding:
tables:
# 定义逻辑表和真实表的映射关系,t_order 表示逻辑表名称
# 注意,逻辑表名称需要避免使用关键字,例如 order
t_order:
# 使用行表达式定义各个真实表(共六个,可用行表达式写法)
# 以下配置等效于 actual-data-nodes: ds_0.t_order_0,ds_0.t_order_1,ds_0.t_order_2,ds_1.t_order_0,ds_1.t_order_1,ds_1.t_order_2
# 行表达式更多写法可参考官网
actual-data-nodes: ds_${0..1}.t_order_${0..2}
# 分库策略
database-strategy:
standard:
sharding-column: user_id # 根据 user_id 字段分库
sharding-algorithm-name: order-database-inline # 分库策略名称
# 分表策略
table-strategy:
standard:
sharding-column: user_id # 根据 user_id 字段分表
sharding-algorithm-name: order-table-inline # 分表策略名称
# 定义分片算法
sharding-algorithms:
order-database-inline:
# 分片算法类型,详见下文
type: INLINE
props:
# 取模运算,有几个库 % 后的值就是多少,保证数据均匀分布
# 如果用户 id 为 3,ds_${3 % 2} = ds_1, 他的订单数据会保存到 ds_1 库
algorithm-expression: ds_${user_id % 2}
order-table-inline:
type: INLINE
props:
# 取模运算,每个库有几张表 % 后的值就是多少,保证数据均匀分布
# 如果用户 id 为 3,t_order_${3 % 3} = t_order_0,他的订单数据会保存到 t_order_0 表
algorithm-expression: t_order_${user_id % 3}@Slf4j
@ComponentScan(basePackages = "com.zjx")
@SpringBootTest
public class DemoUnitTest {
private final IOrderService orderService;
@Autowired
public ServiceTest(IOrderService orderService) {
this.orderService = orderService;
}
// 执行测试代码,观察日志输出,并查看两个数据源是否有数据插入
@Test
// 和垂直分库类似,若不开启分布式事务功能,事务注解将失效;详见上节启用 ShardingSphere 的 XA 事务模式
@Transactional
public void testTest() {
for (int i = 0; i < 5; i++) {
Order order = new Order();
order.setUserId(i);
order.setOrderNo(RandomUtil.randomInt(100, 1000));
orderService.save(order);
}
}
}注意
真正的业务场景中,分库分表需要考虑到多表联查、分页查询、事务一致性问题等问题。必要时还需要给表添加一些冗余字段优化查询性能,具体要根据业务场景、数据特征来选择方案。
JDBC - 分片算法
ShardingSphere 内置提供了多种分片算法,具体使用场景可参考官网。
提示
分库策略(database-strategy)和分表策略(table-strategy)在 ShardingSphere 中是相互独立的,但它们在执行时会共同参与路由判断。在实际项目中,分库和分表通常会选择不同的字段进行分片(如订单表,按 user_id 分库、 order_id 分表),这样做不仅可以避免冲突,还可以带来很多其他方面的优势(比如逻辑更清晰等)。
| 类型 | 自动分片 | 是否常用 | 说明 |
|---|---|---|---|
| INLINE | × | 是 | 基于表达式分片,语法灵活 |
| MOD | √ | 是 | 取模分片算法,只适用于整数类型的分片键,适合分库或分表 |
| HASH_MOD | √ | 是 | 哈希后取模,适用于非整数类型(如字符串、UUID),分布更均衡 |
| BOUNDARY_RANGE | √ | 是 | 基于指定边界值的范围分片,适合固定范围明确的场景 |
| INTERVAL | × | 是 | 固定时间间隔分片,用于自然时间对齐的按年、按月、按日等固定时间单位分片的算法,它自动考虑闰年天数、不同月份天数 |
| AUTO_INTERVAL | √ | 较少 | 自动按时间间隔分片,不对齐自然时间,适合日志流或大数据采集 |
| VOLUME_RANGE | √ | 较少 | 基于分片容量(数量步长)的范围分片,适合单调递增主键场景 |
| COMPLEX_INLINE | × | 较少 | 支持多个列联合分片表达式,适用于复杂组合规则 |
| HINT_INLINE | × | 较少 | 结合 Hint 实现表达式分片,通常用于分片键无法参与 SQL 的场景 |
| CLASS_BASED | × | 否 | 自定义类实现复杂分片逻辑,灵活但复杂度高 |
以下为几种分片算法的示例配置,作为对上节分库分表内容的补充,更多配置及算法参数可参考官网。
# 哈希取模分片算法示例(对指定的字段,先做一次哈希,再取模)
# 将 t_order 表拆分到两个库,每个库三张表
spring:
shardingsphere:
# ...
rules:
sharding:
tables:
# 定义逻辑表和真实表的映射关系,t_order 表示逻辑表名称;
# 注意,逻辑表名称需要避免使用关键字,例如 order
t_order:
# 类举出各个真实表,实际的 6 张物理表,跨两个数据源
# 以下配置等效于 actual-data-nodes: ds_0.t_order_0,ds_0.t_order_1,ds_0.t_order_2,ds_1.t_order_0,ds_1.t_order_1,ds_1.t_order_2
actual-data-nodes: ds_${0..1}.t_order_${0..2}
# 分库策略
database-strategy:
standard:
sharding-column: user_id # 根据 user_id 字段分库
sharding-algorithm-name: order-database-hash-mod # 分库策略名称
# 分表策略
table-strategy:
standard:
sharding-column: order_id # 根据 order_id 字段分表
sharding-algorithm-name: order-table-hash-mod
sharding-algorithms:
order-database-hash-mod:
type: HASH_MOD
props:
# shardingsphere 会自动对 user_id 先做一次哈希,再取模,并根据取模结果对应到到 ds_0 或 ds_1
sharding-count: 2 # 分库数量,两个库(ds_0 和 ds_1)
order-table-hash-mod:
type: HASH_MOD
props:
# shardingsphere 会自动对 order_id 先做一次哈希,再取模,并根据取模结果对应到到 t_order_0、t_order_1 或 t_order_2
sharding-count: 3 # 分表数量,每个库 3 表(t_order_0、t_order_1、t_order_2)# 基于分片边界的范围分片算法
# 哈希分片 + 边界范围分片 联合策略(分库 + 分表)
spring:
shardingsphere:
# ...
rules:
sharding:
tables:
t_order:
# 每个库 3 张分表,共 2 库 6 张表
actual-data-nodes: ds_${0..1}.t_order_${0..2}
# 分库策略:根据 user_id 哈希取模
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: order-database-hash-mod
# 分表策略:根据 order_id 区间进行分表
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order-table-boundary-range
sharding-algorithms:
# 分库使用 HASH_MOD(两库)
order-database-hash-mod:
type: HASH_MOD
props:
sharding-count: 2 # ds_0, ds_1
# 分表使用 BOUNDARY_RANGE(每库三个分表)
order-table-boundary-range:
type: BOUNDARY_RANGE
props:
# 分表逻辑:
# t_order_0: order_id < 100000
# t_order_1: 100000 <= order_id < 200000
# t_order_2: order_id >= 200000
sharding-ranges: 100000,200000# 固定时间间隔分片
# 订单表按自然年分表,无需担心每年或每月的天数不一样,shardingsphere 会自动处理
spring:
shardingsphere:
# ...
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds_0.t_order_${2021..2025}
table-strategy:
standard:
sharding-column: create_time
sharding-algorithm-name: order-year-interval
sharding-algorithms:
order-year-interval:
type: INTERVAL
props:
datetime-pattern: yyyy-MM-dd HH:mm:ss # 要和时间字段格式一致
datetime-lower: 2021-01-01 00:00:00 # 起始时间
datetime-upper: 2026-01-01 00:00:00 # 截止时间(非包含)
sharding-suffix-pattern: yyyy # 真实表的后缀格式,必须遵循 Java DateTimeFormatter 的格式,如 yyyy
datetime-interval-amount: 1 # 分片键时间间隔
datetime-interval-unit: YEARS # 分片键时间间隔单位:年# 自动时间段分片算法示例(AUTO_INTERVAL),他是按固定时间间隔分区,使用场景较少
# 将 t_order 表按 create_time 以 1 天划分为多个分片
spring:
shardingsphere:
# ...
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds_0.t_order_${0..2} # 共分为 3 张分表
table-strategy:
standard:
sharding-column: create_time
sharding-algorithm-name: order-auto-interval-by-time
sharding-algorithms:
order-auto-interval-by-time:
type: AUTO_INTERVAL
props:
# 起始时间(包含),格式:yyyy-MM-dd HH:mm:ss
datetime-lower: 2024-01-01 00:00:00
# 截止时间(不包含)
datetime-upper: 2024-01-04 00:00:00
# 每张表的时间跨度(单位:秒)86400 秒 = 1 天
sharding-seconds: 86400# 分片容量的范围分片算法(VOLUME_RANGE)
# 按 order_id 范围 [0, 300000),每张表最多 100000 条记录,分为 3 张表
# 适合单调递增主键
spring:
shardingsphere:
# ...
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds_0.t_order_${0..2} # 3 张分表
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order-table-volume-range
sharding-algorithms:
order-table-volume-range:
type: VOLUME_RANGE
props:
# 分片 ID 最小值(包含,超过边界的数据会报错)
range-lower: 0
# 分片 ID 最大值(不包含,超过边界的数据会报错)
range-upper: 300000
# 每张表的容量(步长)
sharding-volume: 100000JDBC - 分布式主键自动生成
spring:
shardingsphere:
# ...
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds_0.t_order_${0..2}
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: order-table-mod
# 指定主键生成策略
key-generate-strategy:
# 指定 t_order 表中需要自动生成主键的字段名称
column: order_id
key-generator-name: my_snowflake-gen # 自定义主键生成算法名称
sharding-algorithms:
order-table-mod:
type: HASH_MOD
props:
sharding-count: 3
key-generators:
my_snowflake-gen:
# 按雪花算法生成主键
type: SNOWFLAKE # Snowflake 是长整型 (bigint),请确保你的表字段 order_id 是 BIGINT 或类似类型
props:
worker-id: 1 # 可选,建议每个实例配置不同的 ID,避免冲突@Data
@TableName("t_order")
public class Order {
// 注意,当没有配置 shardingsphere-jdbc 的主键生成策略时, IdType.AUTO 依赖数据库的主键自增策略
// 当配置了主键生成策略,IdType.AUTO 就依赖 sharding-jdbc 生成的主键 ID
@TableId(type = IdType.AUTO)
private Long order_id;
private Long user_id;
private BigDecimal amount;
private LocalDateTime create_time;
}JDBC - 多表联查(绑定表)
多表联查常见做法是让存在强关联关系的表分配到同一个库中,这种策略叫做绑定表策略。
一组强关联表要构成绑定表关系,必须让他们的分库策略、分表策略和分片键保持一致;以下是一个绑定表示例:
# 1. 订单和订单详情表,都按 user_id 分库(分片键、分库策略一致),保证同一个用户的数据都在同一个分片(库)中
# 2. 订单和订单详情表,都按 order_id 分表(分片键、分表策略一致),保证同一笔订单,订单和详情表后缀(index)相同
# 用户 id 为 0,订单 id 为 4,这个用户的数据会保存在 ds_0 库中,并只在 t_order_1 和 t_order_item_1 执行 join 查询
# 用户 id 为 0,订单 id 为 5,这个用户的数据会保存在 ds_0 库中,并只在 t_order_2 和 t_order_item_2 执行 join 查询
# 用户 id 为 1,订单 id 为 6,这个用户的数据会保存在 ds_1 库中,并只在 t_order_0 和 t_order_item_0 执行 join 查询
spring:
shardingsphere:
# ...
props:
sql-show: true # 打印SQL日志,方便调试
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds_${0..1}.t_order_${0..2}
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: db-inline
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: tbl-inline
t_order_item:
actual-data-nodes: ds_${0..1}.t_order_item_${0..2}
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: db-inline
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: tbl-inline
# 配置绑定表关系
binding-tables:
- t_order, t_order_item
sharding-algorithms:
db-inline:
type: INLINE
props:
algorithm-expression: ds_${user_id % 2}
tbl-inline:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 3}public interface OrderMapper extends BaseMapper<Order> {
// 查询订单总金额示例
// 注意,所有的查询都是相对于逻辑表的,SQL语句中不要用真实表
@Select({
"SELECT",
" o.order_id, SUM(i.price) AS total_price",
"FROM",
" t_order o",
"JOIN t_order_item i ON o.order_id = i.order_id",
"GROUP BY o.order_id"
})
List<OrderVo> selectOrderList();
}注意
- 单表查询时,若查询条件中不包含分片键,将导致所有库表全量扫描,对性能会有很大影响,应结合业务场景合理设计。
- 联表查询时,若不配置绑定表关系,则会进行笛卡尔积关联(如上述示例,因为有 2 个库 3 张表,订单表就 6 张,同理订单详情表也 6 张,会进行 36 次关联查询,最终取并集返回),严重影响性能,一旦配置了绑定表,只会在一对分片间执行一次 join。
- 联表查询且配置了绑定表时,若查询条件中不包含分片键,只会联查后缀相同的表,并取并集(如:(t_order_1 关联 t_order_item_1) U (t_order_2 关联 t_order_item_2) ... )。
JDBC - 广播表
广播表是指在每个数据库节点中都存在一份完整相同的数据表,适用于那些数据量小、读多写少、且所有分片节点都需要使用其数据的表,功能和 Mycat 类似,例如地区字典表、配置表、角色权限表等,都能作为广播表。广播表在分片查询时不会发生跨库访问,从而提升性能。
以下为一个广播表配置示例:
spring:
shardingsphere:
# ...
rules:
sharding:
tables:
# 逻辑表
t_order:
actual-data-nodes: ds_${0..1}.t_order_${0..1}
# ...
t_order_item:
actual-data-nodes: ds_${0..1}.t_order_item_${0..1}
# ...
# 广播表
broadcast-tables:
- sys_role
- sys_dict_type
- sys_dict_data
- sys_config
# 分片策略
sharding-algorithms:
# ...Proxy
ShardingSphere-Proxy 和 Mycat 的作用非常相似,其核心是像操作普通数据库一样连到 Proxy,由 Proxy 处理底层复杂的分库分表逻辑,相较于 ShardingSphere-JDBC,它能适配多种编程语言。
ShardingSphere-Proxy 需要单独安装,可使用使用二进制发布包、Docker 安装 等方式。
具体使用懒得写了,详情参考官网即可,身为 Java 程序员,ShardingSphere-JDBC 是使用最广泛、最主流的方式。
MySQL 高可用
对于 MySQL 主从复,它完全采用异步复制的策略,配置简单,但是存在一些风险,如主服务器故障可能丢失未复制的数据,且不具备故障转移能力。
MGR
MGR(MySQL Group Replication) 是 MySQL5.7 之后自带的全同步(阻塞)复制策略,将多个 MySQL 服务器组成一个逻辑组,组内每个节点既是主服务器也是从服务器,所有写操作在被提交前必须经过组内多数节点的共识,能保证每个节点的数据一致。自带故障转移功能,部署简单。但是它最多支持 9 个数据库节点。
ShardingSphere 对 MGR 集群有良好的支持,推荐结合使用,实现理海量数据的分库分表和读写分离功能。
MHA
MHA( MySQL Master High Availability ) 是小日子写的开源的半同步复制组件,它通过监控主从复制状态,自动检测主服务器故障,并在故障发生时自动执行主从切换,部署恶心,用着不爽。

