若依前后端分离版集成ShardingSphere-补充版代码演示

  1. 拉取项目:https://gitee.com/y_project/RuoYi-Vue。前后端分离版本
  2. 新建数据库,字符集选择utf8mb4。导入mysql文件。
    在这里插入图片描述
  3. 主pom文件中引入依赖
<!-- 分库分表引擎 -->
 <dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.2.0</version>
</dependency>

在这里插入图片描述
4. framework模块也引入shardingsphere-jdbc-core-spring-boot-starter

  <!-- Sharding-JDBC分库分表 -->
  <dependency>
      <groupId>org.apache.shardingsphere</groupId>
      <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
  </dependency>

在这里插入图片描述

  1. 在admin模块中新增sharding.yml配置文件,详细定义分库分表策略。我以sys_notice和sys_job为例
    在这里插入图片描述
# 数据源配置
spring:
    datasource:
#        type: com.alibaba.druid.pool.DruidDataSource
#        driverClassName: com.mysql.cj.jdbc.Driver
        driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
        url: jdbc:shardingsphere:classpath:sharding.yaml
    # 多数据源
    dynamic:
        datasource:
            druid:
                # 主库数据源
                master:
                    dataSourceClassName: com.alibaba.druid.pool.DruidDataSource
                    driverClassName: com.mysql.cj.jdbc.Driver
                    url: jdbc:mysql://172.16.61.16:3306/sharding_demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                    username: root
                    password: Www_112379
                # 从库数据源
                slave:
                    # 从数据源开关/默认关闭
                    enabled: false
                    url:
                    username:
                    password:
                # 初始连接数
                initialSize: 5
                # 最小连接池数量
                minIdle: 10
                # 最大连接池数量
                maxActive: 20
                # 配置获取连接等待超时的时间
                maxWait: 60000
                # 配置连接超时时间
                connectTimeout: 30000
                # 配置网络超时时间
                socketTimeout: 60000
                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
                timeBetweenEvictionRunsMillis: 60000
                # 配置一个连接在池中最小生存的时间,单位是毫秒
                minEvictableIdleTimeMillis: 300000
                # 配置一个连接在池中最大生存的时间,单位是毫秒
                maxEvictableIdleTimeMillis: 900000
                # 配置检测连接是否有效
                validationQuery: SELECT 1 FROM DUAL
                testWhileIdle: true
                testOnBorrow: false
                testOnReturn: false
                webStatFilter:
                    enabled: true
                statViewServlet:
                    enabled: true
                    # 设置白名单,不填则允许所有访问
                    allow:
                    url-pattern: /druid/*
                    # 控制台管理用户名和密码
                    login-username: ruoyi
                    login-password: 123456
                filter:
                    stat:
                        enabled: true
                        # 慢SQL记录
                        log-slow-sql: true
                        slow-sql-millis: 1000
                        merge-sql: true
                    wall:
                        config:
                            multi-statement-allow: true

  1. 修改配置文件,支持多数据源
# 数据源配置
dataSources:
  sharding:
    dataSourceClassName: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://172.16.61.16:3306/sharding_demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: Www_112379
# 规则配置
rules:
  - !SHARDING
    # 表策略配置
    tables:
      # sys_notice,sys_job 为例
      sys_notice:
        # 配置数据节点,按年分表
        # actualDataNodes: sharding.sys_notice_$->{2023..2024}
        # actualDataNodes: sharding.sys_notice_$->{202401..202412}
        actualDataNodes: sharding.sys_notice
        tableStrategy:
          # 使用标准分片策略
          standard:
            # 配置分片字段,以哪个字段作为分片标准
            shardingColumn: create_time
            # 分片算法名称,不支持大写字母和下划线,否则启动就会报错  time-sharding-monthly-algorithm
            shardingAlgorithmName: time-sharding-altorithm
      sys_job:
        # 配置数据节点,按年分表
        actualDataNodes: sharding.sys_job
        tableStrategy:
          # 使用标准分片策略
          standard:
            # 配置分片字段,以哪个字段作为分片标准
            shardingColumn: create_time
            # 分片算法名称,不支持大写字母和下划线,否则启动就会报错
            shardingAlgorithmName: time-sharding-altorithm
    bindingTables:
      - sys_notice,sys_job
    # 分片算法配置
    shardingAlgorithms:
      # 分片算法名称,不支持大写字母和下划线,否则启动就会报错
      time-sharding-altorithm:
        # 类型:自定义策略
        type: CLASS_BASED
        props:
          # 分片策略
          strategy: standard
          # 分片算法类
          algorithmClassName: com.ruoyi.framework.sharding.TimeShardingAlgorithm
props:
  # 输出SQL
  sql-show: true

  1. 配置DruidProperties为动态数据源,确保需要分表的配置能够正确加载。修改framework下的DruidProperties。
package com.ruoyi.framework.config.properties;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import com.alibaba.druid.pool.DruidDataSource;

/**
 * druid 配置属性
 *
 * @author ruoyi
 */
@Configuration
public class DruidProperties
{
    @Value("${spring.dynamic.datasource.druid.initialSize}")
    private int initialSize;

    @Value("${spring.dynamic.datasource.druid.minIdle}")
    private int minIdle;

    @Value("${spring.dynamic.datasource.druid.maxActive}")
    private int maxActive;

    @Value("${spring.dynamic.datasource.druid.maxWait}")
    private int maxWait;

    @Value("${spring.dynamic.datasource.druid.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;

    @Value("${spring.dynamic.datasource.druid.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;

    @Value("${spring.dynamic.datasource.druid.maxEvictableIdleTimeMillis}")
    private int maxEvictableIdleTimeMillis;

    @Value("${spring.dynamic.datasource.druid.validationQuery}")
    private String validationQuery;

    @Value("${spring.dynamic.datasource.druid.testWhileIdle}")
    private boolean testWhileIdle;

    @Value("${spring.dynamic.datasource.druid.testOnBorrow}")
    private boolean testOnBorrow;

    @Value("${spring.dynamic.datasource.druid.testOnReturn}")
    private boolean testOnReturn;

    public DruidDataSource dataSource(DruidDataSource datasource)
    {
        /** 配置初始化大小、最小、最大 */
        datasource.setInitialSize(initialSize);
        datasource.setMaxActive(maxActive);
        datasource.setMinIdle(minIdle);

        /** 配置获取连接等待超时的时间 */
        datasource.setMaxWait(maxWait);

        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);

        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);

        /**
         * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
         */
        datasource.setValidationQuery(validationQuery);
        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
        datasource.setTestWhileIdle(testWhileIdle);
        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnBorrow(testOnBorrow);
        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnReturn(testOnReturn);
        return datasource;
    }
}

  1. 在DataSourceType文件中,增加一个SHARDING类型,用于区分分表操作。
package com.ruoyi.common.enums;

/**
 * 数据源
 *
 * @author ruoyi
 */
public enum DataSourceType
{
    /**
     * 主库
     */
    MASTER,

    /**
     * 从库
     */
    SLAVE,

    /**
     * 从库
     */
    SHARDING
}

  1. 增加ShardingConfig配置
package com.ruoyi.framework.config;

import org.springframework.context.annotation.Configuration;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;

/**
 * @Description: shardingSphere分表配置
 * @Author WangHan
 * @Date 2024/9/19 15:28
 * @Version 1.0
 */
@Configuration
public class ShardingConfig {

    /** 配置文件路径 */
    private static final String CONFIG_FILE = "sharding.yaml";

    /**
     * 获取数据源配置
     */
    public static byte[] getShardingYAMLFile() throws IOException {
        InputStream inputStream = Objects.requireNonNull(
                ShardingConfig.class.getClassLoader().getResourceAsStream(CONFIG_FILE),
                String.format("Resource `%s` is not found in the classpath.", CONFIG_FILE)
        );
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        int nRead;
        byte[] data = new byte[16384]; // 创建一个合适的缓冲区大小

        // 读取InputStream到ByteArrayOutputStream
        while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
            buffer.write(data, 0, nRead);
        }

        // 将ByteArrayOutputStream转换成byte数组
        buffer.flush();
        return buffer.toByteArray();
    }
}

  1. 修改DruidConfig,增加shardingDataSource的相关配置。
package com.ruoyi.framework.config;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.sql.DataSource;

import org.apache.shardingsphere.driver.api.yaml.YamlShardingSphereDataSourceFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import com.ruoyi.common.enums.DataSourceType;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.config.properties.DruidProperties;
import com.ruoyi.framework.datasource.DynamicDataSource;

/**
 * druid 配置多数据源
 *
 * @author ruoyi
 */
@Configuration
public class DruidConfig
{
    @Bean
    @ConfigurationProperties("spring.dynamic.datasource.druid.master")
    public DataSource masterDataSource(DruidProperties druidProperties)
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean
    @ConfigurationProperties("spring.dynamic.datasource.druid.slave")
    @ConditionalOnProperty(prefix = "spring.dynamic.datasource.druid.slave", name = "enabled", havingValue = "true")
    public DataSource slaveDataSource(DruidProperties druidProperties)
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean(name = "shardingDataSource")
    public DataSource shardingDataSource() throws Exception
    {
        DataSource dataSource = YamlShardingSphereDataSourceFactory.createDataSource(ShardingConfig.getShardingYAMLFile());
        return dataSource;
    }

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource(DataSource masterDataSource)
    {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
        setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
        setDataSource(targetDataSources, DataSourceType.SHARDING.name(), "shardingDataSource");
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }

    /**
     * 设置数据源
     *
     * @param targetDataSources 备选数据源集合
     * @param sourceName 数据源名称
     * @param beanName bean名称
     */
    public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName)
    {
        try
        {
            DataSource dataSource = SpringUtils.getBean(beanName);
            targetDataSources.put(sourceName, dataSource);
        }
        catch (Exception e)
        {
        }
    }

    /**
     * 去除监控页面底部的广告
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    @ConditionalOnProperty(name = "spring.dynamic.datasource.druid.statViewServlet.enabled", havingValue = "true")
    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
    {
        // 获取web监控页面的参数
        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
        // 提取common.js的配置路径
        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
        final String filePath = "support/http/resources/js/common.js";
        // 创建filter进行过滤
        Filter filter = new Filter()
        {
            @Override
            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
            {
            }
            @Override
            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                    throws IOException, ServletException
            {
                chain.doFilter(request, response);
                // 重置缓冲区,响应头不会被重置
                response.resetBuffer();
                // 获取common.js
                String text = Utils.readFromResource(filePath);
                // 正则替换banner, 除去底部的广告信息
                text = text.replaceAll("<a.*?banner\"></a><br/>", "");
                text = text.replaceAll("powered.*?shrek.wang</a>", "");
                response.getWriter().write(text);
            }
            @Override
            public void destroy()
            {
            }
        };
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(filter);
        registrationBean.addUrlPatterns(commonJsPattern);
        return registrationBean;
    }
}

  1. 定义在sharding.yaml中指定的TimeShardingMonthlyAlgorithm算法
  2. 在framework下创建sharding包
  3. 创建分表表缓存枚举
package com.ruoyi.framework.sharding;


import java.util.*;

/**
 * @Description: 分片表缓存枚举
 * @Author WangHan
 * @Date 2024/9/19 15:16
 * @Version 1.0
 */
public enum ShardingTableCache {

    /**
     * 通知公告表
     */
    SYS_NOTICE("sys_notice", new HashSet<>()),

    /**
     * JOB表
     */
    SYS_JOB("sys_job", new HashSet<>());


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

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

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

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

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

    public String logicTableName() {
        return logicTableName;
    }

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

    /**
     * 更新缓存、配置(原子操作)
     *
     * @param tableNameList
     */
    public void atomicUpdateCacheAndActualDataNodes(List<String> tableNameList) {
        synchronized (resultTableNamesCache) {
            // 删除缓存
            resultTableNamesCache.clear();
            // 写入新的缓存
            resultTableNamesCache.addAll(tableNameList);
            // 动态更新配置 actualDataNodes
            ShardingAlgorithmTool.actualDataNodesRefresh(logicTableName, tableNameList);
        }
    }

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

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

}

  1. 配置启动时读取分表进行缓存
package com.ruoyi.framework.sharding;

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

/**
 * @Description: 项目启动后,读取已有分表,进行缓存
 * @Author WangHan
 * @Date 2024/9/19 18:46
 * @Version 1.0
 */
@Order(value = 1) // 数字越小,越先执行
@Component
public class ShardingTablesLoadRunner implements CommandLineRunner {

    private final static Logger log = LoggerFactory.getLogger(ShardingTablesLoadRunner.class);

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

        log.info(">>>>>>>>>> 【ShardingTablesLoadRunner】缓存已有分表成功 <<<<<<<<<<");
    }

}

  1. 定义分片算法,按年分片
package com.ruoyi.framework.sharding;

import com.google.common.collect.Range;
import org.apache.shardingsphere.sharding.api.sharding.ShardingAutoTableAlgorithm;
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @Description: 分片算法,按年分片
 * @Author WangHan
 * @Date 2024/9/19 15:11
 * @Version 1.0
 */
public class TimeShardingAlgorithm implements StandardShardingAlgorithm<Date>, ShardingAutoTableAlgorithm {

    private static final Logger log = LoggerFactory.getLogger(TimeShardingAlgorithm.class);

    /**
     * 分片时间格式
     */
    private static final DateTimeFormatter TABLE_SHARD_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy");

    /**
     * 完整时间格式
     */
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd HH:mm:ss");
    private static final DateTimeFormatter YYYY_MM_DD_HH_MM_SS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

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

    private Properties props;

    private int autoTablesAmount;


    /**
     * 精准分片(新增)
     * @param tableNames 对应分片库中所有分片表的集合
     * @param preciseShardingValue 分片键值,其中 logicTableName 为逻辑表,columnName 分片键,value 为从 SQL 中解析出来的分片键的值
     * @return 表名
     */
    @Override
    public String doSharding(Collection<String> tableNames, PreciseShardingValue<Date> preciseShardingValue) {
        String logicTableName = preciseShardingValue.getLogicTableName();
        // 打印分片信息
        log.info("精确分片,节点配置表名:{}", tableNames);
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = preciseShardingValue.getValue();

        String string = format.format(date);
        LocalDateTime dateTime = LocalDateTime.parse(string, YYYY_MM_DD_HH_MM_SS);
        String resultTableName = logicTableName + "_" + dateTime.format(TABLE_SHARD_TIME_FORMATTER);

        // 检查是否需要初始化
        if (tableNames.size() == 1) {
            // 如果只有一个表,说明需要获取所有表名
            List<String> allTableNameBySchema = ShardingAlgorithmTool.getAllTableNameBySchema(logicTableName);
            tableNames.clear();// 先清除,再加载所有的分片表
            tableNames.addAll(allTableNameBySchema);
            autoTablesAmount = allTableNameBySchema.size();
        }

        return getShardingTableAndCreate(logicTableName, resultTableName, tableNames);
    }

    /**
     * 范围分片(查询、更新、删除)
     * @param tableNames 对应分片库中所有分片表的集合
     * @param rangeShardingValue 分片范围
     * @return 表名集合
     */
    private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    @Override
    public Collection<String> doSharding(Collection<String> tableNames, RangeShardingValue<Date> rangeShardingValue) {
        String logicTableName = rangeShardingValue.getLogicTableName();
        // 打印分片信息
        log.info(">>>>>>>>>> 【INFO】范围分片,节点配置表名:{}", tableNames);
        // 检查是否需要初始化
        if (tableNames.size() == 1) {
            // 如果只有一个表,说明需要获取所有表名
            List<String> allTableNameBySchema = ShardingAlgorithmTool.getAllTableNameBySchema(logicTableName);
            tableNames.clear();// 先清除,再加载所有的分片表
            tableNames.addAll(allTableNameBySchema);
            autoTablesAmount = allTableNameBySchema.size();
        }
        // between and 的起始值
        Range<Date> valueRange = rangeShardingValue.getValueRange();
        boolean hasLowerBound = valueRange.hasLowerBound();
        boolean hasUpperBound = valueRange.hasUpperBound();
        // 获取日期范围的上下界
        Date lowerDate = null;
        Date upperDate = null;
        try {
            lowerDate = DATE_FORMATTER.parse(String.valueOf(rangeShardingValue.getValueRange().lowerEndpoint()));
            upperDate = DATE_FORMATTER.parse(String.valueOf(rangeShardingValue.getValueRange().upperEndpoint()));
        } catch (ParseException e) {
            try {
                lowerDate = DATE_FORMATTER.parse(String.valueOf(rangeShardingValue.getValueRange().lowerEndpoint()) + " 00:00:00");
                upperDate = DATE_FORMATTER.parse(String.valueOf(rangeShardingValue.getValueRange().upperEndpoint()) + " 23:59:59");
            } catch (ParseException ex) {
                throw new RuntimeException(ex);
            }
        }
        LocalDateTime lowerEndpoint = lowerDate.toInstant()
                .atZone(ZoneId.systemDefault())
                .toLocalDateTime();

        LocalDateTime upperEndpoint = upperDate.toInstant()
                .atZone(ZoneId.systemDefault())
                .toLocalDateTime();

        // 获取最大值和最小值
        LocalDateTime min = hasLowerBound ? lowerEndpoint :getLowerEndpoint(tableNames);
        LocalDateTime max = hasUpperBound ? upperEndpoint :getUpperEndpoint(tableNames);

        // 循环计算分表范围
        Set<String> resultTableNames = new LinkedHashSet<>();
        resultTableNames.add(logicTableName);
        while (min.isBefore(max) || min.equals(max)) {
            String tableName = logicTableName + TABLE_SPLIT_SYMBOL + min.format(TABLE_SHARD_TIME_FORMATTER);
            resultTableNames.add(tableName);
            min = min.plusMinutes(1);
        }
        // 查询与修改不需要执行创建表
        return resultTableNames;
    }

    @Override
    public void init(Properties props) {
        this.props = props;
    }

    @Override
    public int getAutoTablesAmount() {
        return autoTablesAmount;
    }

    @Override
    public String getType() {
        return "CLASS_BASED";
    }

    public Properties getProps() {
        return props;
    }

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

    /**
     * 检查分表获取的表名是否存在,不存在则自动建表
     * @param logicTableName   逻辑表
     * @param resultTableName 真实表名,例:mall_order_2023
     * @return 确认存在于数据库中的真实表名
     */
    private String getShardingTableAndCreate(String logicTableName, String resultTableName, Collection<String> availableTargetNames) {
        // 缓存中有此表则返回,没有则判断创建
        if (availableTargetNames.contains(resultTableName)) {
            return resultTableName;
        } else {
            // 检查分表获取的表名不存在,需要自动建表
            boolean isSuccess = ShardingAlgorithmTool.createShardingTable(logicTableName, resultTableName);
            if (isSuccess) {
                // 如果建表成功,需要更新缓存
                availableTargetNames.add(resultTableName);
                autoTablesAmount++;
                return resultTableName;
            } else {
                // 如果建表失败,返回逻辑空表
                return logicTableName;
            }
        }
    }

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

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

}

  1. 定义分片算法工具类
package com.ruoyi.framework.sharding;

import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.config.ShardingConfig;
import org.apache.shardingsphere.driver.api.yaml.YamlShardingSphereDataSourceFactory;
import org.apache.shardingsphere.driver.jdbc.core.datasource.ShardingSphereDataSource;
import org.apache.shardingsphere.infra.config.rule.RuleConfiguration;
import org.apache.shardingsphere.mode.manager.ContextManager;
import org.apache.shardingsphere.sharding.api.config.ShardingRuleConfiguration;
import org.apache.shardingsphere.sharding.api.config.rule.ShardingTableRuleConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.sql.*;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @Description: 分片算法工具类
 * @Author WangHan
 * @Date 2024/9/19 15:15
 * @Version 1.0
 */
@Component
public class ShardingAlgorithmTool implements InitializingBean {

    private static final Logger log = LoggerFactory.getLogger(ShardingAlgorithmTool.class);

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

    @Value("${spring.dynamic.datasource.druid.master.url}")
    private String MASTER_URL;

    @Value("${spring.dynamic.datasource.druid.master.username}")
    private String MASTER_USERNAME;

    @Value("${spring.dynamic.datasource.druid.master.password}")
    private String MASTER_PASSWORD;

    private static String DATASOURCE_URL;
    private static String DATASOURCE_USERNAME;
    private static String DATASOURCE_PASSWORD;
    @Override
    public void afterPropertiesSet() throws Exception {
        DATASOURCE_URL = MASTER_URL;
        DATASOURCE_USERNAME = MASTER_USERNAME;
        DATASOURCE_PASSWORD = MASTER_PASSWORD;
    }


    /**
     * 获取ContextManager
     * @param dataSource
     * @return
     */
    public static ContextManager getContextManager(final ShardingSphereDataSource dataSource) {
        try {
            Field field = ShardingSphereDataSource.class.getDeclaredField("contextManager");
            field.setAccessible(true);
            return (ContextManager) field.get(dataSource);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 重载全部缓存
     */
    public static void tableNameCacheReloadAll() {
        Arrays.stream(ShardingTableCache.values()).forEach(ShardingAlgorithmTool::tableNameCacheReload);
    }

    /**
     * 重载指定分表缓存
     * @param logicTable 逻辑表,例:mall_order
     */
    public static void tableNameCacheReload(ShardingTableCache logicTable) {
        // 读取数据库中所有表名
        List<String> tableNameList = getAllTableNameBySchema(logicTable.logicTableName());
        // 更新缓存、配置(原子操作)
        logicTable.atomicUpdateCacheAndActualDataNodes(tableNameList);
        // 删除旧的缓存(如果存在)
        logicTable.resultTableNamesCache().clear();
        // 写入新的缓存
        logicTable.resultTableNamesCache().addAll(tableNameList);
        // 动态更新配置 actualDataNodes
        actualDataNodesRefresh(logicTable.logicTableName(), tableNameList);
    }

    /**
     * 获取所有表名
     * @return 表名集合
     * @param logicTableName 逻辑表
     */
    public static List<String> getAllTableNameBySchema(String logicTableName) {
        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()) {
            try (ResultSet rs = st.executeQuery("show TABLES like '" + logicTableName + TABLE_SPLIT_SYMBOL + "%'")) {
                tableNames.add(logicTableName);
                while (rs.next()) {
                    String tableName = rs.getString(1);
                    // 匹配分表格式 例:^(mall\_contract_\d{6})$
                    if (tableName != null && tableName.matches(String.format("^(%s\\d{4})$", logicTableName + TABLE_SPLIT_SYMBOL))) {
                        tableNames.add(rs.getString(1));
                    }
                }
            }
        } catch (SQLException e) {
            log.error(">>>>>>>>>> 【ERROR】数据库连接失败,请稍后重试,原因:{}", e.getMessage(), e);
            throw new IllegalArgumentException("数据库连接失败,请稍后重试");
        }
        return tableNames;
    }

    /**
     * 动态更新配置 actualDataNodes
     *
     * @param logicTableName  逻辑表名
     * @param tableNamesCache 真实表名集合
     */
    public static void actualDataNodesRefresh(String logicTableName, List<String> tableNamesCache)  {
        try {
            // 获取数据分片节点
            String dbName = "sharding";
            log.info(">>>>>>>>>> 【INFO】更新分表配置,logicTableName:{},tableNamesCache:{}", logicTableName, tableNamesCache);

            // generate actualDataNodes
            String newActualDataNodes = tableNamesCache.stream().map(o -> String.format("%s.%s", dbName, o)).collect(Collectors.joining(","));
            ShardingSphereDataSource shardingSphereDataSource = (ShardingSphereDataSource) YamlShardingSphereDataSourceFactory.createDataSource(ShardingConfig.getShardingYAMLFile());
            updateShardRuleActualDataNodes(shardingSphereDataSource, logicTableName, newActualDataNodes);
        }catch (Exception e){
            log.error("初始化 动态表单失败,原因:{}", e.getMessage(), e);
        }
    }

    /**
     * 刷新ActualDataNodes
     */
    private static void updateShardRuleActualDataNodes(ShardingSphereDataSource dataSource, String logicTableName, String newActualDataNodes) {
        // Context manager.
        ContextManager contextManager = getContextManager(dataSource);

        // Rule configuration.
        String schemaName = "logic_db";
        Collection<RuleConfiguration> newRuleConfigList = new LinkedList<>();
        Collection<RuleConfiguration> oldRuleConfigList = contextManager.getMetaDataContexts()
                .getMetaData()
                .getGlobalRuleMetaData()
                .getConfigurations();

        for (RuleConfiguration oldRuleConfig : oldRuleConfigList) {
            if (oldRuleConfig instanceof ShardingRuleConfiguration) {

                // Algorithm provided sharding rule configuration
                ShardingRuleConfiguration oldAlgorithmConfig = (ShardingRuleConfiguration) oldRuleConfig;
                ShardingRuleConfiguration newAlgorithmConfig = new ShardingRuleConfiguration();

                // Sharding table rule configuration Collection
                Collection<ShardingTableRuleConfiguration> newTableRuleConfigList = new LinkedList<>();
                Collection<ShardingTableRuleConfiguration> oldTableRuleConfigList = oldAlgorithmConfig.getTables();

                oldTableRuleConfigList.forEach(oldTableRuleConfig -> {
                    if (logicTableName.equals(oldTableRuleConfig.getLogicTable())) {
                        ShardingTableRuleConfiguration newTableRuleConfig = new ShardingTableRuleConfiguration(oldTableRuleConfig.getLogicTable(), newActualDataNodes);
                        newTableRuleConfig.setTableShardingStrategy(oldTableRuleConfig.getTableShardingStrategy());
                        newTableRuleConfig.setDatabaseShardingStrategy(oldTableRuleConfig.getDatabaseShardingStrategy());
                        newTableRuleConfig.setKeyGenerateStrategy(oldTableRuleConfig.getKeyGenerateStrategy());

                        newTableRuleConfigList.add(newTableRuleConfig);
                    } else {
                        newTableRuleConfigList.add(oldTableRuleConfig);
                    }
                });

                newAlgorithmConfig.setTables(newTableRuleConfigList);
                newAlgorithmConfig.setAutoTables(oldAlgorithmConfig.getAutoTables());
                newAlgorithmConfig.setBindingTableGroups(oldAlgorithmConfig.getBindingTableGroups());
                newAlgorithmConfig.setDefaultDatabaseShardingStrategy(oldAlgorithmConfig.getDefaultDatabaseShardingStrategy());
                newAlgorithmConfig.setDefaultTableShardingStrategy(oldAlgorithmConfig.getDefaultTableShardingStrategy());
                newAlgorithmConfig.setDefaultKeyGenerateStrategy(oldAlgorithmConfig.getDefaultKeyGenerateStrategy());
                newAlgorithmConfig.setDefaultShardingColumn(oldAlgorithmConfig.getDefaultShardingColumn());
                newAlgorithmConfig.setShardingAlgorithms(oldAlgorithmConfig.getShardingAlgorithms());
                newAlgorithmConfig.setKeyGenerators(oldAlgorithmConfig.getKeyGenerators());

                newRuleConfigList.add(newAlgorithmConfig);
            }
        }

        // update context
        contextManager.alterRuleConfiguration(schemaName, newRuleConfigList);
    }

    /**
     * 创建分表
     * @param logicTableName 逻辑表
     * @param resultTableName 真实表名,例:mall_order_2024
     * @return 创建结果(true创建成功,false未创建)
     */
    public static boolean createShardingTable(String logicTableName, String resultTableName) {
        // 根据日期判断,当前年份之后分表不提前创建
        String year = resultTableName.replace(logicTableName + TABLE_SPLIT_SYMBOL,"");
        YearMonth shardingMonth = YearMonth.parse(year + "01", DateTimeFormatter.ofPattern("yyyyMM"));
        if (shardingMonth.isAfter(YearMonth.now())) {
            return false;
        }
        synchronized (logicTableName.intern()) {
            // 缓存中无此表,则建表并添加缓存
            executeSql(Collections.singletonList("CREATE TABLE IF NOT EXISTS `" + resultTableName + "` LIKE `" + logicTableName + "`;"));
        }
        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("数据库连接失败,请稍后重试");
        }
    }
}

在这里插入图片描述

使用

在需要使用的serviceImpl和mapper接口上声明,以启用分表功能。未声明的接口将默认使用master数据源。
在这里插入图片描述

放行通知公告接口
在这里插入图片描述

在这里插入图片描述
示例Demo:
https://gitee.com/wwh_work/sharding

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/938688.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Postman接口测试:全局变量/接口关联/加密/解密

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 全局变量和环境变量 全局变量&#xff1a;在postman全局生效的变量&#xff0c;全局唯一 环境变量&#xff1a;在特定环境下生效的变量&#xff0c;本环境内唯一 …

基于PHP的民宿预订管理系统

有需要请加文章底部Q哦 可远程调试 基于PHP的民宿预订管理系统 一 介绍 此民宿预订管理系统基于原生PHP开发&#xff0c;数据库mysql&#xff0c;前端bootstrap。系统角色分为用户和管理员。(附带配套设计文档) 技术栈 phpmysqlbootstrapphpstudyvscode 二 功能 用户 1 注册…

Elasticsearch:使用 Open Crawler 和 semantic text 进行语义搜索

作者&#xff1a;来自 Elastic Jeff Vestal 了解如何使用开放爬虫与 semantic text 字段结合来轻松抓取网站并使其可进行语义搜索。 Elastic Open Crawler 演练 我们在这里要做什么&#xff1f; Elastic Open Crawler 是 Elastic 托管爬虫的后继者。 Semantic text 是 Elasti…

NVM:安装配置使用(详细教程)

文章目录 一、简介二、安装 nvm三、配置 nvm 镜像四、配置环境变量五、使用教程5.1 常用命令5.2 具体案例 六、结语 一、简介 在实际的开发和学习中可能会遇到不同项目的 node 版本不同&#xff0c;而出现的兼容性问题。 而 nvm 就可以很好的解决这个问题&#xff0c;它可以在…

【HarmonyOS】HarmonyOS 和 Flutter混合开发 (一)之鸿蒙Flutter环境安装

【HarmonyOS】HarmonyOS 和 Flutter混合开发 &#xff08;一&#xff09;之鸿蒙Flutter环境安装 一、前言 flutter作为开源适配框架方案&#xff0c;已经在Android&#xff0c;IOS&#xff0c;Web&#xff0c;Window四大平台进行了适配&#xff0c;一套代码&#xff0c;可以同…

期权懂|期权新手入门知识:个股期权标的资产的作用

锦鲤三三每日分享期权知识&#xff0c;帮助期权新手及时有效地掌握即市趋势与新资讯&#xff01; 期权新手入门知识&#xff1a;个股期权标的资产的作用 个股期权标的资产的作用主要体现在以下几个方面‌&#xff1a; &#xff08;1&#xff09;基本面影响‌&#xff1a; 标的资…

Unity超优质动态天气插件(含一年四季各种天气变化,可用于单机局域网VR)

效果展示&#xff1a;https://www.bilibili.com/video/BV1CkkcYHENf/?spm_id_from333.1387.homepage.video_card.click 在你的项目中设置enviro真的很容易&#xff01;导入包裹并按照以下步骤操作开始的步骤&#xff01; 1. 拖拽“EnviroSky”预制件&#xff08;“environme…

【算法】【优选算法】链表

目录 一、链表常用技巧与操作总结二、2.两数相加三、24.两两交换链表中的节点3.1 迭代3.2 递归 四、143.重排链表五、23.合并K个升序链表5.1 堆5.2 分治5.3 暴力枚举 六、25.K个⼀组翻转链表 一、链表常用技巧与操作总结 技巧&#xff1a; 画图解题。使用虚拟头结点。像有插入…

【面试】Redis 常见面试题

一、介绍一下什么是 Redis&#xff0c;有什么特点? Redis 是一个高性能的 key-value 内存数据库。 不同于传统的 MySQL 这样的关系型数据库&#xff0c;Redis 主要使用内存存储数据&#xff08;当然也支持持久化存储到硬盘上&#xff09;&#xff0c;并非是使用 “表” 这样…

【Linux】NET9运行时移植到低版本GLIBC的Linux纯内核板卡上

背景介绍 自制了一块Linux板卡(基于全志T113i) 厂家给的SDK和根文件系统能够提供的GLIBC的版本比较低 V2.25/GCC 7.3.1 这个版本是无法运行dotnet以及dotnet生成的AOT应用的 我用另一块同Cortex-A7的板子运行dotnet的报错 版本不够&#xff0c;运行不了 而我的板子是根本就识…

MySQL Explain 分析SQL语句性能

一、EXPLAIN简介 使用EXPLAIN关键字可以模拟优化器执行SQL查询语句&#xff0c;从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是表结构的性能瓶颈。 &#xff08;1&#xff09; 通过EXPLAIN&#xff0c;我们可以分析出以下结果&#xff1a; 表的读取顺序数据读取…

vue3实现商城系统详情页(前端实现)

目录 写在前面 预览 实现 图片部分 详情部分 代码 源码地址 总结 写在前面 笔者不是上一个月毕业了么&#xff1f;找工作没找到&#xff0c;准备在家躺平两个月。正好整理一下当时的毕业设计&#xff0c;是一个商城系统。还是写篇文章记录下吧 预览 商品图片切换显示…

uniapp 微信小程序 功能入口

单行单独展示 效果图 html <view class"shopchoose flex jsb ac" click"routerTo(要跳转的页面)"><view class"flex ac"><image src"/static/dyd.png" mode"aspectFit" class"shopchooseimg"&g…

6.1 初探MapReduce

MapReduce是一种分布式计算框架&#xff0c;用于处理大规模数据集。其核心思想是“分而治之”&#xff0c;通过Map阶段将任务分解为多个简单任务并行处理&#xff0c;然后在Reduce阶段汇总结果。MapReduce编程模型包括Map和Reduce两个阶段&#xff0c;数据来源和结果存储通常在…

聚观早报 | 百度回应进军短剧;iPad Air将升级OLED

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 12月18日消息 百度回应进军短剧 iPad Air将升级OLED 三星Galax S25 Ultra配色细节 一加Ace 5系列存储规格 小米…

CH582F BLE5.3 蓝牙核心板开发板 60MHz RAM:32KB ROM:448KB

CH582F BLE5.3 蓝牙核心板开发板 60MHz RAM:32KB ROM:448KB 是一款基于南京沁恒&#xff08;WCH&#xff09;推出的高性能、低功耗无线通信芯片CH582F的开发板。以下是该开发板的功能和参数详细介绍&#xff1a; 主要特性 双模蓝牙支持&#xff1a; 支持蓝牙5.0标准&#xff0…

【软件工程复习】

第1章 软件工程概述 1.2软件工程 ​ 1983年IEEE给出的定义&#xff1a;“软件工程是 开发、运行、维护和修复软件的系统方法 ” 1.4软件生存期 软件开发和运行维护由三个时期组成&#xff1a; 软件定义时期软件开发时期运行维护时期 里程碑指可以用来标识项目进程状态的事…

DuckDB: 从MySql导出数据至Parquet文件

在这篇文章中&#xff0c;介绍使用DuckDB将数据从MySQL数据库无缝传输到Parquet文件的过程。该方法比传统的基于pandas方法更高效、方便&#xff0c;我们可以从DuckDB cli实现&#xff0c;也可以结合Python编程方式实现&#xff0c;两者执行核心SQL及过程都一样。 Parquet格式…

safe area helper插件

概述 显示不同机型的必能显示的区域 实现步骤 引入safearea&#xff0c;引入其中的safearea的csharp 为cancas加入gameobject gameobject中加入safearea脚本 将UI作为这个gameobject的子物体&#xff0c;就可以完成显示

数据结构 ——二叉树转广义表

数据结构 ——二叉树转广义表 1、树转广义表 如下一棵树&#xff0c;转换为广义表 root(c(a()(b()()))(e(d()())(f()(j(h()())())))) (根&#xff08;左子树&#xff09;&#xff08;右子树&#xff09;) 代码实现 #include<stdio.h> #include<stdlib.h>//保存…