Sharding-jdbc实现分库分表一些尝试

787 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第20天,点击查看活动详情

话不多说,直接开始。

一、引入依赖

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.1.2</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

二、yml配置

包含数据源配置、数据节点配置、分片策略配置、主键生成策略等配置

server:
  port: 9088


spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.alibaba.druid.pool.DruidDataSource
        url: jdbc:mysql://localhost:3306/order?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
        username: root
        password: root
      ds1:
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.alibaba.druid.pool.DruidDataSource
        url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
        username: root
        password: root
    enabled: true
    mode:
      type: memory
    props:
      sql-show: true
    rules:
      sharding:
        default-key-generate-strategy:
          column: id
        key-generators:
          id-key:
            type: SNOWFLAKE
        tables:
          # 需要分表的逻辑表名, 如果需要配置多个数据表,按照这个规则配置就可以
          test_data:
            # 表的真实表列表(需要注意这里的表都应该存在,否则就有可能出现我第一篇文章里面提到的 NPE 异常)
            actual-data-nodes: ds0.test_data_20220${8..9}, ds0.test_data_2022${10..12}
            # 分片策略配置
            table-strategy:
              standard:
                # 分片算法是 his_month_sharding, 这个名称是在下面配置的
                sharding-algorithm-name: his_month_sharding
                # 分片列是 acquisition_time, 需要注意的是,分表算法的数据类型一定要和这个分片的列数据类型一致
                sharding-column: create_time
        # 配置分片算法
        sharding-algorithms:
          # 分片算法名称
          his_month_sharding:
            # 分片算法类型,这个type就是我们的分片算法实现类中 getType() 的返回值,SPI适用于这种方式
            type: HIS_DATA_SPI_BASED

三、实现分片算法

这里主要标准分片算法接口,官方保留了接口,具体算法需要自己动手实现。通过分片键create_time实现范围查找和精确查找。


@Slf4j
public class TstDataMonthShardingAlgorithm implements StandardShardingAlgorithm<Date> {

    private final ThreadLocal<SimpleDateFormat> formatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMM"));

    private Properties props;
    /**
     * 设置该参数的原因是,如果在范围查找的时候我们没有设置最小值,比如下面的查询
     * where acquisition_time < '2022-08-11 00:00:00'
     * 这个时候范围查找就只有上限而没有下限,这时候就需要有一个下限值兜底,不能一致遍历下去
     */
    private Date tableLowerDate;


    /**
     *
     * @param availableTargetNames 可用的表列表(配置文件中配置的 actual-data-nodes会被解析成 列表被传递过来)
     * @param shardingValue        精确的值
     * @return
     */
    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {
        Date value = shardingValue.getValue();
        // 根据精确值获取路由表
        String actuallyTableName = shardingValue.getLogicTableName() + shardingSuffix(value);
        if (availableTargetNames.contains(actuallyTableName)) {
            return actuallyTableName;
        }
        return null;
    }

    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Date> shardingValue) {
        // 获取到范围查找的最小值,如果条件中没有最小值设置为 tableLowerDate
        Date rangeLowerDate;
        if (shardingValue.getValueRange().hasLowerBound()) {
            rangeLowerDate = shardingValue.getValueRange().lowerEndpoint();
        } else {
            rangeLowerDate = tableLowerDate;
        }

        // 获取到范围查找的最大值,如果没有配置最大值,设置为当前时间 + 1 月
        // 这里需要注意,你的项目里面这样做是否合理
        Date rangeUpperDate;
        if (shardingValue.getValueRange().hasUpperBound()) {
            rangeUpperDate = shardingValue.getValueRange().upperEndpoint();
        } else {
            // 往后延一个月
            rangeUpperDate = DateUtil.offsetMonth(new Date(), 1);
        }
        rangeUpperDate = DateUtil.endOfMonth(rangeUpperDate);
        List<String> tableNames = new ArrayList<>();
        // 过滤那些存在的表
        while (rangeLowerDate.before(rangeUpperDate)) {
            String actuallyTableName = shardingValue.getLogicTableName() + shardingSuffix(rangeLowerDate);
            if (availableTargetNames.contains(actuallyTableName)) {
                tableNames.add(actuallyTableName);
            }
            rangeLowerDate = DateUtil.offsetMonth(rangeLowerDate, 1);
        }
        return tableNames;
    }

    /**
     * sharding 表后缀 _yyyyMM
     */
    private String shardingSuffix(Date shardingValue) {
        return "_" + formatThreadLocal.get().format(shardingValue);
    }

    /**
     * SPI方式的 SPI名称,配置文件中配置的时候需要用到
     */
    @Override
    public String getType() {
        return "HIS_DATA_SPI_BASED";
    }

    @Override
    public void init(Properties properties) {

        this.props = properties;
        String autoCreateTableLowerDate = properties.getProperty("auto-create-table-lower");
        try {
            this.tableLowerDate = formatThreadLocal.get().parse(autoCreateTableLowerDate);
        } catch (Exception e) {
            log.error("parse auto-create table lower date failed: {}, use default date 202208", e.getMessage());
            try {
                this.tableLowerDate = formatThreadLocal.get().parse("202208");
            } catch (ParseException ignored) {
            }
        }
    }

    @Override
    public Properties getProps() {
        return this.props;
    }
}

四、使用SPI注册服务

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。

1、在resources目录下新建META-INF/services目录

    -- resources
        -- META-INF
            -- services

2、在这个目录下新建一个与上述接口的全限定名一致的文件 image.png

3、在这个文件中写入接口的实现类的全限定名 image.png

五、操作尝试

1、新增数据,根据createTime放入不同的表 { "name":"8月5", "createTime":"2022-09-05 12:00:00" }

image.png

2、根据ID=1600390521566187522查询数据

image.png

3、根据日期区间查询

image.png