Sharding-JDBC 按照时间进行分表

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的实现

还是使用LocalDateTime类型的泛型,还是重新实现 doSharding 方法

甚至,doSharding 方法的两个参数的含义都与前面一样。

唯一的区别就是,因为是范围查找,rangeShardingValue 对象的内容,多了 lowerEndpoint() 和 upperEndpoint()。说白了,起止时间呗,有始有终。

然后还是老一套,遍历,循环,最后得到一个或者多个符合条件的数据源节点,然后,就去数据源中寻找数据就好了。

public class MyRangeShardingAlgorithm 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<LocalDateTime> {
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<LocalDateTime> 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种类型,它们是组合,而非互斥的关系。

归并引擎的整体结构划分如下:

在这里插入图片描述

结果归并从结构上,分为 流式归并,内存归并,和 装饰着归并流式归并内存归并 是互斥的,装饰者归并可以在流式归并内存归并之上做进一步的处理。

  • 内存归并:将所有的数据遍历存储在内存中,再通过统一的分组,排序及聚合计算之后,最后将数据封装成逐条访问的数据结果集并且返回。
  • 流式归并:指每一次从数据库结果集中获取到数据,都能够通过游标逐条获取的方式返回正确的单条数据,它与原生数据库的返回结果集最为契合。
  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值