ShardingSphere 入门教程之二

大纲

前言

学习资源

ShardingSphere 核心概念

接入端

Apache ShardingSphere 由 ShardingSphere-JDBC 和 ShardingSphere-Proxy 这两款既能够独立部署,又支持混合部署配合使用的产品组成。它们均提供标准化的基于数据库作为存储节点的增量功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。

ShardingSphere-JDBC

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

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

ShardingSphere-Proxy

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

  • 对应用程序完全透明,可直接当作 MySQL / PostgreSQL 使用;
  • 适用于任何兼容 MySQL / PostgreSQL 协议的客户端;
  • ShardingSphere-Proxy 的优势在于对异构语言的支持,以及为 DBA 提供可操作入口。

混合架构

  • ShardingSphere-JDBC 采用无中心化架构,与应用程序共享资源,适用于 Java 开发的高性能的轻量级 OLTP 应用;ShardingSphere-Proxy 提供静态入口以及异构语言的支持,独立于应用程序部署,适用于 OLAP 应用以及对分片数据库进行管理和运维的场景。

  • Apache ShardingSphere 是多接入端共同组成的生态圈。通过混合使用 ShardingSphere-JDBC 和 ShardingSphere-Proxy,并采用同一注册中心统一配置分片策略,能够灵活的搭建适用于各种场景的应用系统,使得架构师更加自由地调整适合于当前业务的最佳系统架构。

运行模式

  • 背景说明

    • Apache ShardingSphere 是一套完善的产品,使用场景非常广泛。 除生产环境的集群部署之外,还为工程师在开发和自动化测试等场景提供相应的运行模式。
    • Apache ShardingSphere 提供的 3 种运行模式分别是内存模式、单机模式和集群模式。
  • 内存模式

    • 初始化配置或执行 SQL 等造成的元数据结果变更的操作,仅在当前进程中生效。
    • 适用于集成测试的环境启动,方便开发人员在整合功能测试中集成 Apache ShardingSphere 而无需清理运行痕迹。
  • 单机模式

    • 能够将数据源和规则等元数据信息持久化,但无法将元数据同步至多个 Apache ShardingSphere 实例,无法在集群环境中相互感知。
    • 通过某一实例更新元数据之后,会导致其他实例由于获取不到最新的元数据而产生不一致的错误。
    • 适用于工程师在本地搭建 Apache ShardingSphere 环境。
  • 集群模式

    • 提供了多个 Apache ShardingSphere 实例之间的元数据共享和分布式场景下状态协调的能力。
    • 在真实部署上线的生产环境中,必须使用集群模式。它能够提供计算能力水平扩展和高可用等分布式系统必备的能力。
    • 集群环境需要通过独立部署的注册中心(比如 ZooKeeper、Etcd)来存储元数据和协调节点状态。

DistSQL

  • 背景

    • DistSQL(Distributed SQL)是 Apache ShardingSphere 特有的操作语言。
    • 它与标准 SQL 的使用方式完全一致,用于提供增量功能的 SQL 级别操作能力。
  • 挑战

    • 灵活的规则配置和资源管控能力是 Apache ShardingSphere 的特点之一。在使用 4.x 及其之前版本时,开发者虽然可以像使用原生数据库一样操作数据,但却需要通过本地文件或注册中心配置资源和规则。然而,操作习惯变更,对于运维工程师并不友好。
    • DistSQL 让用户可以像操作数据库一样操作 Apache ShardingSphere,使其从面向开发人员的框架和中间件转变为面向运维人员的数据库产品。
    • DistSQL 细分为 RDL、RQL 和 RAL 三种类型。
      • RDL(Resource & Rule Definition Language)负责资源和规则的创建、修改和删除;
      • RQL(Resource & Rule Query Language)负责资源和规则的查询和展现;
      • RAL(Resource & Rule Administration Language)负责 Hint、事务类型切换、分片执行计划查询等管理功能。
  • 目标

    • 打破中间件和数据库之间的界限,让开发者像使用数据库一样使用 Apache ShardingSphere,是 DistSQL 的设计目标。

可插拔架构

  • 背景

    • 在 Apache ShardingSphere 中,很多功能实现类的加载方式是通过 SPI(Service Provider Interface)注入的方式完成的。
    • SPI 是一种为了被第三方实现或扩展的 API,它可以用于实现框架扩展或组件替换。
  • 挑战

    • 可插拔架构对程序架构设计的要求非常高,需要将各个模块相互独立,互不感知,并且通过一个可插拔内核,以叠加的方式将各种功能组合使用。设计一套将功能开发完全隔离的架构体系,既可以最大限度的将开源社区的活力激发出来,也能够保障项目的质量。
    • Apache ShardingSphere 5.x 版本开始致力于可插拔架构,项目的功能组件能够灵活的以可插拔的方式进行扩展。目前,数据分片、读写分离、数据加密、影子库压测等功能,以及对 MySQL、PostgreSQL、SQLServer、Oracle 等 SQL 与协议的支持,均通过插件的方式织入项目。Apache ShardingSphere 目前已提供数十个 SPI 作为系统的扩展点,而且仍在不断增加中。
  • 目标

    • 让开发者能够像使用积木一样定制属于自己的独特系统,是 Apache ShardingSphere 可插拔架构的设计目标。
  • 实现

    • Apache ShardingSphere 的可插拔架构划分为三层,它们分别是:L1 内核层、L2 功能层、L3 生态层。
    • L1 内核层:
      • 是数据库基本能力的抽象,其所有组件均必须存在,但具体实现方式可通过可插拔的方式更换。
      • 主要包括查询优化器、分布式事务引擎、分布式执行引擎、权限引擎和调度引擎等。
    • L2 功能层:
      • 用于提供增量能力,其所有组件均是可选的,可以包含零至多个组件。组件之间完全隔离,互无感知,多组件可通过叠加的方式相互配合使用。
      • 主要包括数据分片、读写分离、数据加密、影子库等。用户自定义功能可完全面向 Apache ShardingSphere 定义的顶层接口进行定制化扩展,而无需改动内核代码。
    • L3 生态层:
      • 用于对接和融入现有数据库生态,包括数据库协议、SQL 解析器和存储适配器,分别对应于 Apache ShardingSphere 以数据库协议提供服务的方式、SQL 方言操作数据的方式以及对接存储节点的数据库类型。

ShardingSphere-JDBC 使用

读写分离使用案例

提示

本节将演示 SpringBoot + MyBatis Plus 如何整合 ShardingSphere-JDBC,并实现 读写分离,整体架构如下图所示。完整的案例代码可以直接从 GitHub 下载对应章节 sharding-sphere-lesson-01

准备工作

版本说明

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

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

本案例是在 MySQL 一主二从的架构上实现的,数据库的规划如下图所示:

数据库服务器主从角色 IP 端口
主服务器主库(master192.168.2.1913306
从服务器一从库(slave1192.168.2.1913307
从服务器二从库(slave2192.168.2.1913308

MySQL 主从同步环境搭建

MySQL 基于日志点实现主从复制的教程可以看 这里

数据库初始化
  • 在 MySQL 主库(master)中,执行以下 SQL 语句:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 创建数据库
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);

案例代码

添加依赖
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
<properties>
<spring-boot.version>2.7.18</spring-boot.version>
<shardingsphere.version>5.1.1</shardingsphere.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>
<!-- ShardingSphere-Jdbc -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>${shardingsphere.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>
创建实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@TableName("t_user")
@Data
public class User {

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

private String uname;

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

@Mapper
public interface UserMapper extends BaseMapper<User> {

}
配置读写分离
  • 创建配置文件(application.properties),配置读写分离
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
# 应用名称
spring.application.name=sharging-jdbc-demo
# 开发环境设置
spring.profiles.active=dev
# 模式配置(可选值:Memory、Standalone、Cluster)
spring.shardingsphere.mode.type=Memory

# 配置真实数据源(比如:一主二从)
spring.shardingsphere.datasource.names=master,slave1,slave2

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

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

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

# 读写分离类型(可选值:Static,Dynamic),其中 myds 是自定义的数据源名称
spring.shardingsphere.rules.readwrite-splitting.data-sources.myds.type=Static
# 写数据源的名称
spring.shardingsphere.rules.readwrite-splitting.data-sources.myds.props.write-data-source-name=master
# 读数据源的名称,多个从数据源用逗号分隔
spring.shardingsphere.rules.readwrite-splitting.data-sources.myds.props.read-data-source-names=slave1,slave2

# 负载均衡算法的名称,其中 myds 是自定义的数据源名称,alg_round 是自定义的负载均衡算法的名称
spring.shardingsphere.rules.readwrite-splitting.data-sources.myds.load-balancer-name=alg_round

# 负载均衡算法配置(这里配置三种负载均衡算法,底层都由 ShardingSphere-JDBC 提供实现),其中 alg_round、alg_random、alg_weight 都是自定义的负载均衡算法的名称
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_round.type=ROUND_ROBIN
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_random.type=RANDOM
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_weight.type=WEIGHT
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_weight.props.slave1=1
spring.shardingsphere.rules.readwrite-splitting.load-balancers.alg_weight.props.slave2=1

# 打印SQL语句
spring.shardingsphere.props.sql-show=true
  • ShardingSphere-JDBC 默认提供了以下三种负载均衡算法
负载均衡算法工作原理适用场景
ROUND_ROBIN(轮询)多个从库按顺序依次分配请求,循环使用从库性能相近,需要均衡分配请求
RANDOM(随机)每次随机选择一个从库处理请求请求量较小或对负载均衡精度要求不高
WEIGHT(加权)按配置的权重比例分配请求,需要额外配置权重(性能高的从库可设更大权重)从库性能不一致,需要根据性能灵活分配流量

测试代码

读写分离测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootTest
class ReadWriteTest {

@Autowired
private UserMapper userMapper;

/**
* 读写分离测试
*/
@Test
public void testInsert() {
User user = new User();
user.setUname("张三丰");
userMapper.insert(user);

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

}

测试代码运行后的输出结果如下:

1
2
3
4
5
6
2025-09-18 22:02:39.266  INFO 65403 --- [           main] ShardingSphere-SQL                       : Logic SQL: INSERT INTO t_user  ( uname )  VALUES  ( ? )
2025-09-18 22:02:39.267 INFO 65403 --- [ main] ShardingSphere-SQL : SQLStatement: MySQLInsertStatement(setAssignment=Optional.empty, onDuplicateKeyColumns=Optional.empty)
2025-09-18 22:02:39.268 INFO 65403 --- [ main] ShardingSphere-SQL : Actual SQL: master ::: INSERT INTO t_user ( uname ) VALUES ( ? ) ::: [张三丰]
2025-09-18 22:02:39.486 INFO 65403 --- [ main] ShardingSphere-SQL : Logic SQL: SELECT id,uname FROM t_user
2025-09-18 22:02:39.487 INFO 65403 --- [ main] ShardingSphere-SQL : SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
2025-09-18 22:02:39.487 INFO 65403 --- [ main] ShardingSphere-SQL : Actual SQL: slave1 ::: SELECT id,uname FROM t_user
事务一致性测试

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

  • 不添加 @Transactional 注解时:insert 语句对主库操作,select 语句对从库操作
  • 添加 @Transactional 注解时:insert 语句和 select 语句均对主库操作
  • 注意: 在 JUnit 环境下使用 @Transactional 注解,默认情况下会对事务进行回滚(即使在没有使用 @Rollback 注解的情况下,也会对事务进行回滚);如果不希望回滚事务,可以额外使用 @Rollback(false)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@SpringBootTest
class ReadWriteTest {

@Autowired
private UserMapper userMapper;

/**
* 事务一致性测试 <p>
* 当使用 @Transactional 事务注解后,事务内的所有 SQL 语句(包括读和写)都是在主库(master)执行
*/
@Test
@Transactional
public void testTransaction() {
User user = new User();
user.setUname("李思思");
userMapper.insert(user);

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

}

测试代码运行后的输出结果如下:

1
2
3
4
5
6
2022-09-18 22:08:56.139  INFO 62988 --- [           main] ShardingSphere-SQL                       : Logic SQL: INSERT INTO t_user  ( uname )  VALUES  ( ? )
2022-09-18 22:08:56.139 INFO 62988 --- [ main] ShardingSphere-SQL : SQLStatement: MySQLInsertStatement(setAssignment=Optional.empty, onDuplicateKeyColumns=Optional.empty)
2022-09-18 22:08:56.139 INFO 62988 --- [ main] ShardingSphere-SQL : Actual SQL: master ::: INSERT INTO t_user ( uname ) VALUES ( ? ) ::: [李思思]
2022-09-18 22:08:56.274 INFO 62988 --- [ main] ShardingSphere-SQL : Logic SQL: SELECT id,uname FROM t_user
2022-09-18 22:08:56.274 INFO 62988 --- [ main] ShardingSphere-SQL : SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
2022-09-18 22:08:56.274 INFO 62988 --- [ main] ShardingSphere-SQL : Actual SQL: master ::: SELECT id,uname FROM t_user
负载均衡测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringBootTest
class ReadWriteTest {

@Autowired
private UserMapper userMapper;

/**
* 负载均衡测试
*/
@Test
public void testLoadBalance() {
List<User> users1 = userMapper.selectList(null); // 采用轮询负载均衡算法时,执行第一次查询会路由到 slave1 库
List<User> users2 = userMapper.selectList(null); // 采用轮询负载均衡算法时,执行第二次查询会路由到 slave2 库
}

}
1
2
3
4
5
6
2022-09-18 22:24:08.781  INFO 66672 --- [           main] ShardingSphere-SQL                       : Logic SQL: SELECT  id,uname  FROM t_user
2022-09-18 22:24:08.781 INFO 66672 --- [ main] ShardingSphere-SQL : SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
2022-09-18 22:24:08.781 INFO 66672 --- [ main] ShardingSphere-SQL : Actual SQL: slave1 ::: SELECT id,uname FROM t_user
2022-09-18 22:24:08.853 INFO 66672 --- [ main] ShardingSphere-SQL : Logic SQL: SELECT id,uname FROM t_user
2022-09-18 22:24:08.853 INFO 66672 --- [ main] ShardingSphere-SQL : SQLStatement: MySQLSelectStatement(table=Optional.empty, limit=Optional.empty, lock=Optional.empty, window=Optional.empty)
2022-09-18 22:24:08.853 INFO 66672 --- [ main] ShardingSphere-SQL : Actual SQL: slave2 ::: SELECT id,uname FROM t_user