spring boot项目整合 sharding jdbc 实现按日期(String)水平分表

基于原有项目 对check_log  push_log 和 receives表根据日期字段(字段类型为varchar) 按日分表。

以check_log 为例,以下是check_date格式,注意他是varchar

正因为他是varchar 类型 ,所有需要用到精准分片和范围分片,配置文件如下。

配置文件

spring.shardingsphere.enabled=true
spring.shardingsphere.props.sql.show=true
spring.main.allow-bean-definition-overriding=true
spring.shardingsphere.datasource.names=sharding
spring.shardingsphere.datasource.sharding.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.sharding.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.sharding.url=jdbc:mysql://localhost:3306/test
spring.shardingsphere.datasource.sharding.username=root
spring.shardingsphere.datasource.sharding.password=mima

spring.shardingsphere.sharding.tables.check_log.actual-data-nodes=sharding.check_log
spring.shardingsphere.sharding.tables.check_log.table-strategy.standard.sharding-column=check_date
spring.shardingsphere.sharding.tables.check_log.table-strategy.standard.precise-algorithm-class-name=com.example.demo.test.TimeShardingAlgorithm
spring.shardingsphere.sharding.tables.check_log.table-strategy.standard.range-algorithm-class-name=com.example.demo.test.TimeShardingAlgorithm


spring.shardingsphere.sharding.tables.push_log.actual-data-nodes=sharding.push_log
spring.shardingsphere.sharding.tables.push_log.table-strategy.standard.sharding-column=push_date
spring.shardingsphere.sharding.tables.push_log.table-strategy.standard.precise-algorithm-class-name=com.example.demo.test.TimeShardingAlgorithm
spring.shardingsphere.sharding.tables.push_log.table-strategy.standard.range-algorithm-class-name=com.example.demo.test.TimeShardingAlgorithm


spring.shardingsphere.sharding.tables.receives.actual-data-nodes=sharding.receives
spring.shardingsphere.sharding.tables.receives.table-strategy.standard.sharding-column=create_time
spring.shardingsphere.sharding.tables.receives.table-strategy.standard.precise-algorithm-class-name=com.example.demo.test.TimeShardingAlgorithm
spring.shardingsphere.sharding.tables.receives.table-strategy.standard.range-algorithm-class-name=com.example.demo.test.TimeShardingAlgorithm

spring 工具类

package com.example.demo.test;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

/**
 * <p> @Title SpringUtil
 * <p> @Description Spring工具类
 */
@Component
public class SpringUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtil.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return SpringUtil.applicationContext;
    }

    public static <T> T getBean(Class<T> cla) {
        return applicationContext.getBean(cla);
    }

    public static <T> T getBean(String name, Class<T> cal) {
        return applicationContext.getBean(name, cal);
    }

    public static String getProperty(String key) {
        return applicationContext.getBean(Environment.class).getProperty(key);
    }
}

分片表缓存枚举类

package com.example.demo.test;

import java.util.*;

/**
 * <p> @Title ShardingTableCacheEnum
 * <p> @Description 分片表缓存枚举
 */
public enum ShardingTableCacheEnum {


    CHECKLOG("check_log", new HashSet<>()),

    PUSHLOG("push_log", new HashSet<>()),

    RECEIVES("receives", new HashSet<>());

    /**
     * 逻辑表名
     */
    private final String logicTableName;
    /**
     * 实际表名
     */
    private final Set<String> resultTableNamesCache;

    private static Map<String, ShardingTableCacheEnum> valueMap = new HashMap<>();

    static {
        Arrays.stream(ShardingTableCacheEnum.values()).forEach(o -> valueMap.put(o.logicTableName, o));
    }

    ShardingTableCacheEnum(String logicTableName, Set<String> resultTableNamesCache) {
        this.logicTableName = logicTableName;
        this.resultTableNamesCache = resultTableNamesCache;
    }

    public static ShardingTableCacheEnum of(String value) {
        return valueMap.get(value);
    }

    public String logicTableName() {
        return logicTableName;
    }

    public Set<String> resultTableNamesCache() {
        return resultTableNamesCache;
    }

    public static Set<String> logicTableNames() {
        return valueMap.keySet();
    }

    @Override
    public String toString() {
        return "ShardingTableCacheEnum{" +
                "logicTableName='" + logicTableName + '\'' +
                ", resultTableNamesCache=" + resultTableNamesCache +
                '}';
    }
}

分片规则实现类

package com.example.demo.test;

import cn.hutool.core.date.DateUtil;
import com.example.demo.utils.DateUtils;
import com.google.common.collect.Range;
import lombok.extern.slf4j.Slf4j;
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 java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.function.Function;

/**
 * 按天分片
 */
@Slf4j
public class TimeShardingAlgorithm implements PreciseShardingAlgorithm<String>, RangeShardingAlgorithm<String> {

    /**
     * 精准分片
     *
     * @param tableNames           对应分片库中所有分片表的集合
     * @param preciseShardingValue 分片键值,其中 logicTableName 为逻辑表,columnName 分片键,value 为从 SQL 中解析出来的分片键的值
     * @return 表名
     */
    @Override
    public String doSharding(Collection<String> tableNames, PreciseShardingValue<String> preciseShardingValue) {
        String logicTableName = preciseShardingValue.getLogicTableName();
        ShardingTableCacheEnum logicTable = ShardingTableCacheEnum.of(logicTableName);
        if (logicTable == null) {
            log.error(">>>>>>>>>> 【ERROR】数据表类型错误,请稍后重试,logicTableNames:{},logicTableName:{}",
                    ShardingTableCacheEnum.logicTableNames(), logicTableName);
            throw new IllegalArgumentException("数据表类型错误,请稍后重试");
        }

        log.info(">>>>>>>>>> 【INFO】精确分片,节点配置表名:{},数据库缓存表名:{}", tableNames, logicTable.resultTableNamesCache());

        String dateTime = preciseShardingValue.getValue();
        String date = dateTime.replaceAll("-", "_");

        String resultTableName = logicTableName + "_" + date;
        // 检查分表获取的表名是否存在,不存在则自动建表

        return ShardingAlgorithmTool.getShardingTableAndCreate(logicTable.logicTableName(), resultTableName);
    }

    /**
     * 范围分片
     *
     * @param tableNames         对应分片库中所有分片表的集合
     * @param rangeShardingValue 分片范围
     * @return 表名集合
     */
    @Override
    public Collection<String> doSharding(Collection<String> tableNames, RangeShardingValue<String> rangeShardingValue) {
        String logicTableName = rangeShardingValue.getLogicTableName();
        ShardingTableCacheEnum logicTable = ShardingTableCacheEnum.of(logicTableName);
        if (logicTable == null) {
            log.error(">>>>>>>>>> 【ERROR】逻辑表范围异常,请稍后重试,logicTableNames:{},logicTableName:{}",
                    ShardingTableCacheEnum.logicTableNames(), logicTableName);
            throw new IllegalArgumentException("逻辑表范围异常,请稍后重试");
        }
        log.info(">>>>>>>>>> 【INFO】范围分片,节点配置表名:{},数据库缓存表名:{}", tableNames, logicTable.resultTableNamesCache());


        // between and 的起始值
        Range<String> valueRange = rangeShardingValue.getValueRange();
        boolean hasLowerBound = valueRange.hasLowerBound();
        boolean hasUpperBound = valueRange.hasUpperBound();

        // 获取最大值和最小值
        Set<String> tableNameCache = logicTable.resultTableNamesCache();
        String min = hasLowerBound ? valueRange.lowerEndpoint() :getLowerEndpoint(tableNameCache);
        String max = hasUpperBound ? valueRange.upperEndpoint() :getUpperEndpoint(tableNameCache);

        // 循环计算分表范围
        Set<String> resultTableNames = new LinkedHashSet<>();
        while (min.compareTo(max)<0 || min.equals(max)) {
            String temp = min.replaceAll("-", "_");
            String tableName = logicTableName + "_" + temp;
            resultTableNames.add(tableName);
            Date date = null;
            try {
                date = new SimpleDateFormat("yyyy-MM-dd").parse(min);
            } catch (ParseException e) {
                throw new RuntimeException(e);
            }
            min = DateUtil.offsetDay(date, 1).toString("yyyy-MM-dd");
        }
        // 循环计算分表范围
        return ShardingAlgorithmTool.getShardingTablesAndCreate(logicTable.logicTableName(), resultTableNames);
    }


    /**
     * 获取 最小分片值
     * @param tableNames 表名集合
     * @return 最小分片值
     */
    private String getLowerEndpoint(Collection<String> tableNames) {
        Optional<Date> optional = tableNames.stream()
                .map(o -> DateUtils.dateTime("DATE_TIME_FORMATTER_STRING",o.replace("TABLE_SPLIT_SYMBOL", "") + "01 00:00:00"))
                .min(Comparator.comparing(Function.identity()));
        if (optional.isPresent()) {
            return DateUtil.offsetDay(optional.get(), 1).toString("yyyy-MM-dd");
        } else {
            log.error("====> 【ERROR】获取数据最小分表失败,请稍后重试,tableName:{}", tableNames);
            throw new IllegalArgumentException("获取数据最小分表失败,请稍后重试");
        }
    }

    /**
     * 获取 最大分片值
     * @param tableNames 表名集合
     * @return 最大分片值
     */
    private String getUpperEndpoint(Collection<String> tableNames) {
        Optional<Date> optional = tableNames.stream()
                .map(o ->  DateUtils.dateTime("DATE_TIME_FORMATTER_STRING",o.replace("TABLE_SPLIT_SYMBOL", "") + "01 00:00:00"))
                .max(Comparator.comparing(Function.identity()));
        if (optional.isPresent()) {
            return DateUtil.offsetDay(optional.get(), 1).toString("yyyy-MM-dd");
        } else {
            log.error("====> 【ERROR】获取数据最大分表失败,请稍后重试,tableName:{}", tableNames);
            throw new IllegalArgumentException("获取数据最大分表失败,请稍后重试");
        }
    }

}

分片算法工具

package com.example.demo.test;

import cn.hutool.core.collection.ConcurrentHashSet;
import com.alibaba.druid.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.core.rule.DataNode;
import org.apache.shardingsphere.core.rule.TableRule;
import org.apache.shardingsphere.shardingjdbc.jdbc.core.datasource.ShardingDataSource;
import org.springframework.core.env.Environment;

import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.*;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p> @Title ShardingAlgorithmTool
 * <p> @Description 按天分片算法工具
 */
@Slf4j
public class ShardingAlgorithmTool {

    private static volatile ShardingAlgorithmTool instance;

    /**
     * 逻辑表名,例:order
     */
    private final static List<String> LOGIC_TABLE_NAME
            = new ArrayList<>(Arrays.asList(
            "check_log",
            "push_log",
            "receives"
    ));


    /**
     * 表分片符号,例:order_20240808 中,分片符号为 "_"
     */
    private static final String TABLE_SPLIT_SYMBOL = "_";

    /**
     * 已存在表名集合缓存
     */
    private static final Set<String> TABLE_NAME_CACHE = new ConcurrentHashSet<>();

    /**
     * 数据库配置
     */
    private static final Environment ENV = SpringUtil.getApplicationContext().getEnvironment();
    private static final String DATASOURCE_URL = ENV.getProperty("spring.shardingsphere.datasource.sharding.url");
    private static final String DATASOURCE_USERNAME = ENV.getProperty("spring.shardingsphere.datasource.sharding.username");
    private static final String DATASOURCE_PASSWORD = ENV.getProperty("spring.shardingsphere.datasource.sharding.password");

    private ShardingAlgorithmTool(){

    }
    public static ShardingAlgorithmTool getInstance(){
        if (instance == null) {
            synchronized (ShardingAlgorithmTool.class) {
                if (instance == null) {
                    instance = new ShardingAlgorithmTool();
                }
            }
        }
        return instance;
    }
    /**
     * 检查分表获取的表名是否存在,不存在则自动建表
     *
     * @param logicTableName  逻辑表名,例:order
     * @param resultTableName 真实表名,例:order_20240808
     * @return 确认存在于数据库中的真实表名
     */
    public static String getShardingTableAndCreate(String logicTableName, String resultTableName) {
        // 缓存中有此表则返回,没有则判断创建
        if (TABLE_NAME_CACHE.contains(resultTableName)) {
            return resultTableName;
        } else {
            // 未创建的表返回逻辑空表
            boolean isSuccess = createShardingTable(logicTableName, resultTableName);
            return isSuccess ? resultTableName : logicTableName;
        }
    }



    /**
     * 检查分表获取的表名是否存在,不存在则自动建表
     *
     * @param logicTableName   逻辑表名,例:order
     * @param resultTableNames 真实表名,例:order_20240808
     * @return 存在于数据库中的真实表名集合
     */
    public static Set<String> getShardingTablesAndCreate(String logicTableName, Collection<String> resultTableNames) {
        return resultTableNames.stream().map(o -> getShardingTableAndCreate(logicTableName, o)).collect(Collectors.toSet());
    }

    /**
     * 缓存重载
     */
    public static void tableNameCacheReload() {
        // 读取数据库中|所有表名
        List<String> tableNameList = getAllTableNameBySchema();
        // 删除旧的缓存(如果存在)
        TABLE_NAME_CACHE.clear();
        // 写入新的缓存
        TABLE_NAME_CACHE.addAll(tableNameList);
        // 动态更新配置 actualDataNodes
        actualDataNodesRefresh();
    }

    /**
     * 获取所有表名
     *
     * @return 表名集合
     */
    public static List<String> getAllTableNameBySchema() {
        List<String> tableNames = new ArrayList<>();
        if (StringUtils.isEmpty(DATASOURCE_URL) || StringUtils.isEmpty(DATASOURCE_USERNAME) || StringUtils.isEmpty(DATASOURCE_PASSWORD)) {
            log.error(">>>>>>>>>> 【ERROR】数据库连接配置有误,请稍后重试,URL:{}, username:{}, password:{}", DATASOURCE_URL, DATASOURCE_USERNAME, DATASOURCE_PASSWORD);
            throw new IllegalArgumentException("数据库连接配置有误,请稍后重试");
        }
        try (Connection conn = DriverManager.getConnection(DATASOURCE_URL, DATASOURCE_USERNAME, DATASOURCE_PASSWORD);
             Statement st = conn.createStatement()) {
            for (int i=0;i<LOGIC_TABLE_NAME.size();i++){
                try (ResultSet rs = st.executeQuery("show TABLES like '" + LOGIC_TABLE_NAME.get(i) + TABLE_SPLIT_SYMBOL + "%'")) {
                    while (rs.next()) {
                        tableNames.add(rs.getString(1));
                    }
                }
            }
        } catch (SQLException e) {
            log.error(">>>>>>>>>> 【ERROR】数据库连接失败,请稍后重试,原因:{}", e.getMessage(), e);
            throw new IllegalArgumentException("数据库连接失败,请稍后重试");
        }
        return tableNames;
    }

    /**
     * 获取表名缓存
     *
     * @return 表名缓存
     */
    public static Set<String> getTableNameCache() {
        return TABLE_NAME_CACHE;
    }

    /**
     * 动态更新配置 actualDataNodes
     */
    public static void actualDataNodesRefresh() {
        try {
            // 获取数据分片节点
            Set<String> tableNameCache = ShardingAlgorithmTool.getTableNameCache();
            ShardingDataSource dataSource = (ShardingDataSource) SpringUtil.getBean("dataSource", DataSource.class);
            for (int i=0;i<LOGIC_TABLE_NAME.size();i++){
                TableRule tableRule = dataSource.getShardingContext().getShardingRule().getTableRule(LOGIC_TABLE_NAME.get(i));
                List<DataNode> dataNodes = tableRule.getActualDataNodes();
                String dataSourceName = dataNodes.get(0).getDataSourceName();
                List<DataNode> newDataNodes = tableNameCache.stream().map(tableName -> new DataNode(dataSourceName + "." + tableName)).collect(Collectors.toList());

                // 更新actualDataNodes
                Field actualDataNodesField = TableRule.class.getDeclaredField("actualDataNodes");
                Field modifiersField = Field.class.getDeclaredField("modifiers");
                modifiersField.setAccessible(true);
                modifiersField.setInt(actualDataNodesField, actualDataNodesField.getModifiers() & ~Modifier.FINAL);
                actualDataNodesField.setAccessible(true);
                actualDataNodesField.set(tableRule, newDataNodes);
            }

        } catch (Exception e) {
            log.error("初始化 动态表单失败,原因:{}", e.getMessage(), e);
        }
    }


    /**
     * 创建分表
     *
     * @param logicTableName  逻辑表名,例:order
     * @param resultTableName 真实表名,例:order_20240808
     * @return 创建结果(true创建成功,false未创建)
     */
    public static boolean createShardingTable(String logicTableName, String resultTableName) {
        synchronized (logicTableName.intern()) {
            // 缓存中有此表 返回
            if (TABLE_NAME_CACHE.contains(resultTableName)) {
                return false;
            }
            // 缓存中无此表,则建表并添加缓存
            executeSql(Collections.singletonList("CREATE TABLE `" + resultTableName + "` LIKE `" + logicTableName + "`;"));
            // 缓存重载
            tableNameCacheReload();
        }
        return true;
    }

    /**
     * 执行SQL
     *
     * @param sqlList SQL集合
     */
    private static void executeSql(List<String> sqlList) {
        if (StringUtils.isEmpty(DATASOURCE_URL) || StringUtils.isEmpty(DATASOURCE_USERNAME) || StringUtils.isEmpty(DATASOURCE_PASSWORD)) {
            log.error(">>>>>>>>>> 【ERROR】数据库连接配置有误,请稍后重试,URL:{}, username:{}, password:{}", DATASOURCE_URL, DATASOURCE_USERNAME, DATASOURCE_PASSWORD);
            throw new IllegalArgumentException("数据库连接配置有误,请稍后重试");
        }
        try (Connection conn = DriverManager.getConnection(DATASOURCE_URL, DATASOURCE_USERNAME, DATASOURCE_PASSWORD)) {
            try (Statement st = conn.createStatement()) {
                conn.setAutoCommit(false);
                for (String sql : sqlList) {
                    st.execute(sql);
                }
            } catch (Exception e) {
                conn.rollback();
                log.error(">>>>>>>>>> 【ERROR】数据表创建执行失败,请稍后重试,原因:{}", e.getMessage(), e);
                throw new IllegalArgumentException("数据表创建执行失败,请稍后重试");
            }
        } catch (SQLException e) {
            log.error(">>>>>>>>>> 【ERROR】数据库连接失败,请稍后重试,原因:{}", e.getMessage(), e);
            throw new IllegalArgumentException("数据库连接失败,请稍后重试");
        }
    }
}

项目启动后读取分表进行缓存

package com.example.demo.test;

import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * <p> @Title ShardingTablesLoadRunner
 * <p> @Description 项目启动后,读取已有分表,进行缓存
 */
@Order(value = 1) // 数字越小,越先执行
@Component
public class ShardingTablesLoadRunner implements CommandLineRunner {

    @Override
    public void run(String... args) {
        // 读取已有分表,进行缓存
        ShardingAlgorithmTool.tableNameCacheReload();
    }
}

参考ShardingJDBC分表使用-CSDN博客​​​​​​

忘了是不是这个了

其中的分表规则不能满足我的需求,所以做了改动然后发了出来 ,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值