前言
Apache ShardingSphere 是一套开源的分布式数据库解决方案组成的生态圈,旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力。具体内容请看官方ShardingSphere。本文主要记录一下Springboot整合ShardingSphere,并实现精确分片算法、范围分片算法、复合分片算法、读写分离、读写分离+分表的配置记录。
正文
SpringBoot整合ShardingSphere
maven依赖
<dependencies> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.1.1</version> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-namespace</artifactId> <version>4.1.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.shardingsphere/sharding-jdbc-core --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-core</artifactId> <version>4.1.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.3.14</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> </dependencies>
行表达式分片策略
行表达式分辨策略使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作并且只支持单分片(针对一个字段分片例如id)的操作。例如tb_user_$->{id%2}表示通过id对2取模,实现的效果是tb_user_0存放id为偶数的数据,tb_user_1存放id为奇数的数据。
配置文件如下
#基于行策略实现的分表分库 spring: shardingsphere: datasource: #数据源名称,多个值用逗号隔开 names: ds0,ds1 ds0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test_1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root ds1: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test_2?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root sharding: tables: tb_user: #逻辑表名,需要与mapper中sql语句中的表名一致 actual-data-nodes: ds$->{0..1}.tb_user_$->{0..1} #实际的节点名称 例如 ds0.tb_user_0,ds0.tb_user_1,ds1.tb_user_0,ds1.tb_user_1 table-strategy: inline: sharding-column: id #分片字段 algorithm-expression: tb_user_$->{id % 2} #分表表达式 database-strategy: inline: sharding-column: id algorithm-expression: ds$->{id % 2} #分库表达式 key-generator: column: id #id生成策略,雪花算法,uuid type: SNOWFLAKE default-data-source-name: ds0 #不进行分表分库的表,操作的默认数据源 props: sql: show: true #显示sql #注意 #没有分库,只分表的情况下,不需要配置分库策略,配置如下 。结果是ds0.tb_user_0,ds0.tb_user_1 #sharding: # tables: # tb_user: #逻辑表名,需要与mapper中sql语句中的表名一致 # actual-data-nodes: ds0.tb_user_$->{0..1} #实际的节点名称 例如 ds0.tb_user_0,ds0.tb_user_1 # table-strategy: # inline: # sharding-column: id #分片字段 # algorithm-expression: tb_user_$->{id % 2} #分表表达式 # key-generator: # column: id #id生成策略,雪花算法,uuid # type: SNOWFLAKE #如果只分库,不分表,那么需要每个库中的表名称表结构是一样的,那么配置格式如下 #sharding: # tables: # tb_user: #逻辑表名,需要与mapper中sql语句中的表名一致 # actual-data-nodes: ds$->{0..1}.tb_user #实际的节点名称 例如 ds0.tb_user,ds1.tb_user # database-strategy: # inline: # sharding-column: id # algorithm-expression: ds$->{id % 2} #分库表达式 # key-generator: # column: id #id生成策略,雪花算法,uuid # type: SNOWFLAKE #如果按照最上面的配置,结果将是ds0.tb_user_0,ds1.tb_user_1两张表有数据,其他的表将不会存放数据,此时就需要标准分片算法来实现。
标准分片策略
标准分片策略提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND, >, <, >=, <=分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。
配置文件
#标准分片策略 spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test_1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root ds1: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test_2?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root sharding: tables: tb_user: #逻辑表名 actual-data-nodes: ds$->{0..1}.tb_user_$->{0..1} #实际的数据库节点 key-generator: column: id type: SNOWFLAKE database-strategy: standard: #自定义数据库分片算法 sharding-column: age range-algorithm-class-name: com.xiaojie.sharding.sphere.shardingalgorithm.MyDBShardingAlgorithm precise-algorithm-class-name: com.xiaojie.sharding.sphere.shardingalgorithm.MyDBShardingAlgorithm table-strategy: standard: #自定义表分片算法 sharding-column: id range-algorithm-class-name: com.xiaojie.sharding.sphere.shardingalgorithm.MyTableShardingAlgorithm precise-algorithm-class-name: com.xiaojie.sharding.sphere.shardingalgorithm.MyTableShardingAlgorithm default-data-source-name: ds0 #不使用分表分库策略的数据源 props: sql: show: true #显示sql
自定义算法类
自定义分片算法可以根据自己的需要,如按照年、季度、月、星期、天、或者地区等等需要自己实现规则。
数据库自定义分片算法
package com.xiaojie.sharding.sphere.shardingalgorithm; import com.google.common.collect.Range; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue; import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Collection; /** * @Description:自定义数据库分片算法 数据库的分片字段可以和分表分片字段一样,也可以不一样 * 下面配置 分库字段按照age字段,如果年龄超过30那么放在ds0,如果没有超过放在ds1, * 分表字段按照id字段存放。偶数存放到tb_user_0、奇数存放到tb_user_1 * @author: yan * @date: 2022.03.12 */ @Component public class MyDBShardingAlgorithm implements PreciseShardingAlgorithm<Integer>, RangeShardingAlgorithm<Long> { @Override public String doSharding(Collection<String> dbNames, PreciseShardingValue<Integer> shardingValue) { // for (String dbName : dbNames) { // /** // * 取模算法,分片健 % 表数量 数据库 // */ // Integer age = shardingValue.getValue(); // String tableIndex = age%dbNames.size()+""; // if (dbName.endsWith(tableIndex)) { // return dbName;//返回数据库名称 比如db0,db1 // } // } //如果大于30岁放在db0,小于等于30放在db1 if (shardingValue.getValue() > 30) { return (String) dbNames.toArray()[0]; } else { return (String) dbNames.toArray()[1]; } // throw new IllegalArgumentException(); } @Override public Collection<String> doSharding(Collection<String> dbNames, RangeShardingValue<Long> shardingValue) { Collection<String> collect = new ArrayList<>();//数据库节点名称 Range valueRange = shardingValue.getValueRange();//查询返回 String lowerPoint = String.valueOf(valueRange.hasLowerBound() ? valueRange.lowerEndpoint() : "");//下限 String upperPoint = String.valueOf(valueRange.hasUpperBound() ? valueRange.upperEndpoint() : "");//上限 //判断上限,下限值是否存在,如果不存在赋给默认值。用于处理查询条件中只有 >或<一个条件,不是一个范围查询的情况 long lowerEndpoint = 0; //最小值 long lupperEndpoint = 0;//最大值 if (!lowerPoint.isEmpty() && !upperPoint.isEmpty()) { lowerEndpoint = Math.abs(Long.parseLong(lowerPoint)); lupperEndpoint = Math.abs(Long.parseLong(upperPoint)); } else if (lowerPoint.isEmpty() && !upperPoint.isEmpty()) { lupperEndpoint = Math.abs(Long.parseLong(upperPoint)); lowerEndpoint = 0; } else if (!lowerPoint.isEmpty() && upperPoint.isEmpty()) { lowerEndpoint = Math.abs(Long.parseLong(lowerPoint)); lupperEndpoint = 40; } // 循环范围计算分库逻辑 for (long i = lowerEndpoint; i <= lupperEndpoint; i++) { for (String db : dbNames) { if (db.endsWith(i % dbNames.size() + "")) { collect.add(db); } } } return collect; } }
表自定义分片算法
package com.xiaojie.sharding.sphere.shardingalgorithm; import com.google.common.collect.Range; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue; import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Collection; /** * @Description:自定义表分片算法 #范围分片算法类名称,用于BETWEEN,可选。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器 * shardingsphare默认查询只支持=,between and 这种查询,像>,<,>=,<=这种查询目前不支持, * 除非通过继承自定义接口RangeShardingAlgorithm实现,否则无法使用>,<,>=,<=。 * 同时也需要实现PreciseShardingAlgorithm<String>接口 * @author: yan * @date: 2022.03.12 */ @Component public class MyTableShardingAlgorithm implements PreciseShardingAlgorithm<String>, RangeShardingAlgorithm<Long> { @Override public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) { Range<Long> valueRange = shardingValue.getValueRange();//获得输入的查询条件范围 String slowerEndpoint = String.valueOf(valueRange.hasLowerBound() ? valueRange.lowerEndpoint() : "");//查询条件下限 String supperEndpoint = String.valueOf(valueRange.hasUpperBound() ? valueRange.upperEndpoint() : "");//查询条件上限 //处理只有下限或上限的范围 long lowerEndpoint = 0; long lupperEndpoint = 0; if (!slowerEndpoint.isEmpty() && !supperEndpoint.isEmpty()) { lowerEndpoint = Math.abs(Long.parseLong(slowerEndpoint)); lupperEndpoint = Math.abs(Long.parseLong(supperEndpoint)); } else if (slowerEndpoint.isEmpty() && !supperEndpoint.isEmpty()) { lupperEndpoint = Math.abs(Long.parseLong(supperEndpoint)); lowerEndpoint = 18; } else if (!slowerEndpoint.isEmpty() && supperEndpoint.isEmpty()) { lowerEndpoint = Math.abs(Long.parseLong(slowerEndpoint)); lupperEndpoint = 40; } Collection<String> collect = new ArrayList<>(); // 逐个读取查询范围slowerEndpoint~lupperEndpoint的值,得对应的表名称 for (long i = lowerEndpoint; i <= lupperEndpoint; i++) { for (String each : availableTargetNames) { if (each.endsWith("_" + (i % availableTargetNames.size()))) { if (!collect.contains(each)) { collect.add(each); } } } } return collect; } @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) { for (String each : availableTargetNames) { { String hashCode = String.valueOf(shardingValue.getValue());//配置文件中,分表字段对应的值,也是查询条件中输入的查询条件 long segment = Math.abs(Long.parseLong(hashCode)) % availableTargetNames.size(); if (each.endsWith("_" + segment + "")) {// return each; } } } throw new RuntimeException(shardingValue + "没有匹配到表"); } }
符合分片策略
复合分片策略提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,自定义分片规则需要实现ComplexKeysShardingAlgorithm接口
配置文件
#复合分片策略 spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test_1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root ds1: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test_2?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root sharding: tables: tb_user: actual-data-nodes: ds$->{0..1}.tb_user_$->{0..1} key-generator: column: id type: SNOWFLAKE database-strategy: #库分片策略 complex: sharding-columns: age,id #分片字段 algorithm-class-name: com.xiaojie.sharding.sphere.shardingalgorithm.MyDBComplexShardingStrategy table-strategy: #表分片策略 complex: sharding-columns: age,id #分片字段 algorithm-class-name: com.xiaojie.sharding.sphere.shardingalgorithm.MyTableComplexShardingStrategy default-data-source-name: ds0 props: sql: show: true
分库算法
package com.xiaojie.sharding.sphere.shardingalgorithm; import com.google.common.collect.Range; import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm; import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Collection; import java.util.Map; /** * @Description: 复合分片 分库算法 * @author: yan * @date: 2022.03.12 */ @Component public class MyDBComplexShardingStrategy implements ComplexKeysShardingAlgorithm { @Override public Collection<String> doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValue) { //分片的字段集合 Map<String, Collection> columnMap = shardingValue.getColumnNameAndShardingValuesMap(); //分片的范围规则 Map<String, Range> rangeValuesMap = shardingValue.getColumnNameAndRangeValuesMap(); //获取分片字段的集合 Collection<Integer> agesColumn = columnMap.get("age"); Collection<Long> idColumn = columnMap.get("id"); ArrayList<String> list = new ArrayList(); for (Integer age : agesColumn) { for (Long id : idColumn) { String suffix = null; if (age > 30) { suffix = id % age % availableTargetNames.size() + ""; } else { suffix = (id + age) % availableTargetNames.size() + ""; } for (Object db : availableTargetNames) { String dbName = (String) db; if (dbName.endsWith(suffix)) { list.add(dbName); } } } } return list; } }
读写分离
支持
提供一主多从的读写分离配置,可独立使用,也可配合分库分表使用。
独立使用读写分离支持SQL透传。
同一线程且同一数据库连接内,如有写入操作,以后的读操作均从主库读取,用于保证数据一致性。
基于Hint的强制主库路由。
不支持
主库和从库的数据同步。
主库和从库的数据同步延迟导致的数据不一致。
主库双写或多写。
跨主库和从库之间的事务的数据不一致。主从模型中,事务中读写均用主库。
配置文件
##读写分离配置 spring: shardingsphere: datasource: names: master,slave0 master: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/my_test?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root slave0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:33060/my_test?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root masterslave: load-balance-algorithm-type: round_robin name: ms master-data-source-name: master slave-data-source-names: slave0 props: sql: show: true
读写分离+数据库分表
由于只有一个主库,只实现了分表功能,分库策略同非读写分离的配置一样。
配置文件
#主从复制+分表 spring: shardingsphere: datasource: names: master0,master0slave0 master0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/my_test?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root master0slave0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/my_test?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root sharding: tables: tb_user: #逻辑表名 actual-data-nodes: ds0.tb_user_$->{0..1} table-strategy: #分表策略 inline: sharding-column: id algorithm-expression: tb_user_$->{id % 2} key-generator: column: id type: SNOWFLAKE #绑定表,指分片规则一致的主表和子表。例如:t_order表和t_order_item表,均按照order_id分片,则此两张表互为绑定表关系。 #绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升 # binding-tables: t_order,t_order_item #广播表,适用于数据量不大且需要与海量数据的表进行关联查询的场景 #广播表概念只存在有分库的情况,如果只是分表或主从,不涉及这个概念,配置了也没啥意义。以下论述均依据存在分库的情况 #广播表在每个数据库都有一个,且数据一样,适合字典表场景,数据量少。 #当插入一条数据时,所有库的tb_user表都会插入一条一模一样数据(可能出现分布式事务问题) # broadcast-tables:t_config #分库策略 # default-database-strategy: # inline: # sharding-column: id # algorithm-expression: master$->{id % 2} master-slave-rules: ds0: master-data-source-name: master0 slave-data-source-names: master0slave0 props: sql: show: true #显示sql
注意:
1、数据库如图
2、本整合基于shardingsphere4.1.1;jdk17
3、如果你的jdk是大于8的运行过程可能报如下错误
这种情况一般在使用高于 Java 8 版本的 JDK 时会出现,原因是在 Java 9 及之后的版本对源码进行了模块化重构,public 不再意味着完全开放,而是使用了 export 的机制来更细粒度地控制可见性。
解决方法
在JVM启动参数上添加如下参数
--add-opens java.base/java.lang=ALL-UNNAMED
完整项目和sql文件请自取