ShardingSphere-Proxy 入门教程之三

大纲

前言

学习资源

ShardingSphere-Proxy 使用

水平分库使用案例

提示

本节将演示 SpringBoot + MyBatis-Plus 如何基于 ShardingSphere-Proxy 实现 水平分库,即根据某个字段(或几个字段)通过特定规则,将数据分散到多个库或表中,每个分片只存储部分数据。例如,可按主键分片:偶数主键记录放入 0 库(或表),奇数主键记录放入 1 库(或表),如图所示水平分库通常是在水平分表的基础上进一步进行,将原本分散到多张表的数据继续分布到多台数据库服务器上,以提升整体的并发处理能力。完整的案例代码可以直接从 GitHub 下载对应章节 shardingsphere-proxy-lesson-03

准备工作

版本说明

本案例所使用的组件版本如下表所示:

组件版本说明
JDK11
MySQL8.0.29
SpringBoot2.7.18
MyBatis-Plus3.3.1
ShardingSphere-Proxy5.1.1
数据库规划

本案例是在两个 MySQL 数据库上实现的,数据库的规划如下图所示:

数据库服务器 IP 端口库的名称表的名称
订单数据库服务器一(server-order0192.168.2.1913310db_ordert_order0t_order1
订单数据库服务器二(server-order1192.168.2.1913311db_ordert_order0t_order1
ShardingSphere-Proxy 服务器 192.168.2.1913309sharding_db(逻辑库)t_user(逻辑表)、t_order(逻辑表)
数据库部署
  • 部署订单数据库服务器一(server-order0
1
2
3
4
5
6
7
8
9
10
# 创建并启动 MySQL 容器(Docker 会自动在宿主机上创建不存在的目录)
docker run -d \
-p 3310:3306 \
-v /data/server/order0/conf:/etc/mysql/conf.d \
-v /data/server/order0/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-e TZ=Asia/Shanghai \
--name server-order0 \
--restart always \
mysql:8.0.29
  • 更改订单数据库服务器一(server-order0)的默认密码校验方式
1
2
3
4
5
6
7
8
# 进入容器内,其中环境变量 "env LANG=C.UTF-8" 用于避免容器内显示中文乱码的问题
docker exec -it server-order0 env LANG=C.UTF-8 /bin/bash

# 在容器内执行 MySQL 命令行
mysql -uroot -p

# 修改 MySQL 超级管理员用户的默认密码校验方式
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';

  • 部署订单数据库服务器二(server-order1
1
2
3
4
5
6
7
8
9
10
# 创建并启动 MySQL 容器(Docker 会自动在宿主机上创建不存在的目录)
docker run -d \
-p 3311:3306 \
-v /data/server/order1/conf:/etc/mysql/conf.d \
-v /data/server/order1/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-e TZ=Asia/Shanghai \
--name server-order1 \
--restart always \
mysql:8.0.29
  • 更改订单数据库服务器二(server-order1)的默认密码校验方式
1
2
3
4
5
6
7
8
# 进入容器内,其中环境变量 "env LANG=C.UTF-8" 用于避免容器内显示中文乱码的问题
docker exec -it server-order1 env LANG=C.UTF-8 /bin/bash

# 在容器内执行 MySQL 命令行
mysql -uroot -p

# 修改 MySQL 超级管理员用户的默认密码校验方式
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
数据库初始化
  • 在订单数据库服务器一(server-order0)中,执行以下 SQL 语句:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 创建数据库
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)中,执行以下 SQL 语句:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 创建数据库
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)
);

ShardingSphere-Proxy 配置

水平分库配置

ShardingSphere-Proxy 支持两种配置模式,包括 YAML 和 DistSQL,任意选择一种即可。

特别注意

无论 ShardingSphere-Proxy 是使用 YAML 配置 还是 DistSQL 配置,都必须注意 ShardingSphere-Proxy 的版本差异。不同版本之间支持的 DistSQL 命令、YAML 配置语法以及参数格式可能存在较大差异,同一份配置在不同版本下可能无法直接执行或行为不一致,因此在编写和迁移配置时必须结合 ShardingSphere-Proxy 具体版本进行验证。

YAML 模式
  • 创建或编辑 ShardingSphere-Proxy 的 conf/config-sharding.yaml 配置文件,添加以下水平分库的配置内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 逻辑数据库(Schema)的名称
schemaName: sharding_db

# 配置数据源
dataSources:
# 配置第一个数据源
ds_order0:
url: jdbc:mysql://192.168.2.191: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.2.191: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_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: alg_snowflake

# 分片算法
shardingAlgorithms:
# 取模算法
alg_mod:
type: MOD
props:
sharding-count: 2
# 哈希取模算法
alg_hash_mod:
type: HASH_MOD
props:
sharding-count: 2

# 分布式序列算法
keyGenerators:
alg_snowflake:
type: SNOWFLAKE
props:
max-vibration-offset: 1
max-tolerate-time-difference-milliseconds: 10
  • ShardingSphere-Proxy 水平分库的配置内容添加完成后,需要重启 ShardingSphere-Proxy 服务(假设这里是通过 Docker 部署 ShardingSphere-Proxy 服务),否则配置不会生效
1
2
# 重启 Docker 容器
docker restart server-proxy
  • 验证 ShardingSphere-Proxy 中的配置规则是否生效(ShardingSphere-Proxy 启动时会从 YAML 配置文件中加载规则)
1
2
# 远程连接 ShardingSphere-Proxy
mysql -h192.168.2.191 -P3309 -uroot -p
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 切换到逻辑数据库(Schema)
USE sharding_db;

-- 查询所有数据源
SHOW SCHEMA RESOURCES;

-- 查询所有数据分片规则
SHOW SHARDING TABLE RULES;

-- 查询所有分片算法
SHOW SHARDING ALGORITHMS;

-- 查询所有分布式序列算法
SHOW SHARDING KEY GENERATORS;
DistSQL 模式
  • 创建逻辑数据库(Schema)
1
2
3
4
5
-- 创建逻辑数据库(Schema)
CREATE DATABASE sharding_db;

-- 切换到逻辑数据库(Schema)
USE sharding_db;
  • 添加数据源
1
2
3
4
5
6
7
8
9
10
11
12
13
-- 添加第一个数据源
ADD RESOURCE ds_order0 (
URL="jdbc:mysql://192.168.2.191:3310/db_order?serverTimezone=UTC&useSSL=false",
USER=root,
PASSWORD=123456,
PROPERTIES(
"connectionTimeoutMilliseconds"="30000",
"idleTimeoutMilliseconds"="60000",
"maxLifetimeMilliseconds"="1800000",
"maxPoolSize"="50",
"minPoolSize"="1"
)
);
1
2
3
4
5
6
7
8
9
10
11
12
13
-- 添加第二个数据源
ADD RESOURCE ds_order1 (
URL="jdbc:mysql://192.168.2.191:3311/db_order?serverTimezone=UTC&useSSL=false",
USER=root,
PASSWORD=123456,
PROPERTIES(
"connectionTimeoutMilliseconds"="30000",
"idleTimeoutMilliseconds"="60000",
"maxLifetimeMilliseconds"="1800000",
"maxPoolSize"="50",
"minPoolSize"="1"
)
);
  • 创建分片算法
1
2
3
4
-- 取模算法
CREATE SHARDING ALGORITHM alg_mod (
TYPE(NAME=MOD, PROPERTIES("sharding-count"="2"))
);
1
2
3
4
-- 哈希取模算法
CREATE SHARDING ALGORITHM alg_hash_mod (
TYPE(NAME=HASH_MOD, PROPERTIES("sharding-count"="2"))
);
  • 创建分布式序列算法
1
2
3
4
5
6
7
-- 雪花算法
CREATE SHARDING KEY GENERATOR alg_snowflake (
TYPE(NAME=SNOWFLAKE, PROPERTIES(
"max-vibration-offset"="1",
"max-tolerate-time-difference-milliseconds"="10"
))
);
  • 创建数据分片规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE SHARDING TABLE RULE t_order (
DATANODES("ds_order${0..1}.t_order${0..1}"),
DATABASE_STRATEGY(
TYPE=STANDARD,
SHARDING_COLUMN=user_id,
SHARDING_ALGORITHM=alg_mod
),
TABLE_STRATEGY(
TYPE=STANDARD,
SHARDING_COLUMN=order_no,
SHARDING_ALGORITHM=alg_hash_mod
),
KEY_GENERATE_STRATEGY(
COLUMN=id,
KEY_GENERATOR=alg_snowflake
)
);
  • 验证配置规则是否生效(使用 DistSQL 定义数据源和数据分片规则后,不需要重启 ShardingSphere-Proxy 服务,默认会自动生效)
1
2
3
4
5
6
7
8
9
10
11
-- 查询所有数据源
SHOW SCHEMA RESOURCES;

-- 查询所有数据分片规则
SHOW SHARDING TABLE RULES;

-- 查询所有分片算法
SHOW SHARDING ALGORITHMS;

-- 查询所有分布式序列算法
SHOW SHARDING KEY GENERATORS;
查看日志信息
  • 查看 ShardingSphere-Proxy 运行的实时日志信息
1
2
3
4
5
# 进入容器内,其中环境变量 "env LANG=C.UTF-8" 用于避免容器内显示中文乱码的问题
docker exec -it server-proxy env LANG=C.UTF-8 /bin/bash

# 查看日志信息
tail -f /opt/shardingsphere-proxy/logs/stdout.log
远程访问测试
  • 远程连接 ShardingSphere-Proxy
1
mysql -h192.168.2.191 -P3309 -uroot -p
  • 查看所有数据库
1
2
3
4
5
6
7
8
9
10
mysql> show databases;
+------------------------+
| schema_name |
+------------------------+
| sharding_db |
| mysql |
| information_schema |
| performance_schema |
| sys |
+------------------------+
  • 切换数据库
1
mysql> use sharding_db;
  • 查看所有表
1
2
3
4
5
6
mysql> show tables;
+-----------------------+------------+
| Tables_in_sharding_db | Table_type |
+-----------------------+------------+
| t_order | BASE TABLE |
+-----------------------+------------+
  • 查询用户数据
1
mysql> select * from t_order;

提示

执行完上述 select 操作后,可以在 ShardingSphere-Proxy 的日志信息中,查看对应的逻辑 SQL 和真实 SQL,以此判断水平分库是否生效。

SpringBoot 实战使用案例

添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<properties>
<spring-boot.version>2.7.18</spring-boot.version>
<mysql-connector.version>8.2.0</mysql-connector.version>
<mybatis-plus.version>3.3.1</mybatis-plus.version>
</properties>

<dependencyManagement>
<dependencies>
<!-- SpringBoot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Test -->
<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>
创建实体类
  • 订单实体类(特别注意:分库分表场景下不能使用数据库的自增主键,必须采用分布式全局唯一 ID,否则不同库不同表之间会发生主键冲突
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.math.BigDecimal;

@TableName("t_order") // 逻辑表名
@Data
public class Order {

/**
* MyBatis-Plus 的 ID 策略为 None <br>
* 当 ShardingSphere-Proxy 配置了分布式序列策略,会自动注入 ID
*/
@TableId(type = IdType.NONE)
private Long id;

private String orderNo;

private Long userId;

private BigDecimal amount;

}
创建 Mapper
  • 订单 Mapper
1
2
3
4
5
6
7
8
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.clay.shardingjdbc.entity.Order;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderMapper extends BaseMapper<Order> {

}
配置数据源
1
2
3
4
5
6
7
8
9
10
11
# 应用名称
spring.application.name=sharding-proxy-demo

# 配置数据源(这里连接的是 ShardingSphere-Proxy,而不是 MySQL)
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.2.191:3309/sharding_db?serverTimezone=GMT%2B8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root

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

测试代码

插入数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@SpringBootTest
class ShardingTest {

@Autowired
OrderMapper orderMapper;

/**
* 水平分库:插入数据测试
*/
@Test
public void testInsertOrder() {
for (int i = 1; i <= 5; i++) {
Order order = new Order();
order.setOrderNo("000" + i);
order.setUserId((long) i);
order.setAmount(new BigDecimal(100));
orderMapper.insert(order);
}
}

}

测试代码运行后,ShardingSphere-Proxy 运行输出的日志信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
23:29:19.412 [ShardingSphere-Command-8] INFO ShardingSphere-SQL - Logic SQL: INSERT INTO t_order ( id, order_no, user_id, amount ) VALUES ( 2000588989745934337, '0001', 1, 100 )
23:29:19.412 [ShardingSphere-Command-8] INFO ShardingSphere-SQL - SQLStatement: MySQLInsertStatement(setAssignment=Optional.empty, onDuplicateKeyColumns=Optional.empty)
23:29:19.412 [ShardingSphere-Command-8] INFO ShardingSphere-SQL - Actual SQL: ds_order1 ::: INSERT INTO t_order1 ( id, order_no, user_id, amount ) VALUES (2000588989745934337, '0001', 1, 100)

23:29:19.451 [ShardingSphere-Command-8] INFO ShardingSphere-SQL - Logic SQL: INSERT INTO t_order ( id, order_no, user_id, amount ) VALUES ( 2000588991763394562, '0002', 2, 100 )
23:29:19.451 [ShardingSphere-Command-8] INFO ShardingSphere-SQL - SQLStatement: MySQLInsertStatement(setAssignment=Optional.empty, onDuplicateKeyColumns=Optional.empty)
23:29:19.451 [ShardingSphere-Command-8] INFO ShardingSphere-SQL - Actual SQL: ds_order0 ::: INSERT INTO t_order0 ( id, order_no, user_id, amount ) VALUES (2000588991763394562, '0002', 2, 100)

23:29:19.470 [ShardingSphere-Command-9] INFO ShardingSphere-SQL - Logic SQL: INSERT INTO t_order ( id, order_no, user_id, amount ) VALUES ( 2000588991843086337, '0003', 3, 100 )
23:29:19.470 [ShardingSphere-Command-9] INFO ShardingSphere-SQL - SQLStatement: MySQLInsertStatement(setAssignment=Optional.empty, onDuplicateKeyColumns=Optional.empty)
23:29:19.470 [ShardingSphere-Command-9] INFO ShardingSphere-SQL - Actual SQL: ds_order1 ::: INSERT INTO t_order1 ( id, order_no, user_id, amount ) VALUES (2000588991843086337, '0003', 3, 100)

23:29:19.490 [ShardingSphere-Command-9] INFO ShardingSphere-SQL - Logic SQL: INSERT INTO t_order ( id, order_no, user_id, amount ) VALUES ( 2000588991926972418, '0004', 4, 100 )
23:29:19.490 [ShardingSphere-Command-9] INFO ShardingSphere-SQL - SQLStatement: MySQLInsertStatement(setAssignment=Optional.empty, onDuplicateKeyColumns=Optional.empty)
23:29:19.490 [ShardingSphere-Command-9] INFO ShardingSphere-SQL - Actual SQL: ds_order0 ::: INSERT INTO t_order0 ( id, order_no, user_id, amount ) VALUES (2000588991926972418, '0004', 4, 100)

23:29:19.514 [ShardingSphere-Command-9] INFO ShardingSphere-SQL - Logic SQL: INSERT INTO t_order ( id, order_no, user_id, amount ) VALUES ( 2000588992027635713, '0005', 5, 100 )
23:29:19.514 [ShardingSphere-Command-9] INFO ShardingSphere-SQL - SQLStatement: MySQLInsertStatement(setAssignment=Optional.empty, onDuplicateKeyColumns=Optional.empty)
23:29:19.514 [ShardingSphere-Command-9] INFO ShardingSphere-SQL - Actual SQL: ds_order1 ::: INSERT INTO t_order1 ( id, order_no, user_id, amount ) VALUES (2000588992027635713, '0005', 5, 100)
查询所有数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringBootTest
class ShardingTest {

@Autowired
OrderMapper orderMapper;

/**
* 水平分库:查询所有数据 <br>
* 查询了两个数据源,每个数据源中使用 UNION ALL 连接两个表
*/
@Test
public void testShardingSelectAll() {
List<Order> orders = orderMapper.selectList(null);
}

}

测试代码运行后,ShardingSphere-Proxy 运行输出的日志信息如下:

1
2
3
4
23:33:26.195 [ShardingSphere-Command-10] INFO ShardingSphere-SQL - Logic SQL: SELECT  id,order_no,user_id,amount  FROM t_order
23:33:26.195 [ShardingSphere-Command-10] INFO ShardingSphere-SQL - SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
23:33:26.195 [ShardingSphere-Command-10] INFO ShardingSphere-SQL - Actual SQL: ds_order0 ::: SELECT id,order_no,user_id,amount FROM t_order0 UNION ALL SELECT id,order_no,user_id,amount FROM t_order1
23:33:26.195 [ShardingSphere-Command-10] INFO ShardingSphere-SQL - Actual SQL: ds_order1 ::: SELECT id,order_no,user_id,amount FROM t_order0 UNION ALL SELECT id,order_no,user_id,amount FROM t_order1
根据条件查询数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootTest
class ShardingTest {

@Autowired
OrderMapper orderMapper;

/**
* 水平分库:根据 user_id(分片键)查询记录 <br>
* 查询了一个数据源,每个数据源中使用 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);
}

}

测试代码运行后,ShardingSphere-Proxy 运行输出的日志信息如下:

1
2
3
23:36:37.364 [ShardingSphere-Command-11] INFO ShardingSphere-SQL - Logic SQL: SELECT id,order_no,user_id,amount FROM t_order WHERE (user_id = 1)
23:36:37.364 [ShardingSphere-Command-11] INFO ShardingSphere-SQL - SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
23:36:37.364 [ShardingSphere-Command-11] INFO ShardingSphere-SQL - Actual SQL: ds_order1 ::: SELECT id,order_no,user_id,amount FROM t_order0 WHERE (user_id = 1) UNION ALL SELECT id,order_no,user_id,amount FROM t_order1 WHERE (user_id = 1)