Sharding-JDBC 按照时间进行分表
一 前言
实际项目中,一般不涉及时间的业务数据,我们可以按照 Sharding-JDBC 中的 inline模式
进行分片。
可如果涉及按时间顺序的业务数据,尤其是跨时间段的业务数据,则需要按照按照时间来进行分片。
在 SQL 语句中,按照时间查询时,我们一般使用 between
,如果将时间设置为字符串,则还可以使用 >=
,<=
来编写SQL语句。
不过可惜的是,inline模式
虽然简单好用,但却只支持SQL语句中的 =
和in
,显然无法满足我们的需求,这个时候,我们就不得不使用standard模式
来实现了。
提前说明,LocalDateTime类型
默认是不被Sharding-JDBC支持的,所以建议将 LocalDateTime类型
转喊成Date
或者String
使用。
否则,会在最后的数据归并阶段报错。
二 standard模式
standard模式
,即标准模式,与inline模式
类似,它也是一种单分片键模式。使用起来相对复杂,因为它要我们自己实现数据分片逻辑。
划重点,自己实现分片逻辑
千万不要被吓到,自己实现分片逻辑,其实也很简单。归根结底,就是我们需要自己根据数据库的实际情况,去决定数据应该落在哪个库的哪张表。
下面先介绍两个关键类,这两个类支持泛型,方便我们定义任何类型的分片键
- PreciseShardingAlgorithm:定义数据落到哪个数据库中的哪张表
- RangeShardingAlgorithm:定义数据应该去哪个数据库中的哪张表里去查询
三 SprongBoot中实现standard模式的分库分表
一 引入相关依赖
以下是实现分库分表的关键依赖项,至于版本号,根据实际情况来
- sharding-jdbc-spring-boot-starter:核心依赖,实现分库分表就靠它
- druid-spring-boot-starter:非必须,但是建议。使用它来管理数据库连接池
- mybatis-plus-boot-starter:ORM框架,用来方便的操作数据库
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
二 Yaml文件中的相关配置
以下我们需要对数据库中的系统日志做按时间分表的业务,我们按照每月一个表来存储日志数据,这里先来3个月的。
实际项目中,建议大家合理的创建数据表。
spring:
application:
name: node01-time2
main:
# 允许重写bean
allow-bean-definition-overriding: true
shardingsphere:
# 配置数据源
datasource:
# 数据源名称,多个使用(,)分割
names: m1
# 数据源的详细配置,多个需要分别配置
m1:
# 数据库连接池,这里使用 druid
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/xxxx?serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: 123456
# 配置数据库分片
sharding:
# 配置的数据库表
tables:
# 数据表的逻辑表名,即一个数据表的名字,可以是不存在的
system_log:
# 数据节点,这里安排了3张表,分别是 m1数据源 下的 system_log_202306,system_log_202307,system_log_202308
# 多个库时,需要将该表所在的数据源全部写进来,使用(,)分割
actual-data-nodes: m1.system_log_${202306..202308}
# 配置主键规则
key-generator:
# 配置主键
column: id
# 配置主键算法,这里支持雪花算法(SNOWFLAKE)和 UUID ,根据情况编写即可
type: SNOWFLAKE
# 分表规则,这里的分表规则是针对当前 system_log 的规则设置,多个表需要设置不一样的规则时,则需要设置多个
table-strategy:
# 这里采用标准模式
standard:
# 分片键,当然是日志的时间啦(log_time)
sharding-column: log_time
# 数据存入规则的实现类
precise-algorithm-class-name: com.shawn.time2.config.PreciseAlgorithmCustomer
# 数据读取规则的实现类
range-algorithm-class-name: com.shawn.time2.config.RangeShardingAlgorithmCustom
# 顺道展示一下 SQL 的执行过程
props:
sql:
show: true
三 自定义分片规则的实现
前面提到了,分片的两个关键类,而且我们已经在 application.yml
配置好了两个类的实现类,现在就让我们来真正实现它吧。
1.PreciseShardingAlgorithm的实现
这里需要解释一下,我们在application.yml
中配置的log_time
使用的是String
也使用String
类型。
继承 PreciseShardingAlgorithm 并且实现 doSharding 方法,即可完成将数据存入到哪张表中。
doSharding 方法中的 collection 参数,存储了 system_log 的全部物理表名,切记,此处是物理表名。也就是前边在 yaml 中配置的相关数据源。
doSharding 方法中的 preciseShardingValue 参数,则存储的是用户保存数据时,为分片键 log_time
赋的值。
那么结果显而易见了,我们只需要将全部的物理表名循环,再与分片条件(时间)进行比较,就可以直到我们要把数据存到什么地方去了,然后再将物理表名返回即可。
当然,这期间你需要做一丢丢的格式处理。
我这里的表名使用的是年月,要与时间比较,也只能将时间转换为字符串。
public class MyPreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<String> preciseShardingValue) {
for (String str : collection) {
// 此处输出的是物理表名
String dateTimeStr = preciseShardingValue.getValue();
dateTimeStr=dateTimeStr.substring(0,7).replace("-","");
if(str.contains(dateTimeStr)){
return str;
}
}
return null;
}
}
然后,你的数据就可以按照你制定的规则,乖乖的存入指定的数据表中了。
可是,怎么读出来了?或者说,6,7月份数据,我只想去6,7月份的表中去查询。
下面就一起来实现。
2.RangeShardingAlgorithm的实现
还是使用String
类型的泛型,还是重新实现 doSharding 方法
甚至,doSharding 方法的两个参数的含义都与前面一样。
唯一的区别就是,因为是范围查找,rangeShardingValue 对象的内容,多了 lowerEndpoint() 和 upperEndpoint()。说白了,起止时间呗,有始有终。
然后还是老一套,遍历,循环,最后得到一个或者多个符合条件的数据源节点,然后,就去数据源中寻找数据就好了。
public class RangeShardingAlgorithmCustom implements RangeShardingAlgorithm<String> {
@Override
public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<String> rangeShardingValue) {
Range<String> valueRange = rangeShardingValue.getValueRange();
//开始时间,范围值的上限
Integer timeStart = Integer.valueOf(valueRange.lowerEndpoint().toString().substring(0, 7).replace("-", ""));
//截止时间,范围值的下限
Integer timeEnd = Integer.valueOf(valueRange.upperEndpoint().toString().substring(0, 7).replace("-", ""));
Collection<String> list = new ArrayList<>();
for (String s : collection) {
int startIndex = s.lastIndexOf('_') + 1;
Integer time = Integer.valueOf(s.substring(startIndex, startIndex + 6));
if (timeStart <= time && time <= timeEnd) {
list.add(s);
}
}
return list;
}
}
3.对于数据节点配置的问题
对于数据节点的问题,如果有两年的数据节点,则你可能需要这样配置:
# 示例1
sharding:
tables:
system_log:
actual-data-nodes: m1.system_log_${202306..202312},m1.system_log_${202401..202412}
为什么呢?因为${202401…202412} 是 Groovy 的语法,是表示枚举的一种省略写法。在实际情况中,只有1~12月,
如果你写成了:
# 错误示例1
actual-data-nodes: m1.system_log_${202301..202401}
则它会自动编译出 202313,202314…直到202401,这显然是不合理的。并且我们没有那些节点,所以当这些错误的节点被选中时,程序必然会报错。
但是示例1的写法,随着项目运行的时间越久,后面要追加的数据节点就越多,这也是相当麻烦的。
可以考虑将这一块的内容在自定义的分片算法中去处理,在循环物理表时,排除掉错误的时间节点。这也是一种处理办法。
比如使用以下逻辑,错误实例1
就会变成正确且简单好用
的办法:
public class PreciseAlgorithmCustomer implements PreciseShardingAlgorithm<String> {
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<String> preciseShardingValue) {
for (String str : collection) {
// 取物理表名的最后两位,跳过不符合条件的节点
Integer month = Integer.valueOf(str.substring(str.length() - 2, str.length()));
if (month > 12 || month==0) {
continue;
}
// 此处输出的是物理表名
String dateTimeStr = preciseShardingValue.getValue().toString();
dateTimeStr = dateTimeStr.substring(0, 7).replace("-", "");
if (str.contains(dateTimeStr)) {
return str;
}
}
return null;
}
}
小结:知道了实现过程,其实很多细节问题都可以由我们自己来实现了。以上内容只是提供思路。
四 分库
分库,无非是多增加几个数据源,然后先确定进哪个库,然后再确定进哪张表。
具体可参考:Sharding-JDBC分库分表_sharding分库分表配置_我不配拥有55kg的你的博客-CSDN博客
五 结果归并
将各个数据节点的数据合并为一个结果集并正确的返回给客户端,称之为结果归并
Sha
Sharding-JDBC 支持的结果归并从功能上可能分为 遍历,排序,分组,分页 和 聚合 5种类型,它们是组合,而非互斥的关系。
归并引擎的整体结构划分如下:
结果归并从结构上,分为 流式归并,内存归并,和 装饰着归并。流式归并
和 内存归并
是互斥的,装饰者归并
可以在流式归并
和内存归并
之上做进一步的处理。
- 内存归并:将所有的数据遍历存储在内存中,再通过统一的分组,排序及聚合计算之后,最后将数据封装成逐条访问的数据结果集并且返回。
- 流式归并:指每一次从数据库结果集中获取到数据,都能够通过游标逐条获取的方式返回正确的单条数据,它与原生数据库的返回结果集最为契合。