ShardingSphere-JDBC 入门教程之三
大纲
- ShardingSphere-JDBC 入门教程之一、ShardingSphere-JDBC 入门教程之二、ShardingSphere-JDBC 入门教程之三
- ShardingSphere-JDBC 入门教程之四、ShardingSphere-JDBC 入门教程之五、ShardingSphere-JDBC 入门教程之六
- ShardingSphere-JDBC 入门教程之七、ShardingSphere-JDBC 入门教程之八、ShardingSphere-JDBC 入门教程之九
前言
学习资源
- ShardingSphere 官方项目(GitHub)
- ShardingSphere 官方网站(中文站点)
- ShardingSphere 官方文档(最新版本)
- ShardingSphere 官方文档(5.1.1 版本)
ShardingSphere 数据分片
背景说明
传统的将数据集中存储至单一节点的解决方案,在性能、可用性和运维成本这三方面已经难于满足海量数据的场景。
- 从性能方面来说,由于关系型数据库大多采用 B+ 树类型的索引,在数据量超过阈值的情况下,索引深度的增加也将使得磁盘访问的 I/O 次数增加,进而导致查询性能的下降;同时,高并发访问请求也使得集中式数据库成为系统的最大瓶颈。
- 从可用性的方面来说,服务化的无状态性,能够达到较小成本的随意扩容,这必然导致系统的最终压力都落在数据库之上。而单一的数据节点,或者简单的主从架构,已经越来越难以承担。数据库的可用性,已成为整个系统的关键。
- 从运维成本方面考虑,当一个数据库实例中的数据达到阈值以上,对于 DBA 的运维压力就会增大。数据备份和恢复的时间成本都将随着数据量的大小而愈发不可控。一般来讲,单一数据库实例的数据的阈值在 1TB 之内,是比较合理的范围。
在传统的关系型数据库无法满足互联网场景需要的情况下,将数据存储至原生支持分布式的 NoSQL 的尝试越来越多。但 NoSQL 对 SQL 的不兼容性以及生态圈的不完善,使得它们在与关系型数据库的博弈中始终无法完成致命一击,而关系型数据库的地位却依然不可撼动。
- 数据分片是指按照某个维度,将存放在单一数据库中的数据分散地存放至多个数据库或表中,以达到提升性能以及可用性的效果。
- 数据分片的有效手段是对关系型数据库进行分库和分表。分库和分表均可以有效的避免由数据量超过可承受阈值而产生的查询瓶颈。
- 数据分片的拆分方式又分为垂直分片和水平分片。
除此之外,分库还能够用于有效的分散对数据库单点的访问量;分表虽然无法缓解数据库压力,但却能够提供尽量将分布式事务转化为本地事务的可能,一旦涉及到跨库的更新操作,分布式事务往往会使问题变得复杂。
- 使用多主多从的分片方式,可以有效的避免数据单点,从而提升数据架构的可用性。
- 通过分库和分表进行数据的拆分来使得各个表的数据量保持在阈值以下,以及对流量进行疏导应对高访问量,是应对高并发和海量数据系统的有效手段。
核心概念
表
表是透明化数据分片的关键概念。Apache ShardingSphere 通过提供多样化的表类型,适配不同场景下的数据分片需求。
逻辑表:
- 相同结构的水平拆分数据库(表)的逻辑名称,是 SQL 中表的逻辑标识。例如:订单数据根据主键尾数拆分为 10 张表,分别是
t_order_0到t_order_9,它们的逻辑表名为t_order。
真实表:
- 在水平拆分的数据库中真实存在的物理表,即上个示例中的
t_order_0到t_order_9。
绑定表:
- 绑定表是指分片规则(包括分库规则和分表规则)完全一致的一组分片表。使用绑定表进行多表关联查询时,必须使用分片键进行关联,否则会出现笛卡尔积关联或跨库关联,从而严重影响查询效率。
- 例如:
t_order表和t_order_item表,均按照order_id进行分片(包括分库和分表),并且使用order_id进行关联,则此两张表互为绑定表关系。 - 绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
- 举例说明:
- 如果 SQL 为:
1
SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
- 在不配置绑定表关系时,假设分片键
order_id将数值 10 路由至第 0 片,将数值 11 路由至第 1 片,那么路由后的 SQL 应该为 4 条,它们呈现为笛卡尔积:1
2
3
4
5
6
7SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); - 在配置绑定表关系,并且使用
order_id进行关联后,路由的 SQL 应该为 2 条:1
2
3SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); - 其中
t_order表由于指定了分片条件,ShardingSphere 将会以它作为整个绑定表的主表。所有路由计算将会只使用主表的策略,那么t_order_item表的分片计算将会使用t_order的条件。
- 如果 SQL 为:
- 总结说明:
- 绑定表的核心目标是保证 ShardingSphere-JDBC 在执行多表关联查询时,能够计算出相同的路由目标,避免:
- 不必要的跨库 JOIN
- 笛卡尔积路由
- 扫描全部数据源
- 绑定表要求多张有关联的表在分库规则和分表规则上完全一致;否则 ShardingSphere-JDBC 无法在 JOIN 时计算出相同的路由,导致不能被视为绑定表。
- 绑定表的核心目标是保证 ShardingSphere-JDBC 在执行多表关联查询时,能够计算出相同的路由目标,避免:
广播表:
- 概述:
- 广播表是指所有的分片数据源中都存在的表,且表结构及其表数据(包括主键值)在每个数据库中均完全一致。
- 适用于数据量不大,且需要与海量数据的表进行关联查询(
JOIN)的场景,例如:字典表。
- 特性:
- 数据变更会同步到所有分片节点
- 对广播表执行
INSERT、UPDATE、DELETE操作时,ShardingSphere-JDBC 会将变更操作同时广播到所有数据源,确保各节点的数据完全一致。
- 对广播表执行
- 查询操作可在任意节点完成
- 广播表的数据在所有节点中一致,因此查询时可从任意一个数据源获取结果(通常基于负载均衡策略选择节点)。
- 可与任意表进行
JOIN- 广播表在所有节点都有一份完整数据,不涉及跨节点聚合,因此可以安全、方便地与任意分片表或非分片表进行
JOIN操作。
- 广播表在所有节点都有一份完整数据,不涉及跨节点聚合,因此可以安全、方便地与任意分片表或非分片表进行
- 数据变更会同步到所有分片节点
单表:
- 指所有的分片数据源中仅唯一存在的表。
- 适用于数据量不大,且无需分片的表。
数据节点
数据分片的最小单元,由数据源名称和真实表组成。例如:ds_0.t_order_0。逻辑表与真实表的映射关系,可分为均匀分布和自定义分布两种形式。
均匀分布:
- 指数据表在每个数据源内呈现均匀分布的态势,例如:
1
2
3
4
5
6db0
├── t_order0
└── t_order1
db1
├── t_order0
└── t_order1 - 数据节点的配置如下:
1
db0.t_order0, db0.t_order1, db1.t_order0, db1.t_order1
自定义分布:
- 指数据表呈现有特定规则的分布,例如:
1
2
3
4
5
6
7db0
├── t_order0
└── t_order1
db1
├── t_order2
├── t_order3
└── t_order4 - 数据节点的配置如下:
1
db0.t_order0, db0.t_order1, db1.t_order2, db1.t_order3, db1.t_order4
分片
分片键:
- 用于将数据库(表)水平拆分的表字段。
- 例如:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段。
- 如果 SQL 中无分片字段,将执行全路由,性能较差。
- 除了对单分片字段的支持,Apache ShardingSphere 也支持根据多个字段进行分片。
分片算法:
- 用于将数据分片的算法,支持
=、>=、<=、>、<、BETWEEN和IN进行分片。 - 分片算法可由开发者自行实现,也可使用 Apache ShardingSphere 内置的分片算法语法糖,灵活度非常高。
- 自动化分片算法
- 分片算法语法糖,用于便捷的托管所有数据节点,使用者无需关注真实表的物理分布。
- 默认提供取模、哈希取模、范围、时间等常用分片算法的实现。
- 自定义分片算法
- 提供接口让应用开发者自行实现与业务实现紧密相关的分片算法,并允许使用者自行管理真实表的物理分布。
- 自定义分片算法又分为:
- 标准分片算法:用于处理使用单一键作为分片键的
=、IN、BETWEEN AND、>、<、>=、<=进行分片的场景。 - 复合分片算法:用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。
- Hint 分片算法:用于处理使用 Hint 行分片的场景。
- 标准分片算法:用于处理使用单一键作为分片键的
分片策略:
- 分片策略包含了分片键和分片算法,由于分片算法的独立性,将其独立抽离。
- 真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。
强制分片路由:
- 对于分片字段并非由 SQL 而是其他外置条件决定的场景,可使用 SQL Hint 注入分片值。
- 例如:按照员工登录主键分库,而数据库中并无此字段。SQL Hint 支持通过 Java API 和 SQL 注释(待实现)两种方式使用。
行表达式
实现动机:
- 配置的简化与一体化是行表达式所希望解决的两个主要问题。
- 在繁琐的数据分片规则配置中,随着数据节点的增多,大量的重复配置使得配置本身不易被维护。通过行表达式可以有效地简化数据节点配置工作量。
- 对于常见的分片算法,使用 Java 代码实现并不有助于配置的统一管理。通过行表达式书写分片算法,可以有效地将规则配置一同存放,更加易于浏览与存储。
语法说明:
- 行表达式的使用非常直观,只需要在配置中使用
${ expression }或$->{ expression }标识行表达式即可。 - 目前支持数据节点和分片算法这两个部分的配置。行表达式的内容使用的是 Groovy 的语法,Groovy 能够支持的所有操作,行表达式均能够支持。例如:
${begin..end}:表示范围区间${[unit1, unit2, unit_x]}:表示枚举值
- 行表达式中如果出现连续多个
${ expression }或$->{ expression }表达式,整个表达式最终的结果将会根据每个子表达式的结果进行笛卡尔组合。- 例如,以下行表达式:
1
${['online', 'offline']}_table${1..3}
- 最终会解析为:
1
online_table1, online_table2, online_table3, offline_table1, offline_table2, offline_table3
- 例如,以下行表达式:
配置示例:
数据节点
- 对于均匀分布的数据节点
- 如果数据结构如下:
1
2
3
4
5
6db0
├── t_order0
└── t_order1
db1
├── t_order0
└── t_order1 - 用行表达式可以简化为:或者:
1
db${0..1}.t_order${0..1}
1
db$->{0..1}.t_order$->{0..1}
- 如果数据结构如下:
- 对于自定义的数据节点
- 如果数据结构如下:
1
2
3
4
5
6
7db0
├── t_order0
└── t_order1
db1
├── t_order2
├── t_order3
└── t_order4 - 用行表达式可以简化为:或者:
1
db0.t_order${0..1},db1.t_order${2..4}
1
db0.t_order$->{0..1},db1.t_order$->{2..4}
- 如果数据结构如下:
- 对于有前缀的数据节点,也可以通过行表达式灵活配置
- 如果数据结构如下:
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
44db0
├── t_order_00
├── t_order_01
├── t_order_02
├── t_order_03
├── t_order_04
├── t_order_05
├── t_order_06
├── t_order_07
├── t_order_08
├── t_order_09
├── t_order_10
├── t_order_11
├── t_order_12
├── t_order_13
├── t_order_14
├── t_order_15
├── t_order_16
├── t_order_17
├── t_order_18
├── t_order_19
└── t_order_20
db1
├── t_order_00
├── t_order_01
├── t_order_02
├── t_order_03
├── t_order_04
├── t_order_05
├── t_order_06
├── t_order_07
├── t_order_08
├── t_order_09
├── t_order_10
├── t_order_11
├── t_order_12
├── t_order_13
├── t_order_14
├── t_order_15
├── t_order_16
├── t_order_17
├── t_order_18
├── t_order_19
└── t_order_20 - 可以使用分开配置的方式,先配置包含前缀的数据节点,再配置不含前缀的数据节点,再利用行表达式笛卡尔积的特性,自动组合即可。上面的示例,用行表达式可以简化为:或者:
1
db${0..1}.t_order_0${0..9}, db${0..1}.t_order_${10..20}
1
db$->{0..1}.t_order_0$->{0..9}, db$->{0..1}.t_order_$->{10..20}
- 如果数据结构如下:
- 对于均匀分布的数据节点
分片算法:
- 对于只有一个分片键的使用
=和IN进行分片的 SQL,可以使用行表达式代替编码方式配置。 - 行表达式内部的表达式本质上是一段 Groovy 代码,可以根据分片键进行计算的方式,返回相应的真实数据源或真实表名称。
- 例如:拆分为 10 个库,尾数为 0 的路由到后缀为 0 的数据源,尾数为 1 的路由到后缀为 1 的数据源,以此类推。用于表示分片算法的行表达式为:或者:
1
ds${id % 10}
1
ds$->{id % 10}
- 对于只有一个分片键的使用
分布式主键
实现动机:
- 传统数据库软件开发中,主键自动生成技术是基本需求。而各个数据库对于该需求也提供了相应的支持,比如 MySQL 的自增键,Oracle 的自增序列等。数据分片后,不同数据节点生成全局唯一主键是非常棘手的问题。同一个逻辑表内的不同实际表之间的自增键由于无法互相感知而产生重复主键。虽然可通过约束自增主键初始值和步长的方式避免碰撞,但需引入额外的运维规则,使解决方案缺乏完整性和可扩展性。
- 目前有许多第三方解决方案可以完美解决这个问题,如 UUID 等依靠特定算法自生成不重复键,或者通过引入主键生成服务等。为了方便用户使用、满足不同用户不同使用场景的需求, Apache ShardingSphere 不仅提供了内置的分布式主键生成器,例如 UUID、SNOWFLAKE,还抽离出分布式主键生成器的接口,方便用户自行实现自定义的自增主键生成器。
内置的主键生成器:
- UUID:
- 采用
UUID.randomUUID()的方式生成分布式主键。
- 采用
- SNOWFLAKE:
- 算法介绍:
- 在分片规则配置模块可配置每个表的主键生成策略,默认使用雪花算法(Snowflake)生成 64bit 的长整型数据。
- 雪花算法是由 Twitter 公布的分布式主键生成算法,它能够保证不同进程主键的不重复性,以及相同进程主键的有序性。
- 雪花算法主键的详细结构见下图:
![]()
- 实现原理:
- 在同一个进程中,它首先是通过时间戳位保证不重复,如果时间相同则是通过序列位保证不重复;同时由于时间戳位是单调递增的,且各个服务器如果大体做了时间同步,那么生成的主键在分布式环境可以认为是总体有序的,这就保证了对索引字段的插入的高效性。例如 MySQL 的 Innodb 存储引擎的主键。
- 使用雪花算法生成的主键,二进制表示形式包含 4 部分,从高位到低位分表为:1bit 符号位、41bit 时间戳位、10bit 工作进程位以及 12bit 序列号位。
- 符号位(1bit):
- 预留的符号位,恒为零。
- 时间戳位(41bit):
- 41 位的时间戳可以容纳的毫秒数是 2 的 41 次幂,一年所使用的毫秒数是:
365 * 24 * 60 * 60 * 1000。 通过计算可知:1
Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);
- 计算结果约等于
69.73年。Apache ShardingSphere 的雪花算法的时间纪元从 2016 年 11 月 1 日 零点开始,可以使用到 2086 年,相信能满足绝大部分系统的要求。
- 41 位的时间戳可以容纳的毫秒数是 2 的 41 次幂,一年所使用的毫秒数是:
- 工作进程位(10bit):
- 该标志在 Java 进程内是唯一的,如果是分布式应用部署应保证每个工作进程的 id 是不同的。该值默认为 0,可通过属性设置。
- 通常前 5bit 表示数据中心 ID(0 ~ 31),后 5bit 表示工作机器 ID(0 ~ 31),可根据实际情况进行设定。
- 序列号位(12bit):
- 该序列是用来在同一个毫秒内生成不同的 ID。如果在这个毫秒内生成的数量超过 4096 (2 的 12 次幂),那么生成器会等待到下个毫秒再继续生成主键。
- 符号位(1bit):
- 时钟回拨问题:
- 服务器时钟回拨会导致产生重复主键,因此默认分布式主键生成器提供了一个最大容忍的时钟回拨毫秒数。
- 如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;如果在可容忍的范围内,默认分布式主键生成器会等待时钟同步到最后一次主键生成的时间后再继续工作。
- 最大容忍的时钟回拨毫秒数的默认值为 0,可通过属性设置。
- 算法介绍:
强制分片路由
实现动机:
- 通过解析 SQL 语句提取分片键列与值并进行分片是 Apache ShardingSphere 对 SQL 零侵入的实现方式。若 SQL 语句中没有分片条件,则无法进行分片,需要进行全路由。
- 在一些应用场景中,分片条件并不存在于 SQL,而存在于外部业务逻辑。因此需要提供一种通过外部指定分片结果的方式,在 Apache ShardingSphere 中叫做
Hint。
实现机制:
- Apache ShardingSphere 使用
ThreadLocal管理分片键值。可以通过编程的方式向HintManager中添加分片条件,该分片条件仅在当前线程内生效。 - 除了通过编程的方式使用强制分片路由,Apache ShardingSphere 还可以通过 SQL 中的特殊注释的方式引用
Hint,使开发者可以采用更加透明的方式使用该功能。 - 指定了强制分片路由的 SQL 将会无视原有的分片逻辑,直接路由至指定的真实数据节点。
使用规范
SQL 支持
分页查询
ShardingSphere-JDBC 使用
垂直分库使用案例
提示
本节将演示 SpringBoot + MyBatis Plus 如何整合 ShardingSphere-JDBC,并实现 垂直分库,即按照业务拆分数据库,核心理念是专库专用。在拆分之前,一个数据库由多个数据表构成,每个表对应着不同的业务。而拆分之后,则是按照业务将表进行归类,分布到不同的数据库中,从而将读写压力分散至不同的数据库(如下图所示)。完整的案例代码可以直接从 GitHub 下载对应章节 shardingsphere-jdbc-lesson-02。

准备工作
版本说明
本案例所使用的组件版本如下表所示:
| 组件 | 版本说明 |
|---|---|
| JDK | 11 |
| MySQL | 8.0.29 |
| SpringBoot | 2.7.18 |
| MyBatis-Plus | 3.3.1 |
| ShardingSphere-JDBC | 5.1.1 |
数据库规划
本案例是在两个 MySQL 数据库上实现的,数据库的规划如下图所示:

| 数据库服务器 | IP | 端口 | 库的名称 | 表的名称 |
|---|---|---|---|---|
用户数据库服务器(server-user) | 192.168.2.191 | 3301 | db_user | t_user |
订单数据库服务器(server-order) | 192.168.2.191 | 3302 | db_order | t_order |
数据库部署
- 部署用户数据库服务器(
server-user)
1 | # 创建并启动 MySQL 容器(Docker 会自动在宿主机上创建不存在的目录) |
- 更改用户数据库服务器(
server-user)的默认密码校验方式
1 | # 进入容器内,其中环境变量 "env LANG=C.UTF-8" 用于避免容器内显示中文乱码的问题 |
- 部署订单数据库服务器(
server-order)
1 | # 创建并启动 MySQL 容器(Docker 会自动在宿主机上创建不存在的目录) |
- 更改订单数据库服务器(
server-order)的默认密码校验方式
1 | # 进入容器内,其中环境变量 "env LANG=C.UTF-8" 用于避免容器内显示中文乱码的问题 |
数据库初始化
- 在用户数据库服务器(
server-user)中,执行以下 SQL 语句:
1 | -- 创建数据库 |
- 在订单数据库服务器(
server-order)中,执行以下 SQL 语句:
1 | -- 创建数据库 |
案例代码
添加依赖
1 | <properties> |
创建实体类
- 用户实体类
1 | import com.baomidou.mybatisplus.annotation.IdType; |
- 订单实体类
1 | import com.baomidou.mybatisplus.annotation.IdType; |
创建 Mapper
- 用户 Mapper
1 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
- 订单 Mapper
1 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
配置数据分片
- 创建配置文件(
application.properties),配置数据分片
1 | # 应用名称 |
测试代码
插入数据
1 |
|
测试代码运行后的输出结果如下:
1 | 2022-09-05 20:43:18.761 INFO 54451 --- [ main] ShardingSphere-SQL : Logic SQL: INSERT INTO t_user ( uname ) VALUES ( ? ) |
查询数据
1 |
|
测试代码运行后的输出结果如下:
1 | 2022-09-05 20:45:02.718 INFO 54896 --- [ main] ShardingSphere-SQL : Logic SQL: SELECT id,uname FROM t_user WHERE id=? |

