引入依赖
springboot 整合 Sharding-jdbc,第一步依赖引入。
<!-- 指定spring-boot版本号 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/>
</parent>
<!-- 指定依赖包的版本 -->
<properties>
<mysql.version>8.0.30</mysql.version>
<mybatis.version>2.2.2</mybatis.version>
<druid.version>1.2.11</druid.version>
<lombok.version>1.18.24</lombok.version>
<sharding-jdbc-version>4.1.1</sharding-jdbc-version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- sharding-jdbc依赖 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${sharding-jdbc-version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
四种分片算法
在 Sharding-jdbc 中提供了四种分片算法,分别是精准分片算法、范围分片算法、复合分片算法和强制路由分片算法。
- standard:精确分片算法,用于单个字段作为分片键,SQL 中有
=
与in
等条件的分片,需要在标准分片策略下使用,算法策略实现PreciseShardingAlgorithm
接口。 - complex:复合分片算法,用于多个字段作为分片键的分片操作,同时获取多个分片键的值,根据多个字段处理业务逻辑。算法策略实现
ComplexKeysShardingAlgorithm
接口。 - Hint:强制路由分片算法,其他分片的算法都是根据分片键来实现路由到不同的数据库、表,但是在一些特殊的场景,需要强制将某个业务 SQL 路由到指定的数据库、表,就需要手动的干预,使用的就是 Hint 分片算法。算法策略实现
HintShardingAlgorithm
接口。 - 范围分片算法:用于单个字段作为分片键,SQL 中有
between and
、>
、<
、>=
、<=
,需要在标准策略下使用,算法策略实现RangeShardingAlgorithm
接口。
四种分片算法接口中都有一个doSharding
方法,方法中提供两个参数分别是:availableTargetNames
、shardingValue
。代码如下:(以ComplexKeysShardingAlgorithm
为例)
/**
* Type parameters: <T> – class type of sharding value
* 这里指定的是数据类型,比如分片是用的主键id,id使用snowflake算法生成,那么这里的值类型就是Long
*/
public interface ComplexKeysShardingAlgorithm<T extends Comparable<?>> extends ShardingAlgorithm {
/**
* Sharding.
*
* @param availableTargetNames available data sources or tables's names
* 表示的是数据库或者是数据表的列表,如果使用在数据库分片策略中,就是数据库列表,如果使用在数据表分片策略中,就是数据表列表
* 如:["ds0","ds1","ds2"]
* @param shardingValue sharding value
* 获取对应分片键和值,ComplexKeysShardingValue中提供了对应的API,根据不同分片算法有所不同
*
* @return sharding results for data sources or tables's names
*/
Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<T> shardingValue);
}
/**
* 封装列名和对应的值
*/
@RequiredArgsConstructor
@Getter
@ToString
public final class ComplexKeysShardingValue<T extends Comparable<?>> implements ShardingValue {
private final String logicTableName;
/**
* Map集合中,键表示分片策略中的列名,value是对应的值(针对值是精确的)
*/
private final Map<String, Collection<T>> columnNameAndShardingValuesMap;
/**
* Map集合中,键表示分片策略中的列名,value是对应的值(针对值是范围的)
*/
private final Map<String, Range<T>> columnNameAndRangeValuesMap;
}
主键生成策略
在单库的时候,主键可以采用的方式很多,如 uuid、自增长、snowflake 等等,但是在分库分表的情况下,要考虑的内容比较多,比如 ID 重复问题,如何根据 ID 将数据分配到对应的数据库、表等。
sharding-jdbc 提供了两种主键生成策略,分别是 UUID 和 snowflake,UUID 对数据库索引不友好,绝大部分情况都是使用 snowflake,在 sharding-jdbc 中默认使用的也是 snowflake。
UUID 和 snowflake 在 sharding-jdbc 中都有对应的内置策略类,分别是UUIDShardingKeyGenerator
、SnowflakeShardingKeyGenerator
。可以通过在application.yml
配置文件中指定对应。
配置文件的写法
spring:
application:
name: shardingjdbc
shardingsphere:
datasource: # 指定数据源
names: ds0,ds1,ds2 # 指定数据源的别名,可以和真实的数据库名不同
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/database_1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/database_2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
ds2:
type: com.alibaba.druid.pool.DruidDataSource
driver: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/database_3?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
props:
sql.show: true # 表示在日志中打印执行的sql,方便测试分片是否成功
sharding: # 指定分片策略
tables:
t_user:
key-generator:
column: id
type: snowflake # 指定主键生成的策略
props:
worker:
id: 1 # snowflake需要指定当前服务器的节点编号,防止主键重复,比如后端部署3台机器,保证每台编号不同,取值随意
actualDataNodes: ds$->{0..2}.t_user # 这里的数据使用的是别名,对应定义数据源时指定的
database-strategy:
standard:
sharding-column: id # 指定分片键
# 指定精准分片算法策略
precise-algorithm-class-name: top.iweek.sharding.DatasourcePreciseAlgorithm
# 指定范围分片算法策略
range-algorithm-class-name: top.iweek.sharding.DatasourceRangeAlgorithm
table-strategy:
standard:
sharding-column: id
precise-algorithm-class-name: top.iweek.sharding.DataTablePreciseAlgorithm
range-algorithm-class-name: top.iweek.sharding.DataTableRangeAlgorithm
注意内容:
- 当主键使用 sharding-jdbc 生成策略的时候,在写
insert
SQL 的时候,不要指定 id,否则会报错,示例如下:
--- 常规写法
insert into t_user(id,username,age) values (#{id},#{username},#{age});
--- 使用sharding-jdbc以后的写法
insert into t_user(username,age) values (#{username},#{age});
如果不这样写,一般会报一个错误,错误信息如下:
java.lang.IllegalArgumentException: Sharding value must implements Comparable.
- snowflake 算法是在每个服务中的,ID 中包含时间戳、机器码、序列号和高位,高位值固定式 0,序列号是同一毫秒内产生的不同 ID,最大值是 4095。如果此时两台机器的机器码没有特殊指定,当出现时间戳、序列号相同,就会出现重复 ID 问题。因此在配置的之后一定要指定机器码。也就是配置文件中的
worker.id
。具体 snowflake 实现原理移步:分布式唯一 Id(雪花算法):原理+对比+方案
策略算法的实现
本案例根据精准分片来做的,需要实现PreciseShardingAlgorithm
接口。实现逻辑如下:
- 数据库分片规则:通过策略算法确定路由到的数据库名称
@Component
@Slf4j
public class DatasourcePreciseAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
// 获取分片列的名字,根据配置文件确定这里是id
String columnName = preciseShardingValue.getColumnName();
// 获取分片列对应的值
long columnValue =preciseShardingValue.getValue();
log.info("columnName=>{},columnValue=>{}", columnName, columnValue);
// 计算确定具体的分片
int dbIndex = (int)(columnValue % 3L);
// 返回分片数据库的名称
// collection集合内容["ds0","ds1","ds2"],对应配置文件中指定的数据库别名列表
return (String)collection.toArray()[dbIndex];
}
}
- 数据表分片规则:通过策略算法确定路由到的数据表名称
@Component
@Slf4j
public class DataTablePreciseAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
// 每个数据库中只有一个t_user表,这里不用分片,直接使用集合中第一个即可
// collection集合是表的集合["t_user"]
// 如果分为多个表的时候,示例集合["t_user_0","t_user_1","t_user_2"]
// 通过分片算法计算出结果,确定使用具体表名并返回
return (String)collection.toArray()[0];
}
}
sharding-jdbc 执行过程原理
先看图:
- SQL 解析:主要是词法和语法的解析。
- SQL 路由:根据分片规则配置以及解析上下文中的分片条件,将 SQL 定位至真正的数据源。路由分类如下:
SQL 改写:将 SQL 改写为在真实数据库中可以正确执行的语句。SQL 改写分为正确性改写和优化改写。
SQL 执行:通过多线程执行器异步执行。
结果归并:将多个执行结果集归并以便于通过统一的 JDBC 接口输出。结果归并包括流式归并、内存归并和使用装饰者模式的追加归并这几种方式。