ShardingJDBC核心概念与快速实战
一、ShardingSphere
ShardingSphere是一款起源于当当网内部的应用框架。
2015年在当当网内部诞生,最初就叫ShardingJDBC。2016年的时候,由其中一个主要的开发人员张亮,带入到京东数科,组件团队继续开发。在国内历经了当当网、电信翼支付、京东数科等多家大型互联网企业的考验,在2017年开始开源。
并逐渐由原本只关注于关系型数据库增强工具的ShardingJDBC升级成为一整套以数据分片为基础的数据生态圈,更名为ShardingSphere。到2020年4月,已经成为了Apache软件基金会的顶级项目。
ShardingSphere包含三个重要的产品,ShardingJDBC、ShardingProxy和ShardingSidecar。其中sidecar是针对service mesh定位的一个分库分表插件,目前在规划中。而我们今天学习的重点是ShardingSphere的JDBC和Proxy这两个组件。
其中,ShardingJDBC是用来做客户端分库分表的产品,而ShardingProxy是用来做服务端分库分表的产品。这两者定位有什么区别呢?我们看下官方资料中给出的两个重要的图:
1、shardingJDBC
shardingJDBC定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。它使⽤客户端直连数据库,以 jar 包形式提供服务,⽆需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
ShardingJDBC对代码的入侵性比较大,但是有比较灵活的配置分片规则。
2、ShardingProxy
ShardingProxy定位为透明化的数据库代理端,提供封装了数据库⼆进制协议的服务端版本,⽤于完成对异构语⾔的⽀持。⽬前提供 MySQL 和 PostgreSQL 版本,它可以使⽤任何兼容 MySQL/PostgreSQL 协议的访问客⼾端。
ShardingProxy是一个独立部署的服务,类似于MyCat。这意味着分片规则需要提前订好,灵活性降低了。
那这两种方式有什么区别呢?
3、区别
很显然,ShardingJDBC只是客户端的一个工具包,可以理解为一个特殊的JDBC驱动包,所有分库分表逻辑均由业务方自己控制,所以他的功能相对灵活,支持的数据库也非常多,但是对业务侵入大,需要业务方自己定制所有的分库分表逻辑。
而ShardingProxy是一个独立部署的服务,对业务方无侵入,业务方可以像用一个普通的MySQL服务一样进行数据交互,基本上感觉不到后端分库分表逻辑的存在,但是这也意味着功能会比较固定,能够支持的数据库也比较少。这两者各有优劣。
二、ShardingJDBC实战
shardingjdbc的核心功能是数据分片和读写分离,通过ShardingJDBC,应用可以透明的使用JDBC访问已经分库分表、读写分离的多个数据源,而不用关心数据源的数量以及数据如何分布。
1、核心概念
- 逻辑表:水平拆分的数据库的相同逻辑和数据结构表的总称
- 真实表:在分片的数据库中真实存在的物理表
- 数据节点:数据分片的最小单元。由数据源名称和数据表组成
- 绑定表:分片规则一致的主表和子表
- 广播表:也叫公共表,指素有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中都完全一致。例如字典表
- 分片键:用于分片的数据库字段,是将数据库(表)进行水平拆分的关键字段。SQL中若没有分片字段,将会执行全路由,性能会很差
- 分片算法:通过分片算法将数据进行分片,支持通过=、BETWEEN和IN分片。分片算法需要由应用开发者自行实现,可实现的灵活度非常高
- 分片策略:真正用于进行分片操作的是分片键+分片算法,也就是分片策略。在ShardingJDBC中一般采用基于Groovy表达式的inline分片策略,通过一个包含分片键的算法表达式来制定分片策略,如t_user_$->{u_id%8}标识根据u_id模8,分成8张表,表名称为t_user_0到t_user_7
2、测试项目介绍
测试代码gitee仓库地址:https://gitee.com/tuojihu/sharding-sphere-demo
首先我们对测试项目的结构做下简单的梳理:
entity:
public class Course {
private Long cid;
private String cname;
private Long userId;
private String cstatus;
......
@TableName("t_dict")
public class Dict {
private Long dictId;
private String ustatus;
private String uvalue;
.......
@TableName("user")
public class User {
private Long userId;
private String username;
private String ustatus;
private int uage;
......
@TableName("ustatus")
public class Ustatus {
private Long ustatusId;
private String ustatus;
private String uvalue;
......
mapper:
public interface CourseMapper extends BaseMapper<Course> {
}
public interface DictMapper extends BaseMapper<Dict> {
}
public interface UserMapper extends BaseMapper<User> {
@Select("select u.user_id,u.username,us.uvalue ustatus from user u left join ustatus us on u.ustatus = us.ustatus")
public List<User> queryUserStatus();
}
public interface UstatustMapper extends BaseMapper<Ustatus> {
}
启动类:
@SpringBootApplication
@MapperScan("com.jihu.sharding.mapper")
public class ShardingJDBCApplication {
public static void main(String[] args) {
SpringApplication.run(SpringApplication.class, args);
}
}
pom:
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
Sql:
CREATE TABLE course_1 (
cid BIGINT(20) PRIMARY KEY,
cname VARCHAR(50) NOT NULL,
user_id BIGINT(20) NOT NULL,
cstatus varchar(10) NOT NULL
);
CREATE TABLE course_2 (
cid BIGINT(20) PRIMARY KEY,
cname VARCHAR(50) NOT NULL,
user_id BIGINT(20) NOT NULL,
cstatus varchar(10) NOT NULL
);
-- ==========================================
CREATE TABLE `t_dict` (
`dict_id` bigint(0) PRIMARY KEY NOT NULL,
`ustatus` varchar(100) NOT NULL,
`uvalue` varchar(100) NOT NULL
);
CREATE TABLE `t_dict_1` (
`dict_id` bigint(0) PRIMARY KEY NOT NULL,
`ustatus` varchar(100) NOT NULL,
`uvalue` varchar(100) NOT NULL
);
CREATE TABLE `t_dict_2` (
`dict_id` bigint(0) PRIMARY KEY NOT NULL,
`ustatus` varchar(100) NOT NULL,
`uvalue` varchar(100) NOT NULL
);
-- ==========================================
CREATE TABLE `t_user` (
`user_id` bigint(0) PRIMARY KEY NOT NULL,
`username` varchar(100) NOT NULL,
`ustatus` varchar(50) NOT NULL,
`uage` int(3)
);
CREATE TABLE `t_user_1` (
`user_id` bigint(0) PRIMARY KEY NOT NULL,
`username` varchar(100) NOT NULL,
`ustatus` varchar(50) NOT NULL,
`uage` int(3)
);
CREATE TABLE `t_user_2` (
`user_id` bigint(0) PRIMARY KEY NOT NULL,
`username` varchar(100) NOT NULL,
`ustatus` varchar(50) NOT NULL,
`uage` int(3)
);
CREATE TABLE `t_ustatus_1` (
`dict_id` bigint(0) PRIMARY KEY NOT NULL,
`ustatus` varchar(100) NOT NULL,
`uvalue` varchar(100) NOT NULL
);
CREATE TABLE `t_ustatus_1` (
`ustatus_id` bigint(0) PRIMARY KEY NOT NULL,
`ustatus` varchar(100) NOT NULL,
`uvalue` varchar(100) NOT NULL
);
CREATE TABLE `t_ustatus_2` (
`ustatus_id` bigint(0) PRIMARY KEY NOT NULL,
`ustatus` varchar(100) NOT NULL,
`uvalue` varchar(100) NOT NULL
);
算法类后面使用的时候再补充!
注意:
1、引入MyBatisPlus依赖,简化JDBC操作,这样我们就不需要在代码中写SQL语句了。
2、entity中的实体对象就对应数据库中的表结构。而mapper中的接口则对应JDBC操作。
3、所有操作均使用JUnit的测试案例执行。 后续所有测试操作都会配合application.properties中的配置以及JUnit测试案例进行。
4、关于ShardingSphere版本,由于目前最新的5.0版本还在孵化当中,所以我们使用已发布的4.1.1版本来进行学习。
3、快速实战
3.1 分表示例配置
在项目中对应的配置文件名称:application01-split-tables.properties
我们先运行一个简单的实例,来看下ShardingJDBC是如何工作的。
在application.properties配置文件中写入application01.properties文件(默认会使用application.properties文件,我们将下面的内容复制application.properties文件中去执行!后面再复制不同的。)的内容:
注意:第一个数据源名称是m0,不要写错!否则可能报错说找不到数据连接信息!
注意:数据表需要自己提前建好,shardingJDBC不会帮助我们创建数据表的!所以,我们做下面这个测试,我们需要提前在数据库中创建好course_1和course_2表。
# ============ 分表配置: 同一个数据库中的不同的表 ==============================
# 配置真实数据源, 名称为m0
spring.shardingsphere.datasource.names=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://docker.host.a:3306/ShardingJdbcDemo?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.m0.username=root
spring.shardingsphere.datasource.m0.password=123456
# 构建名为course的逻辑表, 并设置逻辑表对应的真实表: m0.course_1, m0.course_2
# actual-data-nodes属性即定义course逻辑表的实际数据分布情
# $->{1..2}: '$->'表示后面要进行的运算; '{1..2}' 表示轮训1和2; 如果是三张表:m0.course_$->{1..3}
spring.shardingsphere.sharding.tables.course.actual-data-nodes=m0.course_$->{1..2}
# ===> 指定表的主键生成策略
# 设置主键对应的列
spring.shardingsphere.sharding.tables.course.key-generator.column=cid
# 设置主键生成策略为雪花算法. 为什么不选UUID? 是字符串, 无法做这种运算. 可读性差
spring.shardingsphere.sharding.tables.course.key-generator.type=SNOWFLAKE
# 雪花算法的一个可选参数
spring.shardingsphere.sharding.tables.course.key-generator.props.worker.id=1
# 使用自定义的主键生成策略
#spring.shardingsphere.sharding.tables.course.key-generator.type=MYKEY
#spring.shardingsphere.sharding.tables.course.key-generator.props.mykey-offset=88
# ===> 指定分表策略为: inline
# 设置分片键
spring.shardingsphere.sharding.tables.course.table-strategy.inline.sharding-column=cid
# 设置分片算法: 偶数course_1, 奇数course_2
spring.shardingsphere.sharding.tables.course.table-strategy.inline.algorithm-expression=course_$->{cid%2+1}
# 打开sql日志输出。
spring.shardingsphere.props.sql.show = true
spring.main.allow-bean-definition-overriding=true
1、首先定义一个数据源m0,并对m0进行实际的JDBC参数配置。
2、spring.shardingsphere.sharding.tables.course开头的一系列属性即定义了一个名为course的逻辑表。
3、actual-data-nodes属性即定义course逻辑表的实际数据分布情况,他分布在m1.course_1和m1.course_2两个表。
4、key-generator属性配置了他的主键列以及主键生成策略。ShardingJDBC默认提供了UUID和SNOWFLAKE两种分布式主键生成策略。
5、table-strategy属性即配置他的分库分表策略。分片键为cid属性。分片算法为course_$->{cid%2+1},表示按照cid模2+1的结果,然后加上前面的course__ 部分作为前缀就是他的实际表结果。注意,这个表达式计算出来的结果需要能够与实际数据分布中的一种情况对应上,否则就会报错。
6、sql.show属性表示要在日志中打印实际SQL。
7、coursedb的表结构见示例中sql文件夹中的sql语句。
然后我们执行测试案例中的addcourse案例。
@Test
public void addCourse(){
for(int i = 0 ; i < 10 ; i ++){
Course c = new Course();
c.setCname("shardingsphere");
c.setUserId(Long.valueOf(""+(1000+i)));
c.setCstatus("1");
courseMapper.insert(c);
}
}
执行后,我们可以在控制台看到很多条这样的日志:
Logic SQL: INSERT INTO course ( cname,user_id,cstatus ) VALUES ( ?,?,? )
Actual SQL: m0 ::: INSERT INTO course_1 ( cname,user_id,cstatus , cid) VALUES (?, ?, ?, ?)
Logic SQL: INSERT INTO course ( cname,user_id,cstatus ) VALUES ( ?,?,?)
Actual SQL: m0 ::: INSERT INTO course_2 ( cname,user_id,cstatus , cid) VALUES (?, ?, ?, ?)
......
从这个日志中我们可以看到,程序中执行的Logic SQL经过ShardingJDBC处理后,被转换成了Actual SQL往数据库里执行。执行的结果可以在MySQL中看到,course_1和course_2两个表中各插入了五条消息。这就是ShardingJDBC帮我们进行的数据库的分库分表操作。
【分析】现在定义的course其实是一个逻辑表,我们使用courseMapper执行sql的时候其实首先是先向逻辑表中插入,然后经过ShardingSphere的转化(转化规则使我们在配置文件中配置的),就会按照分片规则向真实表中插入数据!
然后,其他的几个配置文件依次对应了其他几种分库分表策略,我们可以一一演示一下。
注意,为了看得清楚,我们每次演示之前都清除之前表中的数据!
3.2 分库分表示例配置
在项目中对应的配置文件名称:application02-split-DS-and-tables.properties
【注意】:这里的分库分表概念, 可以是在同一个mysql服务上的两个不同名称的数据库中, 也可以是两个不同的mysql服务上。
我们在数据库ShardingJdbcDemo和ShardingJdbcDemo2上分别创建cours_1和course_2两张表:
# ======= 分库分表配置: 这里的分库概念, 可以是在同一个mysql服务上的两个不同名称的数据库中, 也可以是两个不同的mysql服务上============
# 配置多个数据源:
spring.shardingsphere.datasource.names=m1,m2
# 配置数据源1:m1, 这里数据库名称为ShardingJdbcDemo
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://docker.host.a:3306/ShardingJdbcDemo?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.m1.username=root
spring.shardingsphere.datasource.m1.password=123456
# 配置数据源2:m2, 这里数据库名称为ShardingJdbcDemo2
spring.shardingsphere.datasource.m2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m2.url=jdbc:mysql://docker.host.a:3306/ShardingJdbcDemo2?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.m2.username=root
spring.shardingsphere.datasource.m2.password=123456
# ===> 配置逻辑表course的真实表分布,分库,分表
# '$->{1..2}':轮训1和2; 即现在总共有四种可能:m1.course_1, m1.course_2, m2.course_1, m2.course_2,
spring.shardingsphere.sharding.tables.course.actual-data-nodes=m$->{1..2}.course_$->{1..2}
# ===>指定表的主键及生成策略
# 指定主键列
spring.shardingsphere.sharding.tables.course.key-generator.column=cid
# 设置主键生成策略:支持SNOWFLAKE和UUID
spring.shardingsphere.sharding.tables.course.key-generator.type=SNOWFLAKE
# 雪花算法的一个可选参数
spring.shardingsphere.sharding.tables.course.key-generator.props.worker.id=1
# <======================== 数据表分片策略 (PS: inline这种方式不支持范围查询) ==========================================>
# ===> 【inline策略】设置数据表的inline分片策略(course_1, course_2)
# 指定分片键
spring.shardingsphere.sharding.tables.course.table-strategy.inline.sharding-column=cid
# 指定算法表达式: course_$->{cid%2+1} cid对2取余后加1(结果只可能为1或者2, 即以为这会插入到course_1或者course_2中)
spring.shardingsphere.sharding.tables.course.table-strategy.inline.algorithm-expression=course_$->{cid%2+1}
# <======================== 数据源分片策略 (PS: inline这种方式不支持范围查询) ==========================================>
# ===> 【inline策略】指定数据源的inline分片策略(m1, m2 )
spring.shardingsphere.sharding.tables.course.database-strategy.inline.sharding-column=cid
spring.shardingsphere.sharding.tables.course.database-strategy.inline.algorithm-expression=m$->{cid%2+1}
# ===> 配置显示sql
spring.shardingsphere.props.sql.show = true
spring.main.allow-bean-definition-overriding=true
配置完成后我们继续执行上面的添加course的测试方法,然后来看看结果:
经过测试我们发现,此时的数据只是分布在了ShardingJdbcDemo.source_1和ShardingJdbcDemo2.source_2中,这根本不符合我们的预期。比较理想的结果是这10条数据均匀的分布在4张表中!
现在是基于分库分表的配置,我们来分析一下Sharding JDBC是如何处理SQL的。
3.2.1 全部查询SQL分析
那我们再来演示一下查询操作,我们来查询所有的course,看看是否会返回十条记录呢?
@Test
public void queryCourse(){
// select * from course
List<Course> courses = courseMapper.selectList(null);
courses.forEach(System.out::println);
}
发现ShardingJDBC已经帮我们做好了合并的操作了,我们不用自己查询合并了!!!
【注意】由于我进行了多次测试,所以可能对应的CID不一致,但是都是在同样的配置下进行的测试。
我们可以看一下逻辑sql和真实sql:
Logic SQL: SELECT cid,cname,user_id,cstatus FROM course
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_1
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_2
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_1
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_2
发现查询逻辑表中的所有数据,其实是分别查询了所有真实表中的内容然后拼接在一起了!
3.1.2 条件查询SQL分析
那我们按照分片键查询呢?我们也来看看真实的sql:
@Test
public void queryCourse(){
// select * from course
QueryWrapper<Course> wrapper = new QueryWrapper<>();
wrapper.eq("cid", 645951091040587777L);
List<Course> courses = courseMapper.selectList(wrapper);
courses.forEach(System.out::println);
}
Logic SQL: SELECT cid,cname,user_id,cstatus FROM course WHERE cid = ?
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_2 WHERE cid = ?
发现根据分片键cid查询的时候只会执行一次SQL。因为会行同样的分表策略,然后直接去那张表中查询!
那如果根据其他字段查询的,比如user_id。
@Test
public void queryCourse(){
// select * from course
QueryWrapper<Course> wrapper = new QueryWrapper<>();
wrapper.eq("user_id",1009L);
List<Course> courses = courseMapper.selectList(wrapper);
courses.forEach(System.out::println);
}
Logic SQL: SELECT cid,cname,user_id,cstatus FROM course WHERE user_id = ?
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_1 WHERE user_id = ?
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_2 WHERE user_id = ?
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_1 WHERE user_id = ?
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_2 WHERE user_id = ?
发现当根据不是分片键的字段查询的时候,就需要分别查询所有的数据表,然后再讲结果拼装到一起!
3.1.3 修改SQL分析
略,结果一致。
3.1.4 删除SQL分析
首先我们来测试根据分片键的删除:
@Test
public void deleteByCid(){
// 第十条记录(第二个数据库中的最后一条记录)的cid
Long cid = 645951091241914369L;
HashMap<String, Object> map = new HashMap<>(16);
map.put("cid", cid);
int result = courseMapper.deleteByMap(map);
System.out.println(result);
}
Logic SQL: DELETE FROM course WHERE cid = ?
Actual SQL: m2 ::: DELETE FROM course_2 WHERE cid = ?
根据分片键删除数据,首先会根据分片策略计算出记录所在的数据库和数据表,所以只需要执行一条真实的sql。
接下来我们来测试根据其他属性删除,根据user_id来实删除:
@Test
public void deleteByUserId(){
// 第9条记录的userId
Long userId = 1007L;
HashMap<String, Object> map = new HashMap<>(16);
map.put("user_id", userId);
int result = courseMapper.deleteByMap(map);
System.out.println(result);
}
Logic SQL: DELETE FROM course WHERE user_id = ?
Actual SQL: m1 ::: DELETE FROM course_1 WHERE user_id = ?
Actual SQL: m1 ::: DELETE FROM course_2 WHERE user_id = ?
Actual SQL: m2 ::: DELETE FROM course_1 WHERE user_id = ?
Actual SQL: m2 ::: DELETE FROM course_2 WHERE user_id = ?
按照非分片键字段删除记录,需要逐条扫描,执行多条sql。
3.1.5 排序SQL分析
我们首先测试按照分片键cid排序:
@Test
public void queryCourseOrderBy(){
QueryWrapper<Course> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("cid");
List<Course> courses = courseMapper.selectList(wrapper);
courses.forEach(System.out::println);
}
Logic SQL: SELECT cid,cname,user_id,cstatus FROM course ORDER BY cid DESC
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_1 ORDER BY cid DESC
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_2 ORDER BY cid DESC
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_1 ORDER BY cid DESC
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_2 ORDER BY cid DESC
---
Course{cid=645951091187388416, cname='shardingsphere', userId=1008, cstatus='1'}
Course{cid=645951091099308032, cname='shardingsphere', userId=1006, cstatus='1'}
Course{cid=645951091040587777, cname='shardingsphere', userId=1005, cstatus='1'}
Course{cid=645951091007033344, cname='shardingsphere', userId=1004, cstatus='1'}
Course{cid=645951090965090305, cname='shardingsphere', userId=1003, cstatus='1'}
Course{cid=645951090923147264, cname='shardingsphere', userId=1002, cstatus='1'}
Course{cid=645951090772152321, cname='shardingsphere', userId=1001, cstatus='1'}
Course{cid=645951089950068736, cname='shardingsphere', userId=1000, cstatus='1'}
再来根据非分片键字段user_id的排序:
@Test
public void queryCourseOrderBy(){
QueryWrapper<Course> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("user_id");
List<Course> courses = courseMapper.selectList(wrapper);
courses.forEach(System.out::println);
}
Logic SQL: SELECT cid,cname,user_id,cstatus FROM course ORDER BY user_id DESC
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_1 ORDER BY user_id DESC
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_2 ORDER BY user_id DESC
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_1 ORDER BY user_id DESC
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_2 ORDER BY user_id DESC
--
Course{cid=645951091187388416, cname='shardingsphere', userId=1008, cstatus='1'}
Course{cid=645951091099308032, cname='shardingsphere', userId=1006, cstatus='1'}
Course{cid=645951091040587777, cname='shardingsphere', userId=1005, cstatus='1'}
Course{cid=645951091007033344, cname='shardingsphere', userId=1004, cstatus='1'}
Course{cid=645951090965090305, cname='shardingsphere', userId=1003, cstatus='1'}
Course{cid=645951090923147264, cname='shardingsphere', userId=1002, cstatus='1'}
Course{cid=645951090772152321, cname='shardingsphere', userId=1001, cstatus='1'}
Course{cid=645951089950068736, cname='shardingsphere', userId=1000, cstatus='1'}
发现排序的时候,不论是否是分片键,都是需要先按照殊勋分别查询出各个表中的数据,然后再重新针对全量数据进行排序。
3.1.5 范围查询Sql分析(standard算法使用实例)
我们从第一条记录查询到最后一条记录吧,现在是跨库跨表了。
@Test
public void queryOrderRange(){
QueryWrapper<Course> wrapper = new QueryWrapper<>();
wrapper.between("cid", 645951090772152321L, 645951091187388416L);
List<Course> courses = courseMapper.selectList(wrapper);
courses.forEach(course -> System.out.println(course));
}
inline这种策略不支持范围查询… 所以我们需要使用其他的分片策略。
更多的分片算法,可以参考下一内容:4、ShardingJDBC的分片算法
对于范围查询,我们可以使用standard策略。修改配置文件中的inline分片策略为如下的standard策略:
# <======================== 数据表分片策略 (PS: inline这种方式不支持范围查询) ==========================================>
# ===> 【inline策略】设置数据表的inline分片策略(course_1, course_2)
# 指定分片键
#spring.shardingsphere.sharding.tables.course.table-strategy.inline.sharding-column=cid
# 指定算法表达式: course_$->{cid%2+1} cid对2取余后加1(结果只可能为1或者2, 即以为这会插入到course_1或者course_2中)
#spring.shardingsphere.sharding.tables.course.table-strategy.inline.algorithm-expression=course_$->{cid%2+1}
# <======================== 数据源分片策略 (PS: inline这种方式不支持范围查询) ==========================================>
# ===> 【inline策略】指定数据源的inline分片策略(m1, m2 )
#spring.shardingsphere.sharding.tables.course.database-strategy.inline.sharding-column=cid
#spring.shardingsphere.sharding.tables.course.database-strategy.inline.algorithm-expression=m$->{cid%2+1}
# <======================== 数据表分片策略二: standard策略 (PS: 需要定制精细和范围查询算法类!) ==========================================>
# ===> 【standard 数据表分片策略】包含定制的精细算法类和范围算法类
# 指定分片键
spring.shardingsphere.sharding.tables.course.table-strategy.standard.sharding-column=cid
# 指定数据表分片精精确算法'precise-algorithm-class-name'类(这里不能再是一个表达式了, 而是需要我们指定一个范围算法类)
spring.shardingsphere.sharding.tables.course.table-strategy.standard.precise-algorithm-class-name=com.jihu.sharding.algorithm.MyPreciseTableShardingAlgorithm
# 指定数据表分片范围算法'range-algorithm-class-name'类(这里不能再是一个表达式了, 而是需要我们指定一个范围算法类)
spring.shardingsphere.sharding.tables.course.table-strategy.standard.range-algorithm-class-name=com.jihu.sharding.algorithm.MyRangeTableShardingAlgorithm
# <======================== 数据源分片策略二: standard策略 (PS: 需要定制精细和范围查询算法类!) ==========================================>
# ===> 【standard 数据源分片策略】包含定制的精细算法类和范围算法类
# 指定分片键
spring.shardingsphere.sharding.tables.course.database-strategy.standard.sharding-column=cid
# 指定数据源分片精确算法
spring.shardingsphere.sharding.tables.course.database-strategy.standard.precise-algorithm-class-name=com.jihu.sharding.algorithm.MyPreciseDSShardingAlgorithm
# 指定数据源分片范围算法
spring.shardingsphere.sharding.tables.course.database-strategy.standard.range-algorithm-class-name=com.jihu.sharding.algorithm.MyRangeDSShardingAlgorithm
MyPreciseTableShardingAlgorithm 数据表精确分片算法类定义如下:
/**
* standard策略之自定义精确分表算法类
*
* 执行比如 select * from course where cid = ? or cid in (?,?)
*/
public class MyPreciseTableShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
/**
* 精确分表方法
* @param availableTargetNames 所有的真实表名称集合
* @param shardingValue 过滤条件
* @return 需要查询的数据表名
*/
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
// 获取逻辑表名称
String logicTableName = shardingValue.getLogicTableName();
// 获取分片键 / 过滤字段名称
String cid = shardingValue.getColumnName();
// 分片键 / 过滤字段的值
Long cidValue = shardingValue.getValue();
//实现算法思路还是这个表达式: course_$->{cid%2+1)
BigInteger shardingValueB = BigInteger.valueOf(cidValue);
// 实现cid%2+1逻辑, 返回计算后得到的真实表名称的数字: 1或者2
BigInteger resB = (shardingValueB.mod(new BigInteger("2"))).add(new BigInteger("1"));
// 拼接表名称: course_1或者course_2
String key = logicTableName+"_"+resB;
// 判断表名称是否合法并返回
if(availableTargetNames.contains(key)){
return key;
}
//course_1, course_2
throw new UnsupportedOperationException("route "+ key +" is not supported ,please check your config");
}
}
MyRangeTableShardingAlgorithm 数据表范围分片算法类定义如下:
/**
* standard策略之自定义范围分表算法类
*
* 执行比如 select * from course where cid between ? and ?
*/
public class MyRangeTableShardingAlgorithm implements RangeShardingAlgorithm<Long> {
/**
* 范围分表方法
* @param availableTargetNames 所有的真实表名称集合
* @param shardingValue 上限和下限过滤条件
* @return 需要查询的数据表名集合
*/
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) {
//select * from course where cid between 1 and 100;
// 获取上限
Long upperVal = shardingValue.getValueRange().upperEndpoint();//100
// 获取下限
Long lowerVal = shardingValue.getValueRange().lowerEndpoint();//1
// 获取逻辑表名称
String logicTableName = shardingValue.getLogicTableName();
// 因为范围查询, 我们也不确定数据分布在那些表中, 所以要返回全部的表名称
return Arrays.asList(logicTableName+"_1",logicTableName+"_2");
}
}
MyPreciseDSShardingAlgorithm 自定义数据源精确分片算法类:
/**
* standard策略之自定义数据源精确分片算法类
*
* 执行比如 select * from course where cid = ? or cid in (?,?)
*/
public class MyPreciseDSShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
/**
* 数据源精确分片方法
* @param availableTargetNames 所有的真实数据源名称集合
* @param shardingValue 过滤条件
* @return 需要查询的数据源名称
*/
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
// 获取逻辑表名称
String logicTableName = shardingValue.getLogicTableName();
// 获取分片键 / 过滤字段名称
String cid = shardingValue.getColumnName();
// 分片键 / 过滤字段的值
Long cidValue = shardingValue.getValue();
//实现算法思路还是这个表达式: m_$->{cid%2+1)
BigInteger shardingValueB = BigInteger.valueOf(cidValue);
// 实现cid%2+1逻辑, 返回计算后得到的真实表名称的数字: 1或者2
BigInteger resB = (shardingValueB.mod(new BigInteger("2"))).add(new BigInteger("1"));
// 拼接数据源名称: m_1或者m_2
String key = "m" + resB;
// 判断数据源名称是否合法并返回
if(availableTargetNames.contains(key)){
return key;
}
// m1, m2
throw new UnsupportedOperationException("route "+ key +" is not supported ,please check your config");
}
}
MyRangeDSShardingAlgorithm 自定义数据源精确分片算法类:
/**
* standard策略之自定义数据源范围算法类
* <p>
* 执行比如 select * from course where cid between ? and ?
*/
public class MyRangeDSShardingAlgorithm implements RangeShardingAlgorithm<Long> {
/**
* 数据源范围分片方法
*
* @param availableTargetNames 所有的真实数据源名称集合
* @param shardingValue 过滤条件
* @return 需要查询的数据源名称
*/
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) {
//select * from course where cid between 1 and 100;
Long upperVal = shardingValue.getValueRange().upperEndpoint();//100
Long lowerVal = shardingValue.getValueRange().lowerEndpoint();//1
String logicTableName = shardingValue.getLogicTableName();
// 不确定范围数据分布在哪些数据源中, 所以返回全部的数据源信息
return Arrays.asList("m1", "m2");
}
}
配置完成后,我们再来测试一下上面的范围查询,发现已经可以正常查询了!
Logic SQL: SELECT cid,cname,user_id,cstatus FROM course WHERE cid BETWEEN ? AND ?
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_1 WHERE cid BETWEEN ? AND ?
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_2 WHERE cid BETWEEN ? AND ?
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_1 WHERE cid BETWEEN ? AND ?
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_2 WHERE cid BETWEEN ? AND ?
3.1.6 范围加过滤查询Sql分析(complex算法使用实例)
我们加下来再来测试一下复杂查询,再上面范围查询的基础上再根据user_id字段进行过滤。
@Test
public void queryCourseComplex(){
QueryWrapper<Course> wrapper = new QueryWrapper<>();
wrapper.between("cid",645951089950068736L,645951091149639681L);
wrapper.eq("user_id",1004L);
// wrapper.in()
List<Course> courses = courseMapper.selectList(wrapper);
courses.forEach(System.out::println);
}
Logic SQL: SELECT cid,cname,user_id,cstatus FROM course WHERE cid BETWEEN ? AND ? AND user_id = ?
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_1 WHERE cid BETWEEN ? AND ? AND user_id = ?
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_2 WHERE cid BETWEEN ? AND ? AND user_id = ?
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_1 WHERE cid BETWEEN ? AND ? AND user_id = ?
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_2 WHERE cid BETWEEN ? AND ? AND user_id = ?
Course{cid=645951091007033344, cname='shardingsphere', userId=1004, cstatus='1'}
结果查出来了,好像没什么问题呀?
但是我们仔细来查询真实执行的sql,发现依然是执行了四次!
user_id也是按照奇偶数存的,能不能像处理cid一样。查询之前先算出来这个user_id分布在哪个数据源的哪个数据表中,这样岂不是这用执行一次sql?
那我们能定制这样的查询算法吗?
其实我们可以使用complex算法实现多字段查询问题。
将分片策略修改为如下配置:
# <======================== 数据表分片策略三: complex策略 (PS: 支持多个分片键, 需要定制算法类!) ==========================================>
# ===> 【complex 数据表分片策略】
# 指定分片键, 可以为多个
spring.shardingsphere.sharding.tables.course.table-strategy.complex.sharding-columns=cid, user_id
# 指定数据表分片算法类
spring.shardingsphere.sharding.tables.course.table-strategy.complex.algorithm-class-name=com.jihu.sharding.algorithm.MyComplexTableShardingAlgorithm
# <======================== 数据源分片策略三: complex策略 (PS: 支持多个分片键, 需要定制算法类!) ==========================================>
# ===> 【complex 数据源分片策略】类
# 指定分片键, 可以为多个
spring.shardingsphere.sharding.tables.course.database-strategy.complex.sharding-columns=cid, user_id
# 指定数据源分片算法类
spring.shardingsphere.sharding.tables.course.database-strategy.complex.algorithm-class-name=com.jihu.sharding.algorithm.MyComplexDSShardingAlgorithm
MyComplexTableShardingAlgorithm 数据表复杂分片算法类:
/**
* 数据表复杂分片算法类
*/
public class MyComplexTableShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {
/**
* 分片
*
* @param availableTargetNames 所有可用的真实数据表名称集合
* @param shardingValue 过滤条件
* @return 要查询的数据表名集合(支持=或者in)
*/
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Long> shardingValue) {
// sql: SELECT cid,cname,user_id,cstatus FROM course WHERE cid BETWEEN ? AND ? AND user_id = ?
// sql: SELECT cid,cname,user_id,cstatus FROM course WHERE cid BETWEEN ? AND ? AND user_id in (?,?)
// 获取范围字段cid
Range<Long> cidRange = shardingValue.getColumnNameAndRangeValuesMap().get("cid");
// 获取精确过滤字段user_id集合
Collection<Long> userIdCol = shardingValue.getColumnNameAndShardingValuesMap().get("user_id");
// 获得上限和下限
Long upperVal = cidRange.upperEndpoint();
Long lowerVal = cidRange.lowerEndpoint();
List<String> res = new ArrayList<>();
for (Long userId : userIdCol) {
// 实现course_{userID%2+1}逻辑
BigInteger userIdB = BigInteger.valueOf(userId);
// 根据算法确定user_id所在的表!!! (再结合数据源的算法, 就可以定位出这行记录所在的位置)
BigInteger target = (userIdB.mod(new BigInteger("2"))).add(new BigInteger("1"));
// 返回course_1 / course_2 / course_1,course_2
res.add(shardingValue.getLogicTableName() + "_" + target);
}
return res;
}
}
MyComplexDSShardingAlgorithm 数据源复杂分片算法类:
/**
* 数据源复杂分片算法类
*/
public class MyComplexDSShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {
/**
* 分片
*
* @param availableTargetNames 所有可用的真实数据源名称集合
* @param shardingValue 过滤条件
* @return 要查询的数据源名集合(支持=或者in)
*/
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Long> shardingValue) {
// sql: SELECT cid,cname,user_id,cstatus FROM course WHERE cid BETWEEN ? AND ? AND user_id = ?
// sql: SELECT cid,cname,user_id,cstatus FROM course WHERE cid BETWEEN ? AND ? AND user_id in (?,?)
// 获取范围字段cid
Range<Long> cidRange = shardingValue.getColumnNameAndRangeValuesMap().get("cid");
// 获取精确过滤字段user_id集合
Collection<Long> userIdCol = shardingValue.getColumnNameAndShardingValuesMap().get("user_id");
// 获得上限和下限
Long upperVal = cidRange.upperEndpoint();
Long lowerVal = cidRange.lowerEndpoint();
List<String> res = new ArrayList<>();
for (Long userId : userIdCol) {
// 实现m{userID%2+1}逻辑
BigInteger userIdB = BigInteger.valueOf(userId);
// 根据算法确定user_id所在的数据源
BigInteger target = (userIdB.mod(new BigInteger("2"))).add(new BigInteger("1"));
// 返回course_1 / course_2 / course_1,course_2
res.add("m" + target);
}
return res;
}
}
配置完成后我们再来执行一下上面的查询测试类,来分析sql执行情况(记得注释掉其他的分片策略!):
ogic SQL: SELECT cid,cname,user_id,cstatus FROM course WHERE cid BETWEEN ? AND ? AND user_id = ?
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_1 WHERE cid BETWEEN ? AND ? AND user_id = ?
Course{cid=645951091007033344, cname='shardingsphere', userId=1004, cstatus='1'}
这次也查询出预期的结果了,而且只执行了一次真实的sql,这边全部扫描的性能要好很多!!!
3.1.7 特定表、特定数据源查询Sql分析(hint算法使用实例)
比如说我们有这样的需求,我们只想查询course_2表中的记录,该怎么实现呢?自己写条件不是很好写…
这时候就可以使用hint算法。Hint算法呢,分片不再和sql绑定了,而是我们可以指定其他的查询规则。
我们先来修改分片规则,使用如下配置:
【注意】: 数据源分片策略和数据表分片策略是可以分开配置的,即不一定要使用同种分片策略,可以根据需求搭配使用!
# <======================== 数据源分片策略三: complex策略 (PS: 支持多个分片键, 需要定制算法类!) ==========================================>
# ===> 【complex 数据源分片策略】类
# 指定分片键, 可以为多个
spring.shardingsphere.sharding.tables.course.database-strategy.complex.sharding-columns=cid, user_id
# 指定数据源分片算法类
spring.shardingsphere.sharding.tables.course.database-strategy.complex.algorithm-class-name=com.jihu.sharding.algorithm.MyComplexDSShardingAlgorithm
# <======================== 数据表分片策略四: hint策略 (PS: 需要定制算法类!) ==========================================>
spring.shardingsphere.sharding.tables.course.table-strategy.hint.algorithm-class-name=com.jihu.sharding.algorithm.MyHintTableShardingAlgorithm
MyHintTableShardingAlgorithm 数据表hint分片算法类:
/**
* 数据表hint分片算法类
*/
public class MyHintTableShardingAlgorithm implements HintShardingAlgorithm<Integer> {
/**
* 数据表hint分片算法类
* @param availableTargetNames 所有真实数据表名称集合
* @param shardingValue 数据表过滤条件
* @return
*/
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, HintShardingValue<Integer> shardingValue) {
// 拼装要查询的数据表名称(我们是想查询course_2表中的记录)
String key = shardingValue.getLogicTableName() + "_" + shardingValue.getValues().toArray()[0];
if(availableTargetNames.contains(key)){
return Collections.singletonList(key);
}
throw new UnsupportedOperationException("route "+ key +" is not supported ,please check your config");
}
}
测试方法:
@Test
public void queryCourseByHint(){
HintManager hintManager = HintManager.getInstance();
hintManager.addTableShardingValue("course",2);
List<Course> courses = courseMapper.selectList(null);
courses.forEach(System.out::println);
hintManager.close();
}
我们来看看执行的Sql:
Logic SQL: SELECT cid,cname,user_id,cstatus FROM course
Actual SQL: m1 ::: SELECT cid,cname,user_id,cstatus FROM course_2
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_2
Course{cid=645951090772152321, cname='shardingsphere', userId=1001, cstatus='1'}
Course{cid=645951090965090305, cname='shardingsphere', userId=1003, cstatus='1'}
Course{cid=645951091040587777, cname='shardingsphere', userId=1005, cstatus='1'}
可以看到,真实sql查询了所有数据源中的course_2表的记录。
那如果我们只想查询数据源m2中的course_表呢?
我们使用如下的hint分片策略配置:
# <======================== 数据表分片策略四: hint策略 (PS: 只需要定制算法类!) ==========================================>
spring.shardingsphere.sharding.tables.course.table-strategy.hint.algorithm-class-name=com.jihu.sharding.algorithm.MyHintTableShardingAlgorithm
# <======================== 数据源分片策略四: hint策略 (PS: 只需要定制算法类!) ==========================================>
spring.shardingsphere.sharding.tables.course.database-strategy.hint.algorithm-class-name=com.jihu.sharding.algorithm.MyHintDSShardingAlgorithm
在算法类中我们指定好了要查询的数据表和数据源过滤算法:
MyComplexDSShardingAlgorithm 数据源hint分片算法类:
/**
* 数据源hint分片算法类
*/
public class MyHintDSShardingAlgorithm implements HintShardingAlgorithm<Integer> {
/**
* 数据源hint分片算法类
* @param availableTargetNames 所有真实数据源名称集合
* @param shardingValue 数据源过滤条件
* @return
*/
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, HintShardingValue<Integer> shardingValue) {
// 拼装要查询的数据源名称
String key = "m" + shardingValue.getValues().toArray()[0];
if(availableTargetNames.contains(key)){
return Collections.singletonList(key);
}
throw new UnsupportedOperationException("route "+ key +" is not supported ,please check your config");
}
}
执行测试方法:
@Test
public void queryCourseByHint(){
HintManager hintManager = HintManager.getInstance();
// 设置数据表条件
hintManager.addTableShardingValue("course",2);
// 只查询数据源m2
hintManager.addDatabaseShardingValue("course", 2);
List<Course> courses = courseMapper.selectList(null);
courses.forEach(System.out::println);
hintManager.close();
}
来看执行sql和结果:
Logic SQL: SELECT cid,cname,user_id,cstatus FROM course
Actual SQL: m2 ::: SELECT cid,cname,user_id,cstatus FROM course_2
Course{cid=645951090772152321, cname='shardingsphere', userId=1001, cstatus='1'}
Course{cid=645951090965090305, cname='shardingsphere', userId=1003, cstatus='1'}
Course{cid=645951091040587777, cname='shardingsphere', userId=1005, cstatus='1'}
结果是一样的,因为m1中的course_2表中没有记录。但是指定的Sql只查询了m2中的course_2表。
3.1.8 Sql分析总结
不论是增删改查,和分片键相关的操作,在执行之前都会尽可能根据分片策略先确定出记录所在的库和所在的表,然后只需要执行一条真实sql即可。
而对于非分片键的操作,往往需要多数据库、多表进行扫描,直至完成操作,是比较耗费资源和性能的。
数据库和数据源的分片策略是可以搭配使用的。
3.3 广播表示例配置
在项目中对应的配置文件名称:application03-broadcast-tables.properties
广播表: 也叫公共表,指素有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中都完全一致。例如字典表。
我们之前测试的课程表可以分布在不同的数据库中,但是表示课程状态的字段表每一个数据库中都应该维护一份。这种表示不能分片的,因为数据会不全!
接下来我们来配置广播表。广播表即要求所有的库里面都保留全量,删除、修改或者新增的时候,所有的库中都要同步进行。
我们在数据库ShardingJdbcDemo和ShardingJdbcDemo2中都是建立t_dict表。
然后我们来配置:
# ================== 广播表配置 ==========================
# 配置数据源名称
spring.shardingsphere.datasource.names=m1,m2
# 配置数据源1:m1, 这里数据库名称为ShardingJdbcDemo
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://docker.host.a:3306/ShardingJdbcDemo?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.m1.username=root
spring.shardingsphere.datasource.m1.password=123456
# 配置数据源2:m2, 这里数据库名称为ShardingJdbcDemo2
spring.shardingsphere.datasource.m2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m2.url=jdbc:mysql://docker.host.a:3306/ShardingJdbcDemo2?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.m2.username=root
spring.shardingsphere.datasource.m2.password=123456
# 配置广播表
spring.shardingsphere.sharding.broadcast-tables=t_dict
# 配置虚拟表t_dict主键列
spring.shardingsphere.sharding.tables.t_dict.key-generator.column=dict_id
# 配置虚拟表t_dict主键生成策略
spring.shardingsphere.sharding.tables.t_dict.key-generator.type=SNOWFLAKE
# ===> 配置显示sql
spring.shardingsphere.props.sql.show = true
spring.main.allow-bean-definition-overriding=true
测试方法:
@Test
public void addDict(){
Dict d1 = new Dict();
d1.setUstatus("1");
d1.setUvalue("正常");
dictMapper.insert(d1);
Dict d2 = new Dict();
d2.setUstatus("0");
d2.setUvalue("不正常");
dictMapper.insert(d2);
}
可以看到,我们指定新增操作后,两个库中的t_dict表都新增了同样的数据。
Logic SQL: INSERT INTO t_dict ( ustatus,uvalue ) VALUES ( ?, ? )
Actual SQL: m1 ::: INSERT INTO t_dict ( ustatus, uvalue , dict_id) VALUES (?, ?, ?) ::: [1, 正常, 646117958581485568]
Actual SQL: m2 ::: INSERT INTO t_dict ( ustatus, uvalue , dict_id) VALUES (?, ?, ?) ::: [1, 正常, 646117958581485568]
Logic SQL: INSERT INTO t_dict ( ustatus, uvalue ) VALUES ( ?,? )
Actual SQL: m1 ::: INSERT INTO t_dict ( ustatus, uvalue , dict_id) VALUES (?, ?, ?) ::: [0, 不正常, 646117959361626113]
Actual SQL: m2 ::: INSERT INTO t_dict ( ustatus, uvalue , dict_id) VALUES (?, ?, ?) ::: [0, 不正常, 646117959361626113]
3.4 绑定表示例配置(left join)
在项目中对应的配置文件名称:application04-bind-tables.properties
绑定表:分片规则一致的主表和子表。
比如我们现在有这样的场景,有一张t_user虚拟表,真实表t_user_1和t_user_2分布在ShardingJdbcDemo数据库中;
而t_user表中有一个字段ustatus表示用户的状态id,所以我们还有一张表虚拟表t_ustatus表用来表示用户的状态信息。t_ustatus对应的真实表t_utatus_1和t_ustatus_2分布在ShardingJdbcDemo数据库中。
现在我们的需求是查询所有用户以及用户的具体状态,所以就需要使用user表关联ustatus表查询:
@Select("select u.user_id,u.username,us.uvalue ustatus from user u left join ustatus us on u.ustatus = us.ustatus")
我们来配置一下,使用如下的配置文件:
# ================== 绑定表配置 ==========================
# 配置数据源名称
spring.shardingsphere.datasource.names=m1
# 配置数据源1:m1, 这里数据库名称为ShardingJdbcDemo
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://docker.host.a:3306/ShardingJdbcDemo?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.m1.username=root
spring.shardingsphere.datasource.m1.password=123456
# 配置虚拟表ustatus对应的真实表名称
spring.shardingsphere.sharding.tables.ustatus.actual-data-nodes=m1.t_ustatus_$->{1..2}
# 配置虚拟表ustatus主键列
spring.shardingsphere.sharding.tables.ustatus.key-generator.column=ustatus_id
# 配置虚拟表ustatus主键生成策略
spring.shardingsphere.sharding.tables.ustatus.key-generator.type=SNOWFLAKE
# 配置雪花算法额外参数
spring.shardingsphere.sharding.tables.ustatus.key-generator.props.worker.id=1
# ===> 配置inline分片策略
spring.shardingsphere.sharding.tables.ustatus.table-strategy.inline.sharding-column=ustatus
spring.shardingsphere.sharding.tables.ustatus.table-strategy.inline.algorithm-expression=t_ustatus_$->{ustatus.toInteger()%2+1}
# 配置虚拟表user对应的真实表名称
spring.shardingsphere.sharding.tables.user.actual-data-nodes=m1.t_user_$->{1..2}
# 配置虚拟表user键列
spring.shardingsphere.sharding.tables.user.key-generator.column=user_id
# 配置虚拟表user主键生成策略
spring.shardingsphere.sharding.tables.user.key-generator.type=SNOWFLAKE
# 配置雪花算法额外参数
spring.shardingsphere.sharding.tables.user.key-generator.props.worker.id=1
# ===> 配置inline分片策略
spring.shardingsphere.sharding.tables.user.table-strategy.inline.sharding-column=ustatus
spring.shardingsphere.sharding.tables.user.table-strategy.inline.algorithm-expression=t_user_$->{ustatus.toInteger()%2+1}
# ===> 配置显示sql
spring.shardingsphere.props.sql.show = true
spring.main.allow-bean-definition-overriding=true
我们执行如下的测试方法来添加数据:
@Test
public void addUstatus() {
Ustatus u1 = new Ustatus();
u1.setUstatus("1");
u1.setUvalue("正常");
ustatustMapper.insert(u1);
Ustatus u2 = new Ustatus();
u2.setUstatus("0");
u2.setUvalue("不正常");
ustatustMapper.insert(u2);
for (int i = 0; i < 10; i++) {
User user = new User();
user.setUsername("user No " + i);
user.setUstatus("" + (i % 2));
user.setUage(i * 10);
userMapper.insert(user);
}
}
数据添加成功后,我们执行如下的测试方法查询结果,也就是会执行上面列出的那个left join的sql:
@Test
public void queryUserStatus() {
List<User> users = userMapper.queryUserStatus();
users.forEach(System.out::println);
}
Logic SQL: select u.user_id,u.username,us.uvalue ustatus from user u left join ustatus us on u.ustatus = us.ustatus
Actual SQL: m1 ::: select u.user_id,u.username,us.uvalue ustatus from t_user_2 u left join t_ustatus_2 us on u.ustatus = us.ustatus
Actual SQL: m1 ::: select u.user_id,u.username,us.uvalue ustatus from t_user_2 u left join t_ustatus_1 us on u.ustatus = us.ustatus
Actual SQL: m1 ::: select u.user_id,u.username,us.uvalue ustatus from t_user_1 u left join t_ustatus_2 us on u.ustatus = us.ustatus
Actual SQL: m1 ::: select u.user_id,u.username,us.uvalue ustatus from t_user_1 u left join t_ustatus_1 us on u.ustatus = us.ustatus
我们只插入了10条数据,但是从执行的真实Sql和查询结果来看,是查询了20次,而且有10条的ustatus是null。这如果是做业务,肯定是一个严重的bug呀!!!那么我们该如何处理呢?
其实这个时候,我们就可以使用绑定表配置了。绑定表就是分片策略相同的主表和字表,我们的逻辑表user和ustatus分片规则都是按照ustatus字段,所以是相同的。
我们在上面的配置文件基础上,添加如下的绑定关系:
# ===> 绑定表配置. 特定表: 分片规则一致的主表和子表. 我们这里逻辑表user和ustatus的分片规则就是一致的!!!
# binding-tables是一个集合, 我们实际业务中可能有多个, 也可以配置多个绑定表binding-tables[1]...
spring.shardingsphere.sharding.binding-tables[0]=user,ustatus
此时再运行查询方法:
Logic SQL: select u.user_id,u.username,us.uvalue ustatus from user u left join ustatus us on u.ustatus = us.ustatus
Actual SQL: m1 ::: select u.user_id,u.username,us.uvalue ustatus from t_user_1 u left join t_ustatus_1 us on u.ustatus = us.ustatus
Actual SQL: m1 ::: select u.user_id,u.username,us.uvalue ustatus from t_user_2 u left join t_ustatus_2 us on u.ustatus = us.ustatus
可以看到,配置了绑定表关系之后的查询就复合我们的预期了!!!
3.5 读写分离示例配置
在项目中对应的配置文件名称:application05-read-write-separation.properties
读写主从复制、分离配置实战请参考文章:https://blog.csdn.net/qq_43631716/article/details/120260325
我这里已经配置好了,3306是主服务,3307是从服务,并且配置了它们只会同步masterdemo这个数据库的数据!
所以,我们就在masterdemo这个数据库中完成测试。
首先我们测试一下主从复制是否正常工作。我们将主从中的t_num数据都清楚掉。然后重新向主库中插入一条记录,发现从库已经正常同步数据了!
我们在两个数据库中分贝创建如下数据表,然后以该表来测试读写分离。
CREATE TABLE `t_dict` (
`dict_id` bigint(0) PRIMARY KEY NOT NULL,
`ustatus` varchar(100) NOT NULL,
`uvalue` varchar(100) NOT NULL
);
接着,我们来开始进行读写分离配置。
# ================== 读写分离配置 ==========================
# 配置数据源名称
spring.shardingsphere.datasource.names=m0,s0
# 主库配置
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://docker.host.a:3306/masterdemo?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.m0.username=root
spring.shardingsphere.datasource.m0.password=123456
# 从库配置
spring.shardingsphere.datasource.s0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.s0.url=jdbc:mysql://docker.host.a:3307/masterdemo?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.s0.username=root
spring.shardingsphere.datasource.s0.password=123456
# ===> 主从规则配置
# master-slave-rules是一个map, 可以配置key和value
# 配置主节点名称
spring.shardingsphere.sharding.master-slave-rules.ds0.master-data-source-name=m0
# 配置从节点, 可以配置一主多从
spring.shardingsphere.sharding.master-slave-rules.ds0.slave-data-source-names[0]=s0
# 配置逻辑表t_num对应的真实表, 这里设置为主库对应的表, 来验证从库是否会自动同步
spring.shardingsphere.sharding.tables.t_dict.actual-data-nodes=ds0.t_dict
# ===> 主键策略配置
# 配置主键列
spring.shardingsphere.sharding.tables.t_dict.key-generator.column=dict_id
# 配置主键生成策略
spring.shardingsphere.sharding.tables.t_dict.key-generator.type=SNOWFLAKE
# 配置雪花算法的额外参数
spring.shardingsphere.sharding.tables.t_dict.key-generator.props.worker.id=1
# ===> 配置显示sql
spring.shardingsphere.props.sql.show = true
spring.main.allow-bean-definition-overriding=true
然后我们执行如下测试方法插入数据:
@Test
public void addDictByMS() {
Dict d1 = new Dict();
d1.setUstatus("1");
d1.setUvalue("正常");
dictMapper.insert(d1);
Dict d2 = new Dict();
d2.setUstatus("0");
d2.setUvalue("不正常");
dictMapper.insert(d2);
}
Logic SQL: INSERT INTO t_dict ( ustatus, uvalue ) VALUES ( ?,? )
Actual SQL: m0 ::: INSERT INTO t_dict ( ustatus, uvalue , dict_id) VALUES (?, ?, ?) ::: [1, 正常, 646318474917842944]
Logic SQL: INSERT INTO t_dict ( ustatus, uvalue ) VALUES ( ?, ? )
Actual SQL: m0 ::: INSERT INTO t_dict ( ustatus, uvalue , dict_id) VALUES (?, ?, ?) ::: [0, 不正常, 646318478474612737]
从真实执行的Sql我们可以看出来,两条记录都只是插入到了master数据库中的t_dict表中。并没有插入slave数据库的语句。
首先我们检查主从同步是否生效:
主从同步已经生效!ShardingJDBC只负责将数据插入到主库中,而mysql的主从复制策略会让从库自动读取binlog,从而同步主库的操作。
接下来才是重点,我们这次是写操作,按照读写分离,写操作应该是主库来完成的吧!我们检查上面执行的真实Sql,发现都是m0也就是master节点进行的写操作!这说明写已经符合我们的期望,那我们再来执行一下读操作吧。
@Test
public void queryDictByMS() {
List<Dict> dicts = dictMapper.selectList(null);
dicts.forEach(System.out::println);
}
Logic SQL: SELECT dict_id,ustatus,uvalue FROM t_dict
Actual SQL: s0 ::: SELECT dict_id,ustatus,uvalue FROM t_dict
可以看到,读操作是由s0也就是slave节点进行所有的读操作!
至此,我们已经成功的实现了读写分离配置!
要注意理解在读写分离策略中,ShardingJDBC只能帮我们把读写操作分发到不同的数据库上,而数据库之间的数据同步,还是需要由MySQL主从集群来完成。
4、ShardingJDBC的分片算法
ShardingJDBC的整个实战完成后,可以看到,整个分库分表的核心就是在于配置的分片算法。
我们的这些实战都是使用的inline分片算法,即提供一个分片键和一个分片表达式来制定分片算法。
这种方式配置简单,功能灵活,是分库分表最佳的配置方式,并且对于绝大多数的分库分片场景来说,都已经非常好用了。
但是,如果针对一些更为复杂的分片策略,例如多分片键、按范围分片等场景,inline分片算法就有点力不从心了。所以,我们还需要学习下ShardingSphere提供的其他几种分片策略。
ShardingSphere目前提供了一共五种分片策略:
4.1 NoneShardingStrategy
不分片。这种严格来说不算是一种分片策略了。只是ShardingSphere也提供了这么一个配置。
4.2 InlineShardingStrategy
最常用的分片方式.
配置参数:
- inline.shardingColumn :分片键
- inline.algorithmExpression: 分片表达式
实现方式: 按照分片表达式来进行分片。
4.3 StandardShardingStrategy(支持精确和范围查询)
只支持单分片键的标准分片策略。
配置参数:
- standard.sharding-column :分片键
- standard.precise-algorithm-class-name: 精确分片算法类名;
- standard.range-algorithm-class-name :范围分片算法类名
实现方式:
shardingColumn指定分片算法。
preciseAlgorithmClassName 指向一个实现了io.shardingsphere.api.algorithm.sharding.standard.PreciseShardingAlgorithm接口的java类名,提供按照 = 或者 IN 逻辑的精确分片 示例:com.jihu.sharding.algorithm.MyPreciseShardingAlgorithm
rangeAlgorithmClassName 指向一个实现了 io.shardingsphere.api.algorithm.sharding.standard.RangeShardingAlgorithm接口的java类名,提供按照Between 条件进行的范围分片。示例:com.jihu.sharding.algorithm.MyRangeShardingAlgorithm
说明:
其中精确分片算法是必须提供的,而范围分片算法则是可选的。
4.4 ComplexShardingStrategy(支持多分片键)
支持多分片键的复杂分片策略。
配置参数:
- complex.sharding-columns : 分片键(多个)
- complex.algorithm-class-name : 分片算法实现类
实现方式:
shardingColumn指定多个分片列。
algorithmClassName指向一个实现了org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm接口的java类名。提供按照多个分片列进行综合分片的算法。示例:com.jihu.sharding.algorithm.MyComplexKeysShardingAlgorithm
4.5 HintShardingStrategy
不需要分片键的强制分片策略。
这个分片策略,简单来理解就是说,他的分片键不再跟SQL语句相关联,而是用程序另行指定。对于一些复杂的情况,例如select count(*) from (select userid from t_user where userid in (1,3,5,7,9)) 这样的SQL语句,就没法通过SQL语句来指定一个分片键。这个时候就可以通过程序,给他另行执行一个分片键,例如在按userid奇偶分片的策略下,可以指定1作为分片键,然后自行指定他的分片策略。
配置参数:
- hint.algorithm-class-name: 分片算法实现类。
实现方式:
algorithmClassName指向一个实现了org.apache.shardingsphere.api.sharding.hint.HintShardingAlgorithm接口的java类名。 示例:com.jihu.sharding.algorithm.MyHintShardingAlgorithm
在这个算法类中,同样是需要分片键的。而分片键的指定是通过HintManager.addDatabaseShardingValue方法(分库)和HintManager.addTableShardingValue(分表)来指定。
使用时要注意,这个分片键是线程隔离的,只在当前线程有效,所以通常建议使用之后立即关闭,或者用try资源方式打开。
而Hint分片策略并没有完全按照SQL解析树来构建分片策略,是绕开了SQL解析的,所有对某些比较复杂的语句,Hint分片策略性能有可能会比较好(情况太多了,无法一一分析)。
但是要注意,Hint强制路由在使用时有非常多的限制:
-- 不支持UNION
SELECT * FROM t_order1 UNION SELECT * FROM t_order2
INSERT INTO tbl_name (col1, col2, …) SELECT col1, col2, … FROM tbl_name WHERE col3 = ?
-- 不支持多层子查询
SELECT COUNT(*) FROM (SELECT * FROM t_order o WHERE o.id IN (SELECT id FROM t_order WHERE status = ?))
-- 不支持函数计算。ShardingSphere只能通过SQL字面提取用于分片的值
SELECT * FROM t_order WHERE to_date(create_time, 'yyyy-mm-dd') = '2019-01-01';
示例详见application02.properties配置。
从这里也能看出,即便有了ShardingSphere框架,分库分表后对于SQL语句的支持依然是非常脆弱的。
5、ShardingSphere的SQL使用限制
参见官网文档: https://shardingsphere.apache.org/document/current/cn/features/sharding/use-norms/sql/ 文档中详细列出了非常多ShardingSphere目前版本支持和不支持的SQL类型。这些东西要经常关注。
5.1支持的SQL
5.2 不支持的SQL
5.3 DISTINCT支持情况详细说明
支持的SQL:
不支持的SQL:
6、分库分表带来的问题
1、分库分表,其实围绕的都是一个核心问题,就是单机数据库容量的问题。我们要了解,在面对这个问题时,解决方案是很多的,并不止分库分表这一种。但是ShardingSphere的这种分库分表,是希望在软件层面对硬件资源进行管理,从而便于对数据库的横向扩展,这无疑是成本很小的一种方式。
2、一般情况下,如果单机数据库容量撑不住了,应先从缓存技术着手降低对数据库的访问压力。如果缓存使用过后,数据库访问量还是非常大,可以考虑数据库读写分离策略。如果数据库压力依然非常大,且业务数据持续增长无法估量,最后才考虑分库分表,单表拆分数据应控制在1000万以内。
当然,随着互联网技术的不断发展,处理海量数据的选择也越来越多。在实际进行系统设计时,最好是用MySQL数据库只用来存储关系性较强的热点数据,而对海量数据采取另外的一些分布式存储产品。例如PostGreSQL、VoltDB甚至HBase、Hive、ES等这些大数据组件来存储。
3、从上一部分ShardingJDBC的分片算法中我们可以看到,由于SQL语句的功能实在太多太全面了,所以分库分表后,对SQL语句的支持,其实是步步为艰的,稍不小心,就会造成SQL语句不支持、业务数据混乱等很多很多问题。所以,实际使用时,我们会建议这个分库分表,能不用就尽量不要用。
如果要使用优先在OLTP场景下使用,优先解决大量数据下的查询速度问题。而在OLAP场景中,通常涉及到非
常多复杂的SQL,分库分表的限制就会更加明显。当然,这也是ShardingSphere以后改进的一个方向。
4、如果确定要使用分库分表,就应该在系统设计之初开始对业务数据的耦合程度和使用情况进行考量,尽量控制业务SQL语句的使用范围,将数据库往简单的增删改查的数据存储层方向进行弱化。并首先详细规划垂直拆分的策略,使数据层架构清晰明了。而至于水平拆分,会给后期带来非常非常多的数据问题,所以应该谨慎、谨慎再谨慎。一般也就在日志表、操作记录表等很少的一些边缘场景才偶尔用用。
7、分库分表方案设计实战
接下来,我们来给电商的商品管理模块设计一个分库分表的方案,来理解下分库分表应该如何落地。
一个典型的电商场景,商品管理模块大致的功能组件如下图:
针对这个场景,考虑到商品信息会持续增长,越来越多的情况,要如何设计分库分表方案?
1、以业务为单位考虑对数据进行垂直分片,店铺、产品、商品三种业务数据垂直拆分成三个不同的库。字典表作为广播表冗余到三个不同的库中。
2、考虑数据增长情况,商品将会是以后增长最快的数据,店铺和产品的数据增速会逐渐降低。所以对商品表进行分片。分片策略采用商品ID取模的方式,尽量保证商品数据平均分片。
3、将关联性较强的商品信息表和商品补充信息表配置为绑定表。
整体分库分表大致如下图:
思考:这种分库分表解决了哪些问题?支持的场景有哪些?不支持的场景有哪些?