分布式专题(10)之ShardingSphere分库分表实战指南

一、ShardingSphere产品介绍

         Apache ShardingSphere 是一款分布式的数据库生态系统, 可以将任意数据库转换为分布式数据库,并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。Apache ShardingSphere 设计哲学为 Database Plus,旨在构建异构数据库上层的标准和生态。 它关注如何充分合理地利用数据库的计算和存储能力,而并非实现一个全新的数据库。 它站在数据库的上层视角,关注它们之间的协作多于数据库自身。

        另外需要注意的是ShardingSphere的版本演进路线。

         当前最新的版本是5.x。可以看到,对于当前版本,ShardingSphere的核心是可插拔。其核心设计哲学就是连接、增强以及可拔插。这是官网对于其整个设计哲学的核心描述。

         你现在当然不需要去了解各个细节,但是你应该要理解ShardingSphere是希望演进成一个重要的分库分表的功能核心。这个功能核心是构建在现有的数据库产品之上的,同时他可以支持大量的可插拔的上层应用扩展。这也意味着,在后面学习ShardingSphere时,你一定需要花更多心思去理解如何对ShardingSphere的功能进行扩展,而不能仅仅是学会如何使用ShardingSphere已经提供的功能。

二、客户端分库分表与服务端分库分表

        ShardingSphere最为核心的产品有两个:一个是ShardingJDBC,这是一个进行客户端分库分表的框架。另一个是ShardingProxy,这是一个进行服务端分库分表的产品。他们代表了两种不同的分库分表的实现思路。

2.1 ShardingJDBC客户端分库分表

        ShardingSphere最为核心的产品有两个:一个是ShardingJDBC,这是一个进行客户端分库分表的框架。另一个是ShardingProxy,这是一个进行服务端分库分表的产品。他们代表了两种不同的分库分表的实现思路。

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

2.2 ShardingProxy服务端分库分表 

         ShardingSphere-Proxy 定位为透明化的数据库代理端,通过实现数据库二进制协议,对异构语言提供支持。 目前提供 MySQL 和 PostgreSQL 协议,透明化数据库操作,对 DBA 更加友好。

  • 向应用程序完全透明,可直接当做 MySQL/PostgreSQL 使用;
  • 兼容 MariaDB 等基于 MySQL 协议的数据库,以及 openGauss 等基于 PostgreSQL 协议的数据库;
  • 适用于任何兼容 MySQL/PostgreSQL 协议的的客户端,如:MySQL Command Client, MySQL Workbench, Navicat 等。

2.3 ShardingSphere混合部署架构 

         这两个产品都各有优势。ShardingJDBC跟客户端在一起,使用更加灵活。而ShardingProxy是一个独立部署的服务,所以他的功能相对就比较固定。他们的整体区别如下:

        另外,在产品图中,Governance Center也是其中重要的部分。他的作用有点类似于微服务架构中的配置中心,可以使用第三方服务统一管理分库分表的配置信息,当前建议使用的第三方服务是Zookeeper,同时也支持Nacos,Etcd等其他第三方产品。

      由于ShardingJDBC和ShardingProxy都支持通过Governance Center,将配置信息交个第三方服务管理,因此,也就自然支持了通过Governance Center进行整合的混合部署架构。

三、快速上手ShardingJDBC 

         ​ 我们预备将一批课程信息分别拆分到两个库中的两个表里。

3.1 搭建基础环境 

         接下来我们使用最常用的SpringBoot+MyBatis+MyBatis-plus快速搭建一个可以访问数据的简单应用,以这个应用作为我们分库分表的基础。

       ​ step1: 在数据库中创建course表,建表语句如下:

CREATE TABLE course  (`cid` bigint(0) NOT NULL AUTO_INCREMENT,`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;

​         step2: 搭建一个Maven项目,在pom.xml中加入依赖,其中就包含访问数据库最为简单的几个组件。

<dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.2.1.RELEASE</version><type>pom</type><scope>import</scope></dependency><!-- mybatisplus依赖 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.0.5</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>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><!-- 数据源连接池 --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</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><version>3.4.3.3</version></dependency></dependencies>

         step3: 使用MyBatis-plus的方式,直接声明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> {
}@SpringBootApplication
@MapperScan("com.roy.jdbcdemo.mapper")
public class App {public static void main(String[] args) {SpringApplication.run(App.class,args);}
}

         step5: 在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://localhost:3306/coursedb?serverTimezone=UTC
spring.datasource.druid.username=root
spring.datasource.druid.password=root

         step6: 做一个单元测试,简单的把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));}
}

3.2 引入ShardingSphere分库分表 

        ​ 接下来,我们将在这个简单案例上使用ShardingSphere快速Course表的分库分表功能。

          step1:调整pom.xml中的依赖,引入ShardingSphere。

        ShardingSphere的实现机制和我们之前章节中使用DynamicDataSource框架实现读写分离很类似,也是在底层注入一个带有分库分表功能的DataSource数据源。因此,在调整依赖时,需要注意不要直接使用druid-sprint-boot-starter依赖了。因为这个依赖会在Spring容器中注入一个DataSource,这样再要使用ShardingSphere注入DataSource就会产生冲突了。

<dependencies><!-- shardingJDBC核心依赖 --><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><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><!-- 数据源连接池 --><!--注意不要用这个依赖,他会创建数据源,跟上面ShardingJDBC的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><version>3.4.3.3</version></dependency></dependencies>

         step2: 在对应数据库里创建分片表:按照我们之前的设计,去对应的数据库中自行创建course_1和course_2表。但是这里要注意,在创建分片表时,cid字段就不要用自增长了。因为数据分到四个表后,每个表都自增长,就没办法保证cid字段的唯一性了。

        step3: 增加ShardingJDBC的分库分表配置,然后,好玩的事情来了。应用层代码不需要做任何的修改,直接修改application.properties里的配置就可以完成我们之前设计的分库分表的目标。

# 打印SQL
spring.shardingsphere.props.sql-show = true
spring.main.allow-bean-definition-overriding = true# ----------------数据源配置
# 指定对应的真实库
spring.shardingsphere.datasource.names=m0,m1
# 配置真实库
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=rootspring.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
#------------------------分布式序列算法配置
# 雪花算法,生成Long类型主键。
spring.shardingsphere.rules.sharding.key-generators.alg_snowflake.type=SNOWFLAKE
spring.shardingsphere.rules.sharding.key-generators.alg_snowflake.props.worker.id=1
# 指定分布式主键生成策略
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
#-----------------------配置实际分片节点
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
spring.shardingsphere.rules.sharding.tables.course.database-strategy.standard.sharding-algorithm-name=course_db_algspring.shardingsphere.rules.sharding.sharding-algorithms.course_db_alg.type=MOD
spring.shardingsphere.rules.sharding.sharding-algorithms.course_db_alg.props.sharding-count=2
#给course表指定分表策略  standard-按单一分片键进行精确或范围分片
spring.shardingsphere.rules.sharding.tables.course.table-strategy.standard.sharding-column=cid
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
spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.props.algorithm-expression=course_$->{cid%2+1}

配置过程,刚开始看会有点复杂,但是对应之前的设计图不难对应上。而且后面也会详细来解读配置过程。

这里主要需要理解一下的是配置中用到的Groovy表达式。 比如 m$->${0..1}.course_$->{1..2} 和 course_$->{cid%2+1} 。这是ShardingSphere支持的Groovy表达式,在后面会大量接触到这样的表达式。这个表达式中,$->{}部分为动态部分,大括号内的就是Groovy语句。 两个点,表示一个数据组的起点和终点。m$->${0..1}表示m0和m1两个字符串集合。course_$->{1..2}表示course_1和course_2集合。 course_$->{cid%2+1} 表示根据cid的值进行计算,计算的结果再拼凑上course_前缀。

         在日志里也能看到实际的执行情况。

这个示例中,course信息只能平均分到两个表中,而无法均匀分到四个表中。这其实是根据cid进行计算的结果。而将course_tbl_alg的计算表达式改成 course_$->{((cid+1)%4).intdiv(2)+1} 后,理论上,如果cid是连续递增的,就可以将数据均匀分到四个表里。但是snowflake雪花算法生成的ID并不是连续的,所以有时候还是无法分到四个表。

四、理解ShardingSphere核心概念

         从这个简单示例中,我们可以接触到分库分表很多核心的概念。这些概念都是后面进行更复杂的分库分表时,需要大量运用的重要工具。

4.1 垂直分片与水平分片

         这是设计分库分表方案时经常会提到的概念。 其中垂直分片表示按照业务的纬度,将不同的表拆分到不同的库当中。这样可以减少每个数据库的数据量以及客户端的连接数,提高查询效率。而水平分表表示按照数据的纬度,将原本存在同一张表中的数据,拆分到多张子表当中。每个子表只存储一份的数据。这样可以将数据量分散到多张表当中,减少每一张表的数据量,提升查询效率。

4.2 ShardingSphere实现分库分表的核心概念 

         接下来我们依次解析一下刚才示例中配置的一些重要的概念,可以对照一下之前的配置信息进行验证。

  1. 虚拟库: ShardingSphere的核心就是提供一个具备分库分表功能的虚拟库,他是一个ShardingSphereDatasource实例。应用程序只需要像操作单数据源一样访问这个ShardingSphereDatasource即可。
  2. 真实库: 实际保存数据的数据库。这些数据库都被包含在ShardingSphereDatasource实例当中,由ShardingSphere决定未来需要使用哪个真实库。
  3. 逻辑表: 应用程序直接操作的逻辑表。
  4. 真实表: 实际保存数据的表。这些真实表与逻辑表表名不需要一致,但是需要有相同的表结构,可以分布在不同的真实库中。应用可以维护一个逻辑表与真实表的对应关系,所有的真实表默认也会映射成为ShardingSphere的虚拟表。
  5. 分布式主键生成算法: 给逻辑表生成唯一主键。由于逻辑表的数据是分布在多个真实表当中的,所有,单表的索引就无法保证逻辑表的ID唯一性。ShardingSphere集成了几种常见的基于单机生成的分布式主键生成器。比如SNOWFLAKE,COSID_SNOWFLAKE雪花算法可以生成单调递增的long类型的数字主键,还有UUID,NANOID可以生成字符串类型的主键。当然,ShardingSphere也支持应用自行扩展主键生成算法。比如基于Redis,Zookeeper等第三方服务,自行生成主键。
  6. 分片策略: 表示逻辑表要如何分配到真实库和真实表当中,分为分库策略和分表策略两个部分。分片策略由分片键和分片算法组成。分片键是进行数据水平拆分的关键字段。如果没有分片键,ShardingSphere将只能进行全路由,SQL执行的性能会非常差。分片算法则表示根据分片键如何寻找对应的真实库和真实表。简单的分片策略可以使用Groovy表达式直接配置,当然,ShardingSphere也支持自行扩展更为复杂的分片算法。

         示例当中给course表分配配置了分库策略course_db_alg ,和分表策略 course_tbl_alg。其中course_db_alg是使用的ShardingSphere内置的简单算法MOD取模。如果对于字符串类型的主键,也提供了HASH_MOD进行计算。这两个算法都需要配置一个参数sharding‐count分片数量。这种内置的算法虽然简单,但是不太灵活。 因为对2取模的结果只能是0和1,而对于course表来说,他的真实表是course_1和course_2,后缀需要在取模的结果上加1,这种计算就没法通过简单的取模算法实现了,所以需要通过Groovy表达式进行定制。

五、ShardingSphere深入实战

         理解这些基础概念之后,我们就继续深入更多的分库分表场景。下面的过程会通过一系列的问题来给你解释ShardingSphere最常用的分片策略。这个过程强烈建议你自己动手试试。因为不管你之前熟不熟悉ShardingSphere,你都需要一步步回顾总结一下分库分表场景下需要解决的是哪些稀奇古怪的问题。分库分表的问题非常非常多,你需要的是学会思考,而不是API。

4.1 简单INLINE分片算法 

         我们之前配置的简单分库分表策略已经可以根据自动生成的cid,将数据插入到不同的真实库当中。那么当然也支持按照cid进行数据查询。

 @Testpublic void queryCourse() {QueryWrapper<Course> wrapper = new QueryWrapper<Course>();
//        wrapper.eq("cid",851198095084486657L);wrapper.in("cid",851198095084486657L,851198095139012609L,851198095180955649L,4L);List<Course> courses = courseMapper.selectList(wrapper);courses.forEach(course -> System.out.println(course));}

         像= 和 in 这样的操作,可以拿到cid的精确值,所以都可以直接通过表达式计算出可能的真实库以及真实表,ShardingSphere就会将逻辑SQL转去查询对应的真实库和真实表。这些查询的策略,只要配置了sql-show参数,都会打印在日志当中。

        如果不使用分片键cid进行查询,那么SQL语句就只能根据actual-nodes到所有的真实库和真实表里查询。而这时ShardingSphere是怎么执行的呢?例如,如果直接执行select * from course,执行情况是这样的:

2023-04-12 15:55:02.958  INFO 12448 --- [           main] ShardingSphere-SQL                       : Logic SQL: SELECT  cid,cname,user_id,cstatus  FROM course2023-04-12 15:55:02.958  INFO 12448 --- [           main] 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
2023-04-12 15:55:02.958  INFO 12448 --- [           main] 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

         在之前4.x版本下,这种情况会拆分成四个SQL,查询四次。而当前版本下,会将每一个真实库里的语句通过UNION合并成一个大SQL,一起进行查询。为什么要这样呢?这其实是一个很大的优化。因为如果需要对一个真实库进行多个SQL查询,那么就需要通过多线程进行并发查询,这种情况下,如果要进行后续的结果归并,比如sum,max这样的结果归并,那就只能将所有的结果都合并到一个大内存,再进行归并。这种方式称为内存归并。这种方式是比较消耗内存的。而如果合并成了一个大的SQL,对一个真实库只要进行一次SQL查询,这样就可以通过一个线程进行查询。在进行结果归并时,就可以拿一条数据归并一次。这种方式称为流式归并。相比内存归并可以极大的节约内存。

       在使用in进行查询时,有可能计算出属于多个不同的分片。在4.x版本当中,如果出现了这种情况,由于ShardingSphere无法确定in算出来的分片有多少个,所以遇到这种情况,他就不再去计算in中所有的分片结果了,直接改为全路由分片。这样计算比较简单,但是查询的效率肯定不好。而在当前版本下,则优化了这个问题。比如示例当中in操作的cid有奇数也有偶数,新版本就能够准确的计算出m0.course_1和m1.course_2两个分片。

4.2 STANDARD标准分片算法

​         应用当中我们可能对于主键信息不只是进行精确查询,还需要进行范围查询。例如:

@Testpublic void queryCourseRange(){//select * from course where cid between xxx and xxxQueryWrapper<Course> wrapper = new QueryWrapper<>();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参数。这时,就需要给course_tbl_alg算法添加这个参数。

# 允许在inline策略中使用范围查询。
spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.props.allow-range-query-with-inline-sharding=true

         加上这个参数后,就可以进行查询了。但是这样就可以了吗?观察一下Actual SQL的执行方式,你会发现这时SQL还是按照全路由的方式执行的。之前一直强调过,这是效率最低的一种执行方式。那么有没有办法通过查询时的范围下限和范围上限自己计算出一个目标真实库和真实表呢?当然是支持的。记住这个问题,在后续章节会带你解决

5.3  COMPLEX_INLINE复杂分片算法

       现在既然可以根据cid进行查询,那么还可以增加其他的查询条件吗?像这样:

 @Testpublic void queryCourseComplexSimple(){QueryWrapper<Course> wrapper = new QueryWrapper<Course>();wrapper.orderByDesc("user_id");wrapper.in("cid",851198095084486657L,851198095139012609L);wrapper.eq("user_id",1001L);List<Course> course = courseMapper.selectList(wrapper);//select * from couse where cid in (xxx) and user_id =xxxSystem.out.println(course);}

         执行一下,这当然是可以的。但是有一个小问题,user_id查询条件只能参与数据查询,但是并不能参与到分片算法当中。例如在我们的示例当中,所有的user_id都是1001L,这其实是数据一个非常明显的分片规律。如果user_id的查询条件不是1001L,那这时其实不需要到数据库中去查,我们也能知道是不会有结果的。有没有办法让user_id也参与到分片算法当中呢?

        当然是可以的, 不过STANDARD策略就不够用了。这时候就需要引入COMPLEX_INLINE策略。注释掉之前给course表配置的分表策略,重新分配一个新的分表策略:

#给course表指定分表策略  complex-按多个分片键进行组合分片
spring.shardingsphere.rules.sharding.tables.course.table-strategy.complex.sharding-columns=cid,user_id
spring.shardingsphere.rules.sharding.tables.course.table-strategy.complex.sharding-algorithm-name=course_tbl_alg
# 分表策略-COMPLEX:按多个分片键组合分表
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传什么值,就总是会路由到错误的表当中,查不出数据了。 

5.4 CLASS_BASED 分片算法

        虽然对于COMPLEX_INLINE策略,也支持添加allow-range-query-with-inline-sharding参数让他能够支持分片键的范围查询,但是这时这种复杂的分片策略就明显不能再用一个简单的表达式来忽悠了。

        这就需要一个Java类来实现这样的规则。这个算法类也不用自己瞎设计,只要实现ShardingSphere提供的ComplexKeysShardingAlgorithm接口就行了。

  

package com.roy.shardingDemo.algorithm;import com.google.common.collect.Range;
import org.apache.shardingsphere.sharding.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.sharding.api.sharding.complex.ComplexKeysShardingValue;
import org.apache.shardingsphere.sharding.exception.syntax.UnsupportedShardingOperationException;import java.util.*;/*** 实现自定义COMPLEX分片策略* 声明算法时,ComplexKeysShardingAlgorithm接口可传入一个泛型,这个泛型就是分片键的数据类型。* 这个泛型只要实现了Comparable接口即可。* 但是官方不建议声明一个明确的泛型出来,建议是在doSharding中再做类型转换。这样是为了防止分片键类型与算法声明的类型不符合。*/
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 在actual-nodes中配置了的所有数据分片* @param shardingValue 组合分片键* @return 目标分片*/@Overridepublic Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Long> shardingValue) {//select * from cid where cid in (xxx,xxx,xxx) and user_id between {lowerEndpoint} and {upperEndpoint};Collection<Long> cidCol = shardingValue.getColumnNameAndShardingValuesMap().get("cid");Range<Long> userIdRange = shardingValue.getColumnNameAndRangeValuesMap().get("user_id");//拿到user_id的查询范围Long lowerEndpoint = userIdRange.lowerEndpoint();Long upperEndpoint = userIdRange.upperEndpoint();//如果下限 》= 上限if(lowerEndpoint >= upperEndpoint){//抛出异常,终止去数据库查询的操作throw new UnsupportedShardingOperationException("empty record query","course");//如果查询范围明显不包含1001}else if(upperEndpoint<1001L || lowerEndpoint>1001L){//抛出异常,终止去数据库查询的操作throw new UnsupportedShardingOperationException("error range query param","course");
//            return result;}else{List<String> result = new ArrayList<>();//user_id范围包含了1001后,就按照cid的奇偶分片String logicTableName = shardingValue.getLogicTableName();//操作的逻辑表 coursefor (Long cidVal : cidCol) {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;}@Overridepublic String getType(){return "MYCOMPLEX";}
}

        在核心的dosharding方法当中,就可以按照我们之前的规则进行判断。不满足规则,直接抛出UnsupportedShardingOperationException异常,就可以组织ShardingSphere把SQL分配到真实数据库中执行。

         接下来,还是需要增加策略配置,让course表按照这个规则进行分片。

# 使用CLASS_BASED分片算法- 不用配置SPI扩展文件
spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.type=CLASS_BASED
# 指定策略 STANDARD|COMPLEX|HINT
spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.props.strategy=COMPLEX
# 指定算法实现类。这个类必须是指定的策略对应的算法接口的实现类。 STANDARD-> StandardShardingAlgorithm;COMPLEX->ComplexKeysShardingAlgorithm;HINT -> HintShardingAlgorithm
spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.props.algorithmClassName=com.roy.shardingDemo.algorithm.MyComplexAlgorithm

 5.5 HINT_INLINE强制分片算法

         接下来我们把查询场景再进一步,需要查询所有cid为奇数的课程信息。这要怎么查呢?按照MyBatis-plus的机制,你应该很快能想到在CourseMapper中实现一个自定义的SQL语句就行了。

public interface CourseMapper extends BaseMapper<Course> {@Select("select * from course where MOD(cid,2)=1")List<Long> unsupportSql();
}

         OK,拿过去试试。

@Testpublic void unsupportTest(){//select * from course where mod(cid,2)=1List<Long> res = courseMapper.unsupportSql();res.forEach(System.out::println);}

         执行结果当然是没有问题。但是你会发现,分片的问题又出来了。在我们当前的这个场景下,course的信息就是按照cid的奇偶分片的,所以自然是希望只去查某一个真实表就可以了。这种基于虚拟列的查询语句,对于ShardingSphere来说实际上是一块难啃的骨头。因为他很难解析出你是按照cid分片键进行查询的,并且不知道怎么组织对应的策略去进行分库分表。所以他的做法只能又是性能最低的全路由查询。

实际上ShardingSphere无法正常解析的语句还有很多。基本上用上分库分表后,你的应用就应该要和各种多表关联查询、多层嵌套子查询、distinct查询等各种复杂查询分手了。

         这个cid的奇偶关系并不能通过SQL语句正常体现出来,这时,就需要用上ShardingSphere提供的另外一种分片算法HINT强制路由。HINT强制路由可以用一种与SQL无关的方式进行分库分表。

        注释掉之前给course表分配的分表算法,重新分配一个HINT_INLINE类型的分表算法。

#给course表指定分表策略  hint-与SQL无关的方式进行分片
spring.shardingsphere.rules.sharding.tables.course.table-strategy.hint.sharding-algorithm-name=course_tbl_alg
# 分表策略-HINT:用于SQL无关的方式分表,使用value关键字。
spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.type=HINT_INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.course_tbl_alg.props.algorithm-expression=course_$->{value}

        ​ 然后,在应用进行查询时,使用HintManager给HINT策略指定value的值。

 @Testpublic void queryCourseByHint(){//强制只查course_1表HintManager hintManager = HintManager.getInstance();// 强制查course_1表hintManager.addTableShardingValue("course","1");//select * from course;List<Course> courses = courseMapper.selectList(null);courses.forEach(course -> System.out.println(course));//线程安全,所有用完要注意关闭。hintManager.close();//hintManager关闭的主要作用是清除ThreadLocal,释放内存。HintManager实现了AutoCloseable接口,所以建议使用try-resource的方式,用完自动关闭。//try(HintManager hintManager = HintManager.getInstance()){ xxxx }}

         这样就可以让SQL语句只查询course_1表,在当前场景下,也就相当于是实现了只查cid为奇数的需求。

5.6 常用策略总结

         在之前的示例中就介绍了ShardingSphere提供的MOD、HASH-MOD这样的简单内置分片策略,standard、complex、hint三种典型的分片策略以及CLASS_BASED这种扩展分片策略的方法。为什么要有这么多的分片策略,其实就是以为分库分表面临的业务场景其实是很复杂的。即便是ShardingSphere,也无法真的像MySQL、Oracle这样的数据库产品一样,完美的兼容所有的SQL语句。因此,一旦开始决定用分库分表,那么后续业务中的每一个SQL语句就都需要结合分片策略进行思考,不能像操作真正数据库那样随心所欲了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/65034.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

帧缓存的分配

帧缓存实际上就是一块内存。在 Android 系统中分配与回收帧缓存&#xff0c;使用的是一个叫 ION 的内核模块&#xff0c;App 使用 ioctl 系统调用后&#xff0c;会在内核内存中分配一块符合要求的内存&#xff0c;用户态会拿到一个 fd&#xff08;有的地方也称之为 handle&…

vue3+vite一个IP对站点名称的前端curd更新-会议系统优化

vue3-tailwind-todo https://github.com/kgrg/vue3-tailwind-todo 基于这个项目,把ip到sta的映射做了前端管理. 核心代码是存储和获得的接口,需要flask提供. def redis2ipdic():global ipdicipdic.clear()tmdiccl.hgetall(IPDIC_KEY)for k in tmdic.keys():ipdic[k.decode() …

Elasticsearch-脚本查询

脚本查询 概念 Scripting是Elasticsearch支持的一种专门用于复杂场景下支持自定义编程的强大的脚本功能&#xff0c;ES支持多种脚本语言&#xff0c;如painless&#xff0c;其语法类似于Java,也有注释、关键字、类型、变量、函数等&#xff0c;其就要相对于其他脚本高出几倍的性…

golang LeetCode 热题 100(动态规划)-更新中

爬楼梯 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 示例 1&#xff1a;输入&#xff1a;n 2 输出&#xff1a;2 解释&#xff1a;有两种方法可以爬到楼顶。 1. 1 阶 1 阶 2. 2 阶 示例 2&…

【每日学点鸿蒙知识】Charles抓包、lock文件处理、WebView组件、NFC相关、CallMethod失败等

1、HarmonyOS系统中如何使用Charles抓包&#xff1f; 在HarmonyOS操作系统中&#xff0c;使用Charles进行抓包的步骤如下&#xff1a; 在Charles中设置代理。 首先&#xff0c;在Charles的菜单栏上选择“Proxy”→“Proxy Settings”&#xff0c;然后填入代理端口&#xff0…

抓取手机HCI日志

荣耀手机 1、打开开发者模式 2、开启HCI、ADB调试 3、开启AP LOG 拨号界面输入*##2846579##* 4、蓝牙配对 5、抓取log adb pull /data/log/bt ./

WebAPI编程(第一天,第二天)

WebAPI编程&#xff08;第一天&#xff0c;第二天&#xff09; day01 - Web APIs 1.1. Web API介绍 1.1.1 API的概念1.1.2 Web API的概念1.1.3 API 和 Web API 总结 1.2. DOM 介绍 1.2.1 什么是DOM1.2.2. DOM树 1.3. 获取元素 1.3.1. 根据ID获取1.3.2. 根据标签名获取元素1.3.…

windows下Redis的使用

Redis简介&#xff1a; Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库&#xff0c;并提供多种语言的 API。 Redis通常被称为数据结构服务器&#xff0c;因为值&#xff08;value&#xff…

【贪吃蛇小游戏 - JavaIDEA】基于Java实现的贪吃蛇小游戏导入IDEA教程

有问题请留言或私信 步骤 下载项目源码&#xff1a;项目源码 解压项目源码到本地 打开IDEA 左上角&#xff1a;文件 → 新建 → 来自现有源代码的项目 找到解压在本地的项目源代码文件&#xff0c;点击确定 选择“从现有项目创建项目”。点击“下一步” 点击下一步&a…

RTOS下的任务管理

2.3 RTOS下的任务管理(***) RTOS的任务管理主要是进行哪些功能&#xff1f; RTOS的任务管理的多任务管理是怎样进行与实现的&#xff1f; 任务管理中FreeRTOS如何给每个任务分配CPU时间&#xff1f; 文章目录 2.3 RTOS下的任务管理(***)2.3.0 任务概述2.3.1任务的创建与删除2.3…

深度学习——神经网络中前向传播、反向传播与梯度计算原理

一、前向传播 1.1 概念 神经网络的前向传播&#xff08;Forward Propagation&#xff09;就像是一个数据处理的流水线。从输入层开始&#xff0c;按照网络的层次结构&#xff0c;每一层的神经元接收上一层神经元的输出作为自己的输入&#xff0c;经过线性变换&#xff08;加权…

【初阶数据结构与算法】八大排序算法之归并排序与非比较排序(计数排序)

文章目录 一、归并排序二、非比较排序之计数排序三、归并排序和计数排序的性能测试 一、归并排序 归并排序&#xff08;MERGE-SORT&#xff09;是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法&#xff08;Divide andConquer&#xff09;的⼀个⾮常典型的应⽤   …

window安装TradingView

目录 下载安装包 修改文件后缀&#xff0c;解压 将K线换成国内涨红跌绿样式 下载安装包 https://www.tradingview.com/desktop/ 下载完成后是.msix格式文件 &#xff08;我在win10和win11的系统中尝试运行msix都没有成功&#xff0c;所以放弃直接双击运行msix&#xff…

FPGA多路MIPI转FPD-Link视频缩放拼接显示,基于IMX327+FPD953架构,提供2套工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐本博主所有FPGA工程项目-->汇总目录我这里已有的 MIPI 编解码方案我这里已有的FPGA图像缩放方案本博已有的已有的FPGA视频拼接叠加融合方案 3、本 MIPI CSI-RX IP 介绍4、详细设计方案设计原理框图IMX327 及其配置FPD-Link视频…

React+Vite从零搭建项目及配置详解

相信很多React初学者第一次搭建自己的项目&#xff0c;搭建时会无从下手&#xff0c;本篇适合快速实现功能&#xff0c;熟悉React项目搭建流程。 目录 一、创建项目react-item 二、调整项目目录结构 三、使用scss预处理器 四、组件库Ant Design 五、配置基础路由 六、配置…

Unity复刻胡闹厨房复盘 模块一 新输入系统订阅链与重绑定

本文仅作学习交流&#xff0c;不做任何商业用途 郑重感谢siki老师的汉化教程与代码猴的免费教程以及搬运烤肉的小伙伴 版本&#xff1a;Unity6 模板&#xff1a;3D 核心 渲染管线&#xff1a;URP ------------------------------…

从零开始的编程-java篇1.6.1 万变不离其宗,hello word

前言&#xff1a; 通过实践而发现真理&#xff0c;又通过实践而证实真理和发展真理。从感性认识而能动地发展到理性认识&#xff0c;又从理性认识而能动地指导革命实践&#xff0c;改造主观世界和客观世界。实践、认识、再实践、再认识&#xff0c;这种形式&#xff0c;循环往…

【漏洞复现】CVE-2021-45788 SQL Injection

漏洞信息 NVD - cve-2021-45788 Time-based SQL Injection vulnerabilities were found in Metersphere v1.15.4 via the “orders” parameter. Authenticated users can control the parameters in the “order by” statement, which causing SQL injection. API: /test…

Mac系统下 idea运行maven项目中存在的问题BeanDefinitionStoreException

1.在进行 注解XML 方式整合三层架构事出现此问题 org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: file [/Volumes/PS2000/Java/SpringProject/micro-shop/spring-annotation-practice-03/target/classes/com/ja…

shiro注入filter内存马(绕过长度限制)

shiro环境 https://github.com/yyhuni/shiroMemshell&#xff08;实验环境&#xff09; 这里用的 Client_memshell.java package com.example.demo;import javassist.ClassPool; import javassist.CtClass; import org.apache.shiro.crypto.AesCipherService; import org.ap…