分布式专题——10.2 ShardingSphere-JDBC分库分表实战与讲解
1 分库分表案例
-
下面实现一个分库分表案例,将一批课程信息分别拆分到两个库,四个表中:
-
需提前准备一个 MySQL 数据库,并在其中创建 Course 表。Course 表的建表语句如下:
CREATE TABLE course (`cid` bigint(0) NOT NULL,`cname` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`user_id` bigint(0) NOT NULL,`cstatus` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,PRIMARY KEY (`cid`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
1.1 搭建基础 JDBC 应用
-
接下来使用 SpringBoot+MyBatisPlus 快速搭建一个可以访问数据库的简单应用,以这个应用作为后续分库分表的基础;
-
搭建一个 Maven 项目,在
pom.xml
中加入相关依赖,其中就包含访问数据库最为简单的几个组件:<dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.7.18</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.33</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.7</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.20</version></dependency></dependencies> </dependencyManagement>
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope></dependency> </dependencies>
-
使用 MyBatisPlus 的方式,直接声明 Entity 和 Mapper,映射数据库中的 course 表:
public class Course {private Long cid;private String cname;private Long userId;private String cstatus;//省略。getter ... setter .... }
public interface CourseMapper extends BaseMapper<Course> { }
-
增加 SpringBoot 启动类,扫描 mapper 接口:
@SpringBootApplication @MapperScan("com.tl.jdbcdemo.mapper") public class App {public static void main(String[] args) {SpringApplication.run(App.class,args);} }
-
在 SpringBoot 的配置文件
application.properties
中增加数据库配置:spring.datasource.druid.db-type=mysql spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.druid.url=jdbc:mysql://192.168.65.212:3306/test?serverTimezone=UTC spring.datasource.druid.username=root spring.datasource.druid.password=root
-
做一个单元测试:把course课程信息插入到数据库,然后从数据库中进行查询
@SpringBootTest @RunWith(SpringRunner.class) public class JDBCTest {@Resourceprivate CourseMapper courseMapper;@Testpublic void addcourse() {for (int i = 0; i < 10; i++) {Course c = new Course();c.setCname("java");c.setUserId(1001L);c.setCstatus("1");courseMapper.insert(c);//insert into course values ....System.out.println(c);}}@Testpublic void queryCourse() {QueryWrapper<Course> wrapper = new QueryWrapper<Course>();wrapper.eq("cid",1L);List<Course> courses = courseMapper.selectList(wrapper);courses.forEach(course -> System.out.println(course));} }
1.2 引入 ShardingSphere-JDBC 快速实现分库分表
-
另起了一个新模块来讲解,除了下面的依赖,其余的内容,比如实体类、mapper接口等,与上面
1.1 搭建基础 JDBC 应用
中讲解的一致; -
在
pom.xml
中引入ShardingSphere
:<dependencies><!-- ShardingSphere-JDBC核心依赖 --><dependency><groupId>org.apache.shardingsphere</groupId><artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId><version>5.2.1</version><exclusions><exclusion><artifactId>snakeyaml</artifactId><groupId>org.yaml</groupId></exclusion></exclusions></dependency><!-- 版本冲突 --><dependency><groupId>org.yaml</groupId><artifactId>snakeyaml</artifactId><version>1.33</version></dependency><!-- SpringBoot依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><exclusions><exclusion><artifactId>snakeyaml</artifactId><groupId>org.yaml</groupId></exclusion></exclusions></dependency><!-- 数据源连接池 --><!--注意不要用这个依赖,他会创建数据源,跟上面ShardingSphere-JDBC的SpringBoot集成依赖有冲突 --><!-- <dependency>--><!-- <groupId>com.alibaba</groupId>--><!-- <artifactId>druid-spring-boot-starter</artifactId>--><!-- <version>1.1.20</version>--><!-- </dependency>--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.20</version></dependency><!-- mysql连接驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!-- mybatisplus依赖 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope></dependency> </dependencies>
-
按照设计,要创建
m0
和m1
数据库,在其中分别创建course_1
和course_2
表,他们的表结构与course
表是一致的; -
增加 ShardingSphere-JDBC 的分库分表配置:
# 启用SQL日志打印,在控制台输出实际执行的SQL语句,便于调试和监控 spring.shardingsphere.props.sql-show = true # 允许Bean定义覆盖,解决多个数据源配置时可能出现的Bean冲突问题 spring.main.allow-bean-definition-overriding = true# 定义数据源名称列表,此处配置了两个数据源:m0和m1 spring.shardingsphere.datasource.names=m0,m1# 配置第一个数据源m0的连接参数 spring.shardingsphere.datasource.m0.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.m0.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.m0.url=jdbc:mysql://192.168.65.212:3306/shardingdb1?serverTimezone=UTC spring.shardingsphere.datasource.m0.username=root spring.shardingsphere.datasource.m0.password=root# 配置第二个数据源m1的连接参数 spring.shardingsphere.datasource.m1.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.m1.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.m1.url=jdbc:mysql://192.168.65.212:3306/shardingdb2?serverTimezone=UTC spring.shardingsphere.datasource.m1.username=root spring.shardingsphere.datasource.m1.password=root# 配置分布式主键生成器(雪花算法) spring.shardingsphere.rules.sharding.key-generators.alg_snowflake.type=SNOWFLAKE # 设置雪花算法的worker-id,用于分布式环境下避免ID冲突 spring.shardingsphere.rules.sharding.key-generators.alg_snowflake.props.worker-id=1# 为course表配置主键生成策略:使用cid字段作为主键,并采用雪花算法生成 spring.shardingsphere.rules.sharding.tables.course.key-generate-strategy.column=cid spring.shardingsphere.rules.sharding.tables.course.key-generate-strategy.key-generator-name=alg_snowflake# 配置course表的实际数据节点:分布在m0和m1两个数据库中,每个库有course_1和course_2两个表 spring.shardingsphere.rules.sharding.tables.course.actual-data-nodes=m$->{0..1}.course_$->{1..2}# 配置分库策略:采用标准分片策略,使用cid字段作为分片键 spring.shardingsphere.rules.sharding.tables.course.database-strategy.standard.sharding-column=cid # 指定分库算法名称为course_db_alg spring.shardingsphere.rules.sharding.tables.course.database-strategy.standard.sharding-algorithm-name=course_db_alg# 配置分库算法:使用MOD取模算法,分成2个库 spring.shardingsphere.rules.sharding.sharding-algorithms.course_db_alg.type=MOD spring.shardingsphere.rules.sharding.sharding-algorithms.course_db_alg.props.sharding-count=2# 配置分表策略:采用标准分片策略,使用cid字段作为分片键 spring.shardingsphere.rules.sharding.tables.course.table-strategy.standard.sharding-column=cid # 指定分表算法名称为course_tbl_alg spring.shardingsphere.rules.sharding.tables.course.table-strategy.standard.sharding-algorithm-name=course_tbl_alg# 配置分表算法:使用INLINE表达式分表算法 spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.type=INLINE # 分表表达式:根据cid取模2再加1,结果将为1或2,对应course_1和course_2表 spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.props.algorithm-expression=course_$->{cid%2+1}# 当前配置使用雪花算法生成cid,但雪花算法生成的ID不是严格递增的 # 如果需要更均匀的数据分布,可以改用自定义的MYSNOWFLAKE算法并调整分表表达式,((cid+1)%4).intdiv(2)+1 可以将数据更均匀分布到4个分片中 #spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.props.algorithm-expression=course_$->{((cid+1)%4).intdiv(2)+1}
上面配置中用到了 Groovy 表达式,比如
m$->{0..1}.course_$->{1..2}
、course_$->{cid % 2 + 1}
数据节点表达式:
m$->{0..1}.course_$->{1..2}
,这个表达式定义了物理表在数据库中的实际分布位置,即“数据节点”;-
结构解析:
-
m
和course_
是静态的字符串前缀; -
$->{0..1}
是一个Groovy范围表达式,动态生成一个序列; -
中间的
.
是字面意义上的点,用于分隔数据库名和表名;
-
-
Groovy范围表达式
{0..1}
:- 在Groovy中,
{0..1}
表示生成一个从0开始到1结束的整数序列,即[0, 1]
; - 同理,
{1..2}
生成的序列是[1, 2]
;
- 在Groovy中,
-
ShardingSphere会解析这个表达式,进行笛卡尔积计算,生成所有可能的数据节点组合。最终,这个表达式解析出的物理表有:
m0.course_1
(数据库m0
中的表course_1
)m0.course_2
(数据库m0
中的表course_2
)m1.course_1
(数据库m1
中的表course_1
)m1.course_2
(数据库m1
中的表course_2
)
总结:这条配置告诉ShardingSphere,逻辑表
course
的数据实际上被分散存储在 2个数据库(m0, m1) 且每个数据库中有 2张表(course_1, course_2),总共 4个物理分片 中;分片算法表达式:
course_$->{cid % 2 + 1}
,这个表达式是一个分片策略算法,它定义了如何根据某个字段的值(分片键)来计算出一条数据应该被路由到哪个具体的物理表;-
结构解析:
course_
是静态的字符串前缀,是目标表名的前半部分;$->{cid % 2 + 1}
是一个Groovy算法表达式,会根据每行数据的cid
值进行动态计算,得出表名的后缀部分;
-
Groovy算法表达式
{cid % 2 + 1}
:cid
:这是我们的数据中的分片键字段的值(例如:1001
,1002
);%
:是取模(求余数)运算符。cid % 2
的意思是计算cid
的值除以2
后的余数。结果只能是0
或1
;+ 1
:将取模的结果加1
;
-
代入不同的
cid
值来计算一下:- 如果
cid = 4
:4 % 2 = 0
->0 + 1 = 1
-> 最终表名为course_1
- 如果
cid = 7
:7 % 2 = 1
->1 + 1 = 2
-> 最终表名为course_2
- 如果
cid = 10
:10 % 2 = 0
->0 + 1 = 1
-> 最终表名为course_1
- 如果
cid = 15
:15 % 2 = 1
->1 + 1 = 2
-> 最终表名为course_2
- 如果
总结:这条配置定义了一个简单的取模分表算法。它根据主键
cid
的奇偶性来决定数据存放在course_1
(cid
为偶数) 还是course_2
(cid
为奇数) 表中。+1
的操作是为了让结果从(0,1)
映射到(1,2)
,以匹配我们之前定义的物理表名course_1
和course_2
; -
-
执行同
1.1 搭建基础 JDBC 应用
一样的测试案例,就可以看到:执行addcourse
方法时,十条课程信息会根据cid
(课程 ID)的奇偶性,被拆分到m0.course_1
和m1.course_2
两张表中。从日志能看到实际执行的 SQL 语句,比如向course_2
或course_1
插入数据;根据配置文件,十条课程信息按
cid
奇偶性拆分到m0.course_1
和m1.course_2
的全过程如下:-
系统使用雪花算法生成
cid
(主键),配置了两个数据源:m0
(对应数据库shardingdb1
)和m1
(对应数据库shardingdb2
),每个数据源中存在course_1
和course_2
两张物理表; -
分库决策(确定存储到 m0 还是 m1):当系统执行添加课程操作时,会按以下步骤决定数据存储位置:
-
生成
cid
,雪花算法自动生成cid
值(分布式唯一ID),假设生成的值为123456
-
根据分库算法:
MOD
(取模算法),配置sharding-count=2
; -
计算逻辑:
cid % 2
。若结果为0
:数据分配到m0
数据源;若结果为1
:数据分配到m1
数据源; -
示例:
123456 % 2 = 0
→ 选择m0
数据源;
-
-
分表决策(确定存储到course_1还是course_2)
-
根据分表算法:
INLINE
表达式,配置course_$->{cid%2+1}
; -
计算逻辑:若
cid
为偶数(cid%2=0
):0+1=1
→ 选择course_1
表;若cid
为奇数(cid%2=1
):1+1=2
→ 选择course_2
表; -
示例:
123456 % 2 + 1 = 1
→ 选择course_1
表;
-
-
最后这条
cid
为123456
的数据最终存储到m0.course_1
表。由于雪花算法生成的ID奇偶性大致各占一半,最终分布为:-
约5条偶数
cid
数据 → 存储到m0.course_1
-
约5条奇数
cid
数据 → 存储到m1.course_2
-
-
-
现在想把数据均匀分配到四张表,但目前只能分到两张表。若要实现,需从数据(ID 生成规则)和分片算法两方面调整;
- 若
cid
是连续增长的,可把分片算法course_db_alg
的计算表达式改为course_$->((cid+1)%4).intdiv(2)+1
; - 但实际情况是,这里的
cid
由 snowflake(雪花算法) 生成,而雪花算法生成的 ID 并非连续的,所以这个修改思路在当前场景下无法真正实现数据分到四张表,后续会在讲解“分布式 ID”的相关内容中详细分析原因。
- 若
2 核心概念
2.1 ShardingSphere 分库分表的核心概念
- 表 :: ShardingSphere;
-
核心角色:
-
逻辑表(Logic Table):应用程序直接操作的表,图中是
Course
。它不需要在实际数据库中真实存在,是对真实表的抽象; -
真实库(Actual Database):实际存储数据的数据库,图中有
m0
和m1
这两个真实库,它们被包含在 ShardingSphere 的数据源实例中,由 ShardingSphere 来决定实际上使用哪一个; -
真实表(Actual Table):实际存储数据的表,图中每个真实库下有
Course_1
和Course_2
这两个真实表。真实表与逻辑表结构需相同,可分布在不同真实库中,应用维护逻辑表与真实表的对应关系;
-
-
分片策略的作用
-
分库策略:决定逻辑表的数据如何分配到不同的真实库中。通过分库策略,ShardingSphere 确定在操作逻辑表时,具体使用哪个真实库;
-
分表策略:决定逻辑表的数据如何分配到真实库内的不同真实表中。借助分表策略,ShardingSphere 能确定在某个真实库中,具体使用哪个真实表;
-
-
以操作
Course
逻辑表为例:- 应用程序向
Course
逻辑表发起操作请求; - 分库策略生效,ShardingSphere 根据分库策略,从
m0
和m1
中选择一个真实库; - 进入选中的真实库后,分表策略生效,ShardingSphere 再从该真实库内的
Course_1
和Course_2
中选择一个真实表; - 最终,操作实际作用于所选真实库的所选真实表上;
- 应用程序向
-
其他关键概念
-
虚拟库:ShardingSphere 提供的具备分库分表功能的虚拟库,是
ShardingSphereDataSource
实例。应用程序只需像操作单数据源一样访问它,示例中 MyBatis 框架使用的就是 ShardingSphere 的DataSource
; -
分布式主键生成算法:用于为逻辑表生成唯一主键。因为逻辑表数据分布在多个真实表,单表索引无法保证主键全局唯一,所以需要独立的分布式主键生成算法,示例中用的是
SNOWFLAKE
(雪花算法); -
分片策略的组成:分为分库策略和分表策略,由分片键和分片算法组成。分片键是进行水平拆分的关键字段,分片算法则根据分片键确定对应的真实库和真实表,示例中对
cid
(课程 ID)取模就是一种分片算法。若 ShardingSphere 匹配不到合适分片策略,会进行全分片路由,这是效率最差的实现方式;
在
1.2 引入 ShardingSphere-JDBC 快速实现分库分表
配置文件中,分片键和分片算法的定义如下:分片键是用于决定数据分片(分库或分表)的字段,当前配置中统一使用
cid
作为分片键:-
分库分片键:
cid
-
配置位置:
spring.shardingsphere.rules.sharding.tables.course.database-strategy.standard.sharding-column=cid
-
作用:通过
cid
的值决定数据存储到哪个数据库(m0 或 m1)
-
-
分表分片键:
cid
- 配置位置:
spring.shardingsphere.rules.sharding.tables.course.table-strategy.standard.sharding-column=cid
- 作用:通过
cid
的值决定数据存储到对应数据库中的哪个表(course_1 或 course_2)
- 配置位置:
分库算法
-
算法名称:
course_db_alg
-
算法类型:
MOD
(取模算法)- 配置位置:
spring.shardingsphere.rules.sharding.sharding-algorithms.course_db_alg.type=MOD
- 配置位置:
-
算法参数:
sharding-count=2
(分 2 个库)- 配置位置:
spring.shardingsphere.rules.sharding.sharding-algorithms.course_db_alg.props.sharding-count=2
- 配置位置:
-
计算逻辑:
cid % 2
- 结果为
0
→ 数据分配到m0
库 - 结果为
1
→ 数据分配到m1
库
- 结果为
分表算法
-
算法名称:
course_tbl_alg
-
算法类型:
INLINE
(表达式算法)- 配置位置:
spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.type=INLINE
- 配置位置:
-
算法表达式:
course_$->{cid%2+1}
- 配置位置:
spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.props.algorithm-expression=...
- 配置位置:
-
计算逻辑:
cid
为偶数时(cid%2=0
)→0+1=1
→ 数据分配到course_1
表cid
为奇数时(cid%2=1
)→1+1=2
→ 数据分配到course_2
表
总结
- 分片键:全程使用
cid
(课程 ID)作为分库和分表的判断依据; - 分库算法:基于
MOD
取模,将数据分到 2 个库(m0、m1); - 分表算法:基于
INLINE
表达式,在每个库内将数据分到 2 个表(course_1、course_2)。
-
2.2 垂直分片和水平分片
-
在设计分库分表方案时,有垂直分片和水平分片两种拆分数据的维度,目的都是提高查询效率;
-
垂直分片
-
核心逻辑:按照业务维度,把不同的表分到不同的库中;
-
示例展示:原本有一个包含多类业务表(比如产品表、用户表、订单表等)的数据库(上图中的蓝色大“DB”)。通过垂直分片,将产品相关表放到
Product
库、用户相关表放到User
库、订单相关表放到Order
库; -
作用:减少每个数据库的数据量以及客户端的连接数,从而提高查询效率;
-
-
水平分片
-
核心逻辑:按照数据分布维度,把原本同一张表中的数据,拆分到多张子表中,每个子表只存储一部分数据;
-
示例展示:以订单表为例,原本订单表在
Order
库中是一张大表。通过水平分片,依据订单ID(id
)取模的规则(如id%10=0
分到Order1
表、id%10=1
分到Order2
表、id%10=2
分到Order3
表等),将订单数据拆分到Order1
、Order2
、Order3
等多张子表中; -
作用:减少单张表的数据量,提升查询效率;
-
-
通常我们说的分库分表主要指水平分片,因为它能从根本上减少数据量,解决数据量过大带来的存储和查询问题。但垂直分片方案也很重要,并非可以忽视。
3 ShardingSphere-JDBC常见数据分片策略
3.1 INLINE 简单分片
-
INLINE 简单分片主要用于处理针对分片键的
=
和in
这类查询操作。在这些操作里,能获取到分片键的精确值,进而通过表达式计算出可能的真实库和真实表,ShardingSphere-JDBC 会把逻辑 SQL 转化为对应的真实 SQL,并路由到真实库中执行。例:/*** 针对分片键进行精确查询,都可以使用表达式控制*/ @Test public void queryCourse() {QueryWrapper<Course> wrapper = new QueryWrapper<Course>();// 传入分片键cid的具体值进行精确查询wrapper.eq("cid",924770131651854337L);// 传入分片键cid的多个值进行范围查询// wrapper.in("cid",901136075815124993L, 901136075903205377L, 901136075966119937L,5L);// 可以添加排序条件,不会影响分片逻辑// wrapper.orderByDesc("user_id");List<Course> courses = courseMapper.selectList(wrapper);courses.forEach(course -> System.out.println(course)); }
-
ShardingSphere-JDBC 对查询条件的关注重点
-
ShardingSphere-JDBC 关注的是过滤数据的关键查询条件中是否包含分片键,而非简单关注附加条件。比如在 SQL 语句后面加上
order by user_id
,不会影响 ShardingSphere-JDBC 的处理过程。但如果查询条件中不包含分片键,ShardingSphere-JDBC 就只能根据actual-nodes
,到所有的真实表和真实库中查询,这就是全分片路由; -
对于全分片路由,ShardingSphere-JDBC 做了一定优化,比如通过
Union
将同一库的多条语句结合起来,减少与数据库的交互次数。比如下面的日志示例,逻辑SQL会被转化为针对不同真实库(如m0
、m1
)下不同真实表(如course_1
、course_2
)的SELECT
语句,再通过UNION ALL
组合。不过在真实项目中,要尽力避免全分片路由,因为真实项目通常有几十个甚至上百个分片,这种情况下进行全分片路由,效率会非常低;[INFO] ShardingSphere-SQL :Logic SQL: SELECT cid,cname,user_id,cstatus FROM course [INFO] ShardingSphere-SQL :Actual SQL: m0 ::: SELECT cid,cname,user_id,cstatus FROM course_1 UNION ALL SELECT cid,cname,user_id,cstatus FROM course_2 [INFO] ShardingSphere-SQL :Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_1 UNION ALL SELECT cid,cname,user_id,cstatus FROM course_2
-
-
ShardingSphere-JDBC 只负责改写和路由SQL,至于有没有数据,它并不关心。
3.2 STANDARD 标准分片
-
在应用中,对于主键信息,不只是进行精确查询,还需要进行范围查询(比如查询
cid
在某个区间内的数据),这时候就需要能同时支持精确查询和范围查询的分片算法,即STANDARD标准分片; -
例:
@Test public void queryCourseRange(){//select * from course where cid between xxx and xxxQueryWrapper<Course> wrapper = new QueryWrapper<>();// 构建cid在799020473758714891到799020475802988351之间的范围查询条件wrapper.between("cid", 799020475735871489L, 799020475802980353L);List<Course> courses = courseMapper.selectList(wrapper);courses.forEach(course -> System.out.println(course)); }
-
如果不修改分片算法,直接执行范围查询,ShardingSphere 无法根据配置的表达式计算出可能的分片情况,执行时会抛出异常。从报错信息看,是因为
allow-range-query-with-inline-sharding
属性为false
,内联(INLINE)分片算法无法处理范围查询; -
解决:修改配置文件,允许在内联策略中使用范围查询
# 允许在inline策略中使用范围查询 spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.props.allow-range-query-with-inline-sharding=true
-
加上该参数后,虽然可以进行范围查询,但观察实际执行的 SQL(Actual SQL),会发现 SQL 还是按照全路由的方式执行,这种方式效率很低。那有没有办法通过查询的执行范围下限和范围上限自己计算出目标真实库和真实表呢?其实是支持的,不过这种范围查询要匹配的精确值太多,无法通过简单的表达式来处理,后续的讲解会解决这个问题。
3.3 COMPLEX_INLINE 复杂分片
-
除了针对单个分片键(如
cid
)的查询,实际应用中还可能需要针对多个属性(比如同时涉及cid
和user_id
)进行组合查询。例:@Test public void queryCourseComplexSimple(){// select * from couse where cid in (xxx) and user_id =xxxQueryWrapper<Course> wrapper = new QueryWrapper<Course>();wrapper.in("cid",851198095084486657L, 851198095139012609L); // cid的in查询wrapper.eq("user_id", 1001L); // user_id的eq查询wrapper.orderByDesc("user_id"); // 按user_id降序排序List<Course> course = courseMapper.selectList(wrapper);System.out.println(course); }
-
上面的代码是可以执行的,但是有一个小问题:
user_id
查询条件只能参与数据查询,却不能参与到分片算法中;- 数据库中的测试数据的所有
user_id
都是1001L
,这是很明显的分片规律,但如果user_id
查询条件不是1001L
,我们知道不需要到数据库中查询就能知道不会有结果; - 所以希望
user_id
也能参与到分片算法中,而之前的STANDARD
策略无法满足这个需求;
- 数据库中的测试数据的所有
-
此时需要引入
COMPLEX_INLINE
策略。具体操作是注释掉之前给course
表配置的分表策略,重新配置新的分表策略:# 指定分表策略为complex,即按多个分片键进行组合分表,设置分片键为cid和user_id spring.shardingsphere.rules.sharding.tables.course.table-strategy.complex.sharding-columns=cid,user_id # 指定分片算法名称为course_tbl_alg spring.shardingsphere.rules.sharding.tables.course.table-strategy.complex.sharding-algorithm-name=course_tbl_alg # 配置分片算法类型为COMPLEX_INLINE spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.type=COMPLEX_INLINE # 设置算法表达式 spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.props.algorithm-expression=course_$->{(cid+user_id+1)%2+1}
-
在这个配置中,可以使用
cid
和user_id
两个字段联合确定真实表。例如在查询时,将user_id
条件设定为1002L
,此时不管cid
传什么值,都会路由到错误的表中,查不出数据,这就体现了user_id
参与到分片算法后的效果。
3.4 CLASS_BASED 自定义分片
-
在涉及多属性(如
cid
和user_id
)的组合查询,且有特殊范围查询规则时,之前的COMPLEX_INLINE
策略虽能支持范围查询,但无法用简单表达式满足复杂规则判断。例:@Test public void queryCourdeComplex(){QueryWrapper<Course> wrapper = new QueryWrapper<Course>();// 精确查询cid(课程 ID)在指定集合中的数据wrapper.in("cid", 799020475735871489L, 799020475802980353L);// 范围查询user_id在指定范围的数据wrapper.between("user_id", 3L, 8L);List<Course> course = courseMapper.selectList(wrapper);System.out.println(course); }
- 数据库中的测试数据的所有
user_id
都是1001L
,那么希望在对user_id
进行范围查询时,能够提前判断一些不合理的查询条件,比如在对user_id
进行between
范围查询时,要求查询的范围必须包括 1001L 这个值。如果无法满足该查询规则,那么这个SQL语句的执行结果明显不可能有数据。对于这样的SQL,当然是希望他不要去数据库里执行了。那么这样的需求要怎么实现呢? - 虽然对于 COMPLEX_INLINE 策略,也支持添加
allow-range-query-with-inline-sharding
参数让他能够支持分片键的范围查询,但是这时这种复杂的分片策略就显得不够用了,此时需要自定义分片算法;
- 数据库中的测试数据的所有
-
要实现这样的规则,需要编写一个 Java 类实现 ShardingSphere 提供的
ComplexKeysShardingAlgorithm
接口:public class MyComplexAlgorithm implements ComplexKeysShardingAlgorithm<Long> {private static final String SHARING_COLUMNS_KEY = "sharding-columns";private Properties props;// 配置的分片列private Collection<String> shardingColumns;@Overridepublic void init(Properties props) {this.props = props;this.shardingColumns = getShardingColumns(props);}/*** 复合分片键分片算法实现* @param availableTargetNames 所有可用的数据源或表名称(在配置中定义的实际分片目标)* @param shardingValue 包含复合分片键信息的值对象,这里分片键类型为Long* @return 返回需要路由到的目标分片集合*/@Overridepublic Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Long> shardingValue) {// 从分片值中获取cid列的精确值集合(IN查询的值列表)Collection<Long> cidCol = shardingValue.getColumnNameAndShardingValuesMap().get("cid");// 从分片值中获取user_id列的范围查询值(BETWEEN查询的范围)Range<Long> userIdRange = shardingValue.getColumnNameAndRangeValuesMap().get("user_id");// 提取范围查询的上下界Long lowerEndpoint = userIdRange.lowerEndpoint();Long upperEndpoint = userIdRange.upperEndpoint();// 检查范围查询的有效性if(lowerEndpoint >= upperEndpoint){// 范围无效:下限大于等于上限,抛出异常阻止无效查询throw new UnsupportedShardingOperationException("empty record query","course");}else if(upperEndpoint<1001L || lowerEndpoint>1001L){// 范围明确不包含特定值1001时,抛出异常阻止查询throw new UnsupportedShardingOperationException("error range query param","course");}else{// 范围包含1001时,按cid的奇偶性进行分片路由List<String> result = new ArrayList<>();String logicTableName = shardingValue.getLogicTableName(); // 获取逻辑表名(如"course")// 遍历所有cid值,根据奇偶性确定目标分片for (Long cidVal : cidCol) {// 计算分片后缀:cid为奇数时取1,偶数时取2(cid%2+1)String targetTable = logicTableName+"_"+(cidVal%2+1);// 确保计算出的分片在可用分片中存在if(availableTargetNames.contains(targetTable)){result.add(targetTable);}}return result;}}// 从配置属性中获取分片列配置(实际未在算法逻辑中使用)private Collection<String> getShardingColumns(final Properties props) {String shardingColumns = props.getProperty(SHARING_COLUMNS_KEY, "");return shardingColumns.isEmpty() ? Collections.emptyList() : Arrays.asList(shardingColumns.split(","));}public void setProps(Properties props) {this.props = props;}@Overridepublic Properties getProps() {return this.props;} }
-
类中定义了一些常量和属性,用于存储分片列等配置信息;
-
核心的
doSharding
方法中,会获取user_id
的范围查询上下限(lowerEndpoint
和upperEndpoint
); -
然后进行规则判断,若范围查询的上下限不符合要求(比如不包含
1001L
),就抛出UnsupportedShardingOperationException
异常,阻止后续数据库查询操作;若符合要求,则根据cid
的值计算目标分片表并返回;
-
-
接下来还需要修改配置文件,使用
CLASS_BASED
分片算法:# 指定分片算法类型为CLASS_BASED spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.type=CLASS_BASED # 指定策略为COMPLEX spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.props.strategy=COMPLEX # 指定算法实现类为com.tl.shardingDemo.algorithm.MyComplexAlgoritm spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.props.algorithmClassName=com.tl.shardingDemo.algorithm.MyComplexAlgorithm
-
这时,再去执行查询方法,就会得到这样的异常信息:
- 当执行包含不符合规则的
user_id
范围查询的操作时,ShardingSphere
会根据自定义算法抛出异常(如error range query param
),阻止SQL发送到数据库执行; - 这并非是出现错误,而是对数据库性能的保护,因为
ShardingSphere
模拟成独立虚拟数据库,内部执行异常会以SQLException
形式抛出。
- 当执行包含不符合规则的
3.5 HINT_INLINE 强制分片
-
接下来需要查询所有
cid
为奇数的课程信息。按照 MyBatisPlus 的机制,可能会想到在CourseMapper
中实现自定义 SQL 语句来进行查询:public interface CourseMapper extends BaseMapper<Course> {@Select("select * from course where MOD(cid,2)=1")List<Long> unsupportSql(); }
-
测试一下:
@Test public void unsupportTest(){//select * from course where mod(cid,2)=1List<Long> res = courseMapper.unsupportSql();res.forEach(System.out::println); }
-
执行上述自定义 SQL 语句,执行结果本身没问题,但分库分表的问题出现了;
- 因为课程信息是按照
cid
的奇偶分片的,理想情况下只需要查询一个真实表即可,但由于这种基于虚拟列的查询语句,ShardingSphere 很难解析出是按照cid
分片进行查询的,也不知道如何组织对应的策略进行分库分表,所以只能进行性能最低的全路由查询; - 而且实际上,ShardingSphere 无法正常解析的语句还有很多,分库分表后,应用就难以进行多表关联查询、多层嵌套子查询、
distinct
查询等各种复杂查询了;
- 因为课程信息是按照
-
由于
cid
的奇偶关系无法通过 SQL 语句正常体现,这时候就需要使用 ShardingSphere 提供的HINT
强制路由分片策略。HINT
强制路由可以用一种与 SQL 无关的方式进行分库分表; -
注释掉之前给
course
表分配的分表算法,重新分配一个HINT_INLINE
类型的分表算法:# 指定分表策略为 hint,并设置分片算法名称为 course_tbl_alg spring.shardingsphere.rules.sharding.tables.course.table-strategy.hint.sharding-algorithm-name=course_tbl_alg # 配置分片算法类型为 HINT_INLINE spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.type=HINT_INLINE # 设置算法表达式为 course_$->{value} spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.props.algorithm-expression=course_$->{value}
-
然后,在应用进行查询时,使用 HintManager 给 HINT 策略指定 value 的值:
@Test public void queryCourseByHint(){// 获取 HintManager 实例HintManager hintManager = HintManager.getInstance();// 然后调用 addTablesShardingValue 方法,强制指定查询 course 表的 course_1 表(传入参数 "course" 和 "1")hintManager.addTableShardingValue("course", "1");// 执行查询操作List<Course> courses = courseMapper.selectList(null);courses.forEach(course -> System.out.println(course));// 关闭 HintManager 实例hintManager.close();// HintManager 关闭的主要作用是清除 ThreadLocal,释放内存,也可以使用 try-resource 方式让其用完自动关闭,即:// try(HintManager hintManager = HintManager.getInstance()){ xxxx } }
-
这样就可以让SQL语句只查询
course_1
表,在当前场景下,也就相当于是实现了只查奇数cid
的需求。
3.6 小结
-
分库分表的本质是解决数据量大的问题,但不同业务场景下,数据的生成、查询、增长模式差异极大,这决定了需要多种分片策略适配:
-
简单场景:如按ID奇偶分表(
MOD
策略)、按ID哈希后取模(HASH-MOD
),适合数据均匀分布、查询以ID为主的场景; -
复杂场景:
- 需同时用多个字段决定分片(如
cid+user_id
组合,对应complex_inline
策略); - 需支持范围查询(如按时间范围分片,对应
standard
策略); - 需绕过SQL解析直接指定分片(如特殊业务查询,对应
hint_inline
策略); - 需自定义业务规则(如按用户地区+注册时间分片,对应
CLASS_BASED
扩展策略)。
- 需同时用多个字段决定分片(如
-
ShardingSphere 提供多种策略,正是为了覆盖这些千差万别的业务需求,让分库分表能在不同场景下落地;
-
-
数据库(如MySQL)是成熟的独立产品,能解析并执行几乎所有标准SQL,但分库分表中间件(如ShardingSphere)本质是模拟数据库行为,存在天然局限:
-
SQL解析难度:复杂SQL(如多表关联、嵌套子查询、
distinct
、group by
跨分片)的分片键提取和路由逻辑极其复杂,中间件难以完美处理; -
性能代价:若SQL无法匹配分片策略(如缺少分片键的查询),会触发全分片路由(扫描所有分库分表),性能极差;
-
因此,ShardingSphere无法像原生数据库那样支持所有SQL,必须依赖分片策略来优化路由;
-
-
一旦采用分库分表,开发模式需从随意写SQL转变为结合分片策略设计SQL:
-
写SQL前需明确:是否包含分片键?用了哪种查询方式(
IN
/BETWEEN
/范围查询)?是否匹配当前分片策略? -
避免使用中间件不支持的复杂SQL,必要时需拆分查询或改用
hint
等特殊策略;
-
-
简言之,分库分表带来了性能提升,但代价是业务SQL需适配分片策略,不能再像操作单库单表那样随心所欲。
4 数据加密功能
-
ShardingSphere 内置了多种加密算法,可以用来快速对关键数据(比如用户的密码)进行加密。使用 ShardingSphere 就可以用应用无感知的方式,快速实现数据加密,并且可以灵活切换多种内置的加密算法;
-
在
m0
、m1
数据库中分别新建user_1
和user_2
两张用户表,来实现数据加密的功能:CREATE TABLE user (`userid` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,`password_cipher` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,`userstatus` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,`age` int(0) DEFAULT NULL,`sex` varchar(2) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'F or M',PRIMARY KEY (`userid`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
该表后续还可以用来测试字符串型主键的生成以及数据分片等功能,因此建议在
shardingdb1
和shardingdb2
两个数据库中也分别创建user_1
和user_2
两个分片表; -
创建实体:
@TableName("user") public class User {private String userid;private String username;private String password;private String userstatus;private int age;private String sex;// getter ... setter ... }
-
创建 mapper:
public interface UserCourseInfoMapper extends BaseMapper<UserCourseInfo> { }
-
配置文件中配置
user
表的加密算法:# 启用SQL日志打印,在控制台输出ShardingSphere改写后的真实SQL,用于调试 spring.shardingsphere.props.sql-show = true # 允许Bean定义覆盖,解决多数据源等配置可能引起的Bean冲突问题 spring.main.allow-bean-definition-overriding = true# ---------------- 数据源配置 ---------------- # 定义两个数据源的逻辑名称 spring.shardingsphere.datasource.names=m0,m1# 配置第一个数据源m0的连接参数(使用Druid连接池) spring.shardingsphere.datasource.m0.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.m0.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.m0.url=jdbc:mysql://192.168.65.212:3306/shardingdb1?serverTimezone=Asia/Shanghai spring.shardingsphere.datasource.m0.username=root spring.shardingsphere.datasource.m0.password=root# 配置第二个数据源m1的连接参数(使用Druid连接池) spring.shardingsphere.datasource.m1.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.m1.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.m1.url=jdbc:mysql://192.168.65.212:3306/shardingdb2?serverTimezone=Asia/Shanghai spring.shardingsphere.datasource.m1.username=root spring.shardingsphere.datasource.m1.password=root#------------------------ 分布式序列算法配置 ------------------------ # 配置NANOID算法生成器,用于生成字符串类型的分布式主键(相比UUID更短且有序) spring.shardingsphere.rules.sharding.key-generators.user_keygen.type=NANOID # 为user表指定主键生成策略:使用userid字段,并采用NANOID算法生成值 spring.shardingsphere.rules.sharding.tables.user.key-generate-strategy.column=userid spring.shardingsphere.rules.sharding.tables.user.key-generate-strategy.key-generator-name=user_keygen#----------------------- 分片规则配置 ----------------------- # 配置user表的实际数据节点分布:分布在m0和m1两个库,每个库有user_1和user_2两张表 spring.shardingsphere.rules.sharding.tables.user.actual-data-nodes=m$->{0..1}.user_$->{1..2}# 配置分库策略:使用标准分片策略,以userid字段作为分片键 spring.shardingsphere.rules.sharding.tables.user.database-strategy.standard.sharding-column=userid spring.shardingsphere.rules.sharding.tables.user.database-strategy.standard.sharding-algorithm-name=user_db_alg# 配置分库算法:使用HASH_MOD哈希取模算法,分成2个库 spring.shardingsphere.rules.sharding.sharding-algorithms.user_db_alg.type=HASH_MOD spring.shardingsphere.rules.sharding.sharding-algorithms.user_db_alg.props.sharding-count=2# 配置分表策略:使用标准分片策略,以userid字段作为分片键 spring.shardingsphere.rules.sharding.tables.user.table-strategy.standard.sharding-column=userid spring.shardingsphere.rules.sharding.tables.user.table-strategy.standard.sharding-algorithm-name=user_tbl_alg# 配置分表算法:使用INLINE表达式分表算法 spring.shardingsphere.rules.sharding.sharding-algorithms.user_tbl_alg.type=INLINE # 分表表达式:将数据分布到4个分片中(user_1, user_2) # 1. 对userid字符串取hashCode转为整型 # 2. 对4取模,结果可能为0,1,2,3 # 3. 使用intdiv(2)进行整数除法,将4种结果映射为2种(0,1) # 4. 加1得到最终的表后缀(1,2) spring.shardingsphere.rules.sharding.sharding-algorithms.user_tbl_alg.props.algorithm-expression=user_$->{Math.abs(userid.hashCode()%4).intdiv(2) +1}# ---------------- 数据加密规则配置 ---------------- # 为user表的password字段配置加密规则 # 配置明文存储字段(可选,用于加密解密过程中临时存储或兼容旧数据) spring.shardingsphere.rules.encrypt.tables.user.columns.password.plainColumn = password # 配置密文存储字段(实际存储加密后的密码) spring.shardingsphere.rules.encrypt.tables.user.columns.password.cipherColumn = password_cipher # 指定使用的加密器名称 spring.shardingsphere.rules.encrypt.tables.user.columns.password.encryptorName = user_password_encry# 配置SM3加密器(国密算法,密码散列函数,不可逆加密) spring.shardingsphere.rules.encrypt.encryptors.user_password_encry.type=SM3 # 配置SM3加密的盐值,增强安全性 spring.shardingsphere.rules.encrypt.encryptors.user_password_encry.props.sm3-salt=12345678
-
单元测试案例:
@Test public void addUser(){for (int i = 0; i < 10; i++) {User user = new User();user.setUsername("user"+i);user.setPassword("123qweasd");user.setUserstatus("NORMAL");user.setAge(30+i);user.setSex(i%2==0?"F":"M");userMapper.insert(user);} }
-
在插入时,就会在
password_cipher
字段中加入加密后的密文: -
接下来针对
password
字段的查询,会转化成为密文后,再去查询。查询案例:@Test public void queryUser() {QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("password","123qweasd");List<User> users = userMapper.selectList(queryWrapper);for(User user : users){System.out.println(user);} }
5 读写分离实现
-
读写分离是应用中常用的保护数据库的方案。其核心思路是将写请求和读请求分发到不同的数据库(主库
Master
和从库Slave
),从而减少主库的客户端请求压力; -
读写分离方案通常需要分两个层面配合解决:
-
数据层面:需要将
Master
的数据实时同步到Slave
,这部分通常借助第三方工具(如Canal
框架)或者数据库自身提供的主从同步方案(如MySQL的主从同步)来实现; -
应用层面:要把读请求和写请求分发到不同的数据库中,这本质是一种数据路由功能,使用
ShardingSphere
来实现较为简单,只需配置一个readwrite-splitting
的分片规则即可;
-
-
以针对
user
表的读写分离配置为例:# 启用SQL日志打印,在控制台输出ShardingSphere执行的真实SQL,便于调试和监控 spring.shardingsphere.props.sql-show = true # 允许Bean定义覆盖,解决多数据源配置时可能出现的Bean冲突问题 spring.main.allow-bean-definition-overriding = true# ---------------- 数据源配置 ---------------- # 定义两个数据源的逻辑名称:m0(主库)和m1(从库) spring.shardingsphere.datasource.names=m0,m1# 配置主库数据源m0(使用Druid连接池) spring.shardingsphere.datasource.m0.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.m0.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.m0.url=jdbc:mysql://localhost:3306/coursedb?serverTimezone=UTC spring.shardingsphere.datasource.m0.username=root spring.shardingsphere.datasource.m0.password=root# 配置从库数据源m1(使用Druid连接池) spring.shardingsphere.datasource.m1.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.m1.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.m1.url=jdbc:mysql://localhost:3306/coursedb2?serverTimezone=UTC spring.shardingsphere.datasource.m1.username=root spring.shardingsphere.datasource.m1.password=root#------------------------ 分布式序列算法配置 ------------------------ # 配置NANOID算法生成器,用于生成字符串类型的分布式主键(相比UUID更短且有序) spring.shardingsphere.rules.sharding.key-generators.user_keygen.type=NANOID # 为user表指定主键生成策略:使用userid字段作为主键,并采用NANOID算法生成值 spring.shardingsphere.rules.sharding.tables.user.key-generate-strategy.column=userid spring.shardingsphere.rules.sharding.tables.user.key-generate-strategy.key-generator-name=user_keygen#----------------------- 读写分离配置 ----------------------- # 配置user表的实际数据节点:指向读写分离的虚拟数据库userdb中的user表 spring.shardingsphere.rules.sharding.tables.user.actual-data-nodes=userdb.user# 配置读写分离虚拟数据库userdb:设置写操作指向主库m0,读操作指向从库m1 spring.shardingsphere.rules.readwrite-splitting.data-sources.userdb.static-strategy.write-data-source-name=m0 spring.shardingsphere.rules.readwrite-splitting.data-sources.userdb.static-strategy.read-data-source-names[0]=m1# 为读写分离数据源指定负载均衡器名称 spring.shardingsphere.rules.readwrite-splitting.data-sources.userdb.load-balancer-name=user_lb# 配置负载均衡器:使用轮询策略(按操作轮询) # 每次读请求会依次选择不同的从库,实现负载均衡 spring.shardingsphere.rules.readwrite-splitting.load-balancers.user_lb.type=ROUND_ROBIN# 其他可选的负载均衡策略: # 按事务轮询:在同一事务中的所有读请求都会路由到同一个从库 #spring.shardingsphere.rules.readwrite-splitting.load-balancers.user_lb.type=TRANSACTION_ROUND_ROBIN# 随机选择从库:每次读请求随机选择一个从库 #spring.shardingsphere.rules.readwrite-splitting.load-balancers.user_lb.type=RANDOM# 按事务随机:在同一事务中的所有读请求都会路由到同一个随机选择的从库 #spring.shardingsphere.rules.readwrite-splitting.load-balancers.user_lb.type=TRANSACTION_RANDOM# 强制路由到主库:所有读请求都路由到主库,适用于需要强一致性读的场景 #spring.shardingsphere.rules.readwrite-splitting.load-balancers.user_lb.type=FIXED_PRIMARY
-
执行对
user
表的插入和查询操作,从日志中就能体会到读写分离的实现效果,插入操作会路由到m0
(写数据源),查询操作会路由到m1
(读数据源)。
6 广播表与绑定表
- 表 :: ShardingSphere。
6.1 广播表
-
广播表是指在分库分表的所有分片数据源中都存在的表,且每个数据库里,该表的结构和数据完全一致,适用于数据量不大,但需要和海量数据的表做关联查询的场景;
-
建表:
CREATE TABLE dict (`dictId` bigint NOT NULL,`dictKey` varchar(32) NULL,`dictVal` varchar(32) NULL,PRIMARY KEY (`dictId`) );
-
创建实体:
@TableName("dict") public class Dict {private Long dictid;private String dictkey;private String dictval;// getter ... setter }
-
创建 mapper:
public interface DictMapper extends BaseMapper<Dict> { }
-
配置广播规则:
# 启用SQL日志打印,在控制台输出ShardingSphere执行的真实SQL,便于调试和监控 spring.shardingsphere.props.sql-show = true # 允许Bean定义覆盖,解决多数据源配置时可能出现的Bean冲突问题 spring.main.allow-bean-definition-overriding = true# ---------------- 数据源配置 ---------------- # 定义两个数据源的逻辑名称:m0和m1 spring.shardingsphere.datasource.names=m0,m1# 配置数据源m0(使用Druid连接池) spring.shardingsphere.datasource.m0.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.m0.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.m0.url=jdbc:mysql://localhost:3306/coursedb?serverTimezone=UTC spring.shardingsphere.datasource.m0.username=root spring.shardingsphere.datasource.m0.password=root# 配置数据源m1(使用Druid连接池) spring.shardingsphere.datasource.m1.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.m1.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.m1.url=jdbc:mysql://localhost:3306/coursedb2?serverTimezone=UTC spring.shardingsphere.datasource.m1.username=root spring.shardingsphere.datasource.m1.password=root#------------------------ 分布式序列算法配置 ------------------------ # 配置SNOWFLAKE雪花算法生成器,用于生成长整型分布式主键 spring.shardingsphere.rules.sharding.key-generators.dict_keygen.type=SNOWFLAKE # 为dict表指定主键生成策略:使用dictId字段作为主键,并采用雪花算法生成值 spring.shardingsphere.rules.sharding.tables.dict.key-generate-strategy.column=dictId spring.shardingsphere.rules.sharding.tables.dict.key-generate-strategy.key-generator-name=dict_keygen#----------------------- 分片配置 ----------------------- # 配置dict表的实际数据节点:分布在m0和m1两个库中,每个库都有一个名为dict的表 # 注释掉的分表配置示例:如果需要进行分表,可以使用这种格式 m$->{0..1}.dict_$->{1..2} spring.shardingsphere.rules.sharding.tables.dict.actual-data-nodes=m$->{0..1}.dict# 将dict表配置为广播表:广播表会在所有分片库中都存在完全相同的副本 # 对广播表的任何写操作(INSERT/UPDATE/DELETE)都会自动同步到所有库的对应表中 # 查询广播表时,ShardingSphere会从任意一个库中获取数据(因为所有库的数据都相同) spring.shardingsphere.rules.sharding.broadcast-tables=dict
-
测试示例:
@Test public void addDict() {Dict dict = new Dict();dict.setDictkey("F");dict.setDictval("女");dictMapper.insert(dict);Dict dict2 = new Dict();dict2.setDictkey("M");dict2.setDictval("男");dictMapper.insert(dict2); }
-
这样,对于
dict
表的操作就会被同时插入到两个库当中。
6.2 绑定表
-
绑定表指分片规则一致的一组分片表。使用绑定表进行多表关联查询时,必须使用分片键进行关联,否则会出现笛卡尔积关联或跨库关联,从而影响查询效率;
-
下面另外创建一张用户信息表,与用户表一起来演示这种情况:
-
建表:
CREATE TABLE user_course_info (`infoid` bigint NOT NULL,`userid` varchar(32) NULL,`courseid` bigint NULL,PRIMARY KEY (`infoid`) );
-
创建实体、创建 mapper,省略;
-
配置分片规则:
# 启用SQL日志打印,在控制台输出ShardingSphere执行的真实SQL,便于调试和监控 spring.shardingsphere.props.sql-show = true # 允许Bean定义覆盖,解决多数据源配置时可能出现的Bean冲突问题 spring.main.allow-bean-definition-overriding = true# ---------------- 数据源配置 ---------------- # 定义单个数据源的逻辑名称:m0(单库多表架构) spring.shardingsphere.datasource.names=m0# 配置数据源m0(使用Druid连接池) spring.shardingsphere.datasource.m0.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.m0.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.m0.url=jdbc:mysql://localhost:3306/coursedb?serverTimezone=UTC spring.shardingsphere.datasource.m0.username=root spring.shardingsphere.datasource.m0.password=root#------------------------ 分布式序列算法配置 ------------------------ # 配置SNOWFLAKE雪花算法生成器,用于生成长整型分布式主键 spring.shardingsphere.rules.sharding.key-generators.usercourse_keygen.type=SNOWFLAKE # 为user_course_info表指定主键生成策略:使用infoid字段作为主键,并采用雪花算法生成值 spring.shardingsphere.rules.sharding.tables.user_course_info.key-generate-strategy.column=infoid spring.shardingsphere.rules.sharding.tables.user_course_info.key-generate-strategy.key-generator-name=usercourse_keygen# ---------------------- 配置真实表分布 ---------------------- # 配置user表的实际数据节点:在m0库中分为user_1和user_2两个物理表 spring.shardingsphere.rules.sharding.tables.user.actual-data-nodes=m0.user_$->{1..2} # 配置user_course_info表的实际数据节点:在m0库中分为user_course_info_1和user_course_info_2两个物理表 spring.shardingsphere.rules.sharding.tables.user_course_info.actual-data-nodes=m0.user_course_info_$->{1..2}# ---------------------- 配置分片策略 ---------------------- # 为user表配置分表策略:使用标准分片策略,以userid字段作为分片键 spring.shardingsphere.rules.sharding.tables.user.table-strategy.standard.sharding-column=userid spring.shardingsphere.rules.sharding.tables.user.table-strategy.standard.sharding-algorithm-name=user_tbl_alg# 为user_course_info表配置分表策略:使用标准分片策略,以userid字段作为分片键 spring.shardingsphere.rules.sharding.tables.user_course_info.table-strategy.standard.sharding-column=userid spring.shardingsphere.rules.sharding.tables.user_course_info.table-strategy.standard.sharding-algorithm-name=usercourse_tbl_alg# ---------------------- 配置分表算法 ---------------------- # 配置user表的分表算法:使用INLINE表达式分表算法 spring.shardingsphere.rules.sharding.sharding-algorithms.user_tbl_alg.type=INLINE # user表分表表达式:将数据分布到2个分片表(user_1, user_2) # 1. 对userid字符串取hashCode转为整型 # 2. 对4取模,结果可能为0,1,2,3 # 3. 使用intdiv(2)进行整数除法,将4种结果映射为2种(0,1) # 4. 加1得到最终的表后缀(1,2) spring.shardingsphere.rules.sharding.sharding-algorithms.user_tbl_alg.props.algorithm-expression=user_$->{Math.abs(userid.hashCode()%4).intdiv(2) +1}# 配置user_course_info表的分表算法:使用INLINE表达式分表算法 spring.shardingsphere.rules.sharding.sharding-algorithms.usercourse_tbl_alg.type=INLINE # user_course_info表分表表达式:与user表使用相同的分片逻辑 # 确保相同userid的数据在两个表中被路由到相同后缀的物理表 spring.shardingsphere.rules.sharding.sharding-algorithms.usercourse_tbl_alg.props.algorithm-expression=user_course_info_$->{Math.abs(userid.hashCode()%4).intdiv(2) +1}# 指定绑定表关系:user表和user_course_info表为绑定表 # 绑定表指的是具有相同分片规则且存在关联关系的表 spring.shardingsphere.rules.sharding.binding-tables[0]=user,user_course_info
-
然后把
user
表的数据都清空,重新插入一些有对应关系的用户和用户信息表:@Test public void addUserCourseInfo(){for (int i = 0; i < 10; i++) {String userId = NanoIdUtils.randomNanoId();User user = new User();user.setUserid(userId);user.setUsername("user"+i);user.setPassword("123qweasd");user.setUserstatus("NORMAL");user.setAge(30+i);user.setSex(i%2==0?"F":"M");userMapper.insert(user);for (int j = 0; j < 5; j++) {UserCourseInfo userCourseInfo = new UserCourseInfo();userCourseInfo.setInfoid(System.currentTimeMillis()+j);userCourseInfo.setUserid(userId);userCourseInfo.setCourseid(10000+j);userCourseInfoMapper.insert(userCourseInfo);}} }
-
接下来按照用户ID进行一次关联查询。在
UserCourseInfoMapper
中配置SQL语句:public interface UserCourseInfoMapper extends BaseMapper<UserCourseInfo> {@Select("select uci.* from user_course_info uci ,user u where uci.userid = u.userid")List<UserCourseInfo> queryUserCourse(); }
-
查询案例:
@Test public void queryUserCourseInfo(){List<UserCourseInfo> userCourseInfos = userCourseInfoMapper.queryUserCourse();for (UserCourseInfo userCourseInfo : userCourseInfos) {System.out.println(userCourseInfo);} }
-
在进行查询时,可以先把
application.properties
文件中最后一行,绑定表的配置注释掉。此时两张表的关联查询将要进行笛卡尔查询:Actual SQL: m0 ::: select uci.* from user_course_info_1 uci ,user_1 u where uci.userid = u.userid Actual SQL: m0 ::: select uci.* from user_course_info_1 uci ,user_2 u where uci.userid = u.userid Actual SQL: m0 ::: select uci.* from user_course_info_2 uci ,user_1 u where uci.userid = u.userid Actual SQL: m0 ::: select uci.* from user_course_info_2 uci ,user_2 u where uci.userid = u.userid
-
这种查询明显性能是非常低的,如果两张表的分片数更多,执行的SQL也会更多。而实际上,用户表和用户信息表,他们都是按照
userid
进行分片的,他们的分片规则是一致的。再把绑定关系的注释加上,此时查询,就会按照相同的userid
分片进行查询:Actual SQL: m0 ::: select uci.* from user_course_info_1 uci ,user_1 u where uci.userid = u.userid Actual SQL: m0 ::: select uci.* from user_course_info_2 uci ,user_2 u where uci.userid = u.userid
7 分片审计
-
分片审计功能是针对数据库分片场景下对执行的SQL语句进行审计操作。它既可以进行拦截操作,拦截系统配置的非法SQL语句,也可以对SQL语句进行统计操作;
-
目前ShardingSphere内置的分片审计算法只有一个,即
DML_SHARDING_CONDITIONS
。它的功能是要求对逻辑表进行查询时,必须带上分片键; -
例如在之前的示例中,给
course
表配置一个分片审计策略:# 指定审计器名称为course_auditor spring.shardingsphere.rules.sharding.tables.course.audit-strategy.auditor-names[0]=course_auditor # 设置允许通过提示禁用审计 spring.shardingsphere.rules.sharding.tables.course.audit-strategy.allow-hint-disable=true # 配置审计器course_auditor的类型为DML_SHARDING_CONDITIONS spring.shardingsphere.rules.sharding.auditors.course_auditor.type=DML_SHARDING_CONDITIONS
-
这样配置后,再次执行之前使用
HINT
策略(3.5 HINT_INLINE 强制分片
)的示例时,就会报错:- 黄框内容:不允许在没有分片条件的情况下执行DML操作,这是因为
DML_SHARDING_CONDITIONS
算法检测到查询没有带上分片键,从而拦截了该非法SQL;
- 黄框内容:不允许在没有分片条件的情况下执行DML操作,这是因为
-
当前这个
DML_SHARDING_CONDITIONS
策略看起来用处好像不是很大,但 ShardingSphere 具有可插拔的设计,分片审计是一个扩展点,开发者可以自行扩展,从而实现很多有用的功能,比如根据业务需求定制更复杂的SQL审计规则等。