springboot多数据源集成数据分片代码示例:ShardingSphere JDBC 5.1.1运行期动态切换数据源及自定义分片算法

此文将整合Druid多数据源连接池,根据业务动态切换数据源,并把主库中的某一张表进行分表。

引入依赖(部分依赖,根据实际项目)

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.11</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.8</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <exclusions>
            <exclusion>
                <groupId>com.github.jsqlparser</groupId>
                <artifactId>jsqlparser</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.2.1</version>
        <exclusions>
            <exclusion>
                <groupId>com.github.jsqlparser</groupId>
                <artifactId>jsqlparser</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>com.github.jsqlparser</groupId>
        <artifactId>jsqlparser</artifactId>
        <version>4.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
        <version>5.1.1</version>
    </dependency>

动态切换数据源

启动类排除druid的自动配置 否则无法启动项目!

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})

配置文件(druid配置)

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.frist.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.druid.first.url=${spring.datasource.url}
spring.datasource.druid.first.username=${spring.datasource.username}
spring.datasource.druid.first.password=${spring.datasource.password}
 
spring.datasource.druid.second.driverClassName=oracle.jdbc.driver.OracleDriver
spring.datasource.druid.second.url=${spring.datasource.url2}
spring.datasource.druid.second.username=${spring.datasource.username2}
spring.datasource.druid.second.password=${spring.datasource.password2}
 
 
spring.datasource.druid.third.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.druid.third.url=${spring.datasource.url3}
spring.datasource.druid.third.username=${spring.datasource.username3}
spring.datasource.druid.third.password=${spring.datasource.password3}
 
spring.datasource.druid.fourth.driverClassName=oracle.jdbc.driver.OracleDriver
spring.datasource.druid.fourth.url=${spring.datasource.url4}
spring.datasource.druid.fourth.username=${spring.datasource.username4}
spring.datasource.druid.fourth.password=${spring.datasource.password4}
 
spring.datasource.druid.fifth.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.druid.fifth.url=${spring.datasource.url5}
spring.datasource.druid.fifth.username=${spring.datasource.username5}
spring.datasource.druid.fifth.password=${spring.datasource.password5}
 
spring.datasource.druid.sixth.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.datasource.druid.sixth.url=${spring.datasource.url6}
spring.datasource.druid.sixth.username=${spring.datasource.username6}
spring.datasource.druid.sixth.password=${spring.datasource.password6}
 
spring.datasource.druid.seventh.driverClassName=oracle.jdbc.driver.OracleDriver
spring.datasource.druid.seventh.url=${spring.datasource.url7}
spring.datasource.druid.seventh.username=${spring.datasource.username7}
spring.datasource.druid.seventh.password=${spring.datasource.password7}

定义数据源的名称

/**
 * 定义数据源的名称
 */
public interface DataSourceNames {
    String FIRST = "first";
    String SECOND = "second";
    String THIRD = "third";
    String FOUR = "fourth";
    String FIFTH = "fifth";
    String SIXTH = "sixth";
    String SEVENTH = "seventh";
    String EIGHT = "eight";
    String NINTH = "ninth";
}

注入配置类

import com.alibaba.druid.pool.DruidDataSource;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
 
/**
 * @Description 数据源配置类
 **/
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.druid")
public class DataSourceProperties {
    private DruidDataSource first;
    private DruidDataSource second;
    private DruidDataSource third;
    private DruidDataSource four;
    private DruidDataSource fifth;
    private DruidDataSource sixth;
    private DruidDataSource seventh;
    private DruidDataSource eight;
    private DruidDataSource ninth;
}

这段代码使用了Spring框架的一些注解来配置和管理数据源属性。下面是对这段代码中主要部分的逐行解释:

  1. @Data:这是Lombok库提供的注解,它是一个组合注解,相当于添加了@Getter, @Setter, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode这几个注解。它的作用是为类中的所有字段自动生成getter/setter方法、equals()、hashCode()、toString()方法,以及一个无参构造器(如果类中有final字段或者未初始化的非null字段,则会生成一个包含这些字段的构造器)。

  2. @Configuration:Spring框架的注解,表明这个类是一个配置类,相当于XML配置文件中的 <beans> 标签。Spring会扫描带有此注解的类,并读取其中的bean定义信息,用于创建和管理bean实例。

  3. @ConfigurationProperties(prefix = "spring.datasource.druid"):此注解也是Spring框架的一部分,用于绑定配置文件中的属性到Java Bean中。这里指定的前缀spring.datasource.druid意味着它会从应用的配置文件(如application.properties或application.yml)中查找所有以该前缀开头的属性,并尝试将它们映射到这个类的字段上。例如,如果配置文件中有spring.datasource.druid.first.url=jdbc:mysql://localhost:3306/db1,那么这个值会被自动设置到first字段的DruidDataSource实例的url属性上。

  4. public class DataSourceProperties { ... }:定义了一个名为DataSourceProperties的公共类。

5-13行. 这段代码定义了9个DruidDataSource类型的字段,分别命名为firstninthDruidDataSource是Druid连接池库中的一个类,用于配置和管理数据库连接。通过@ConfigurationProperties注解,这9个数据源可以独立配置,适用于需要管理多个不同数据库连接的场景。

定义动态切换的数据源

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
 
import javax.sql.DataSource;
import java.util.Map;
 
/**
 * @Description 定义动态的数据源
 * @Param
 * @return
 **/
public class DynamicDataSource extends AbstractRoutingDataSource {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
 
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
 
    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }
 
    public static void setDataSource(String dataSource) {
        CONTEXT_HOLDER.set(dataSource);
    }
 
    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }
 
    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
 
}

这段代码定义了一个名为DynamicDataSource的类,继承自Spring框架的AbstractRoutingDataSource类,目的是实现数据源的动态切换功能。下面是代码的详细解析:

  1. 成员变量

    • private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); 定义了一个线程局部变量,用于在每个线程中保存当前应该使用的数据源的key。ThreadLocal保证了每个线程都有自己的独立副本,从而避免了线程安全问题。
  2. 构造函数

    • public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {...} 构造函数接收两个参数:一个是默认的目标数据源(defaultTargetDataSource),另一个是目标数据源集合(targetDataSources)。在这个构造函数中,它通过调用父类的方法设置了默认数据源和可选数据源,并调用了afterPropertiesSet完成必要的初始化。
  3. 重写determineCurrentLookupKey方法

    • @Override protected Object determineCurrentLookupKey() {...} 此方法是从AbstractRoutingDataSource继承而来并被重写,用于确定当前要使用哪个数据源。在此实现中,它直接返回了通过getDataSource()方法获取的数据源key。
  4. 静态方法

    • public static void setDataSource(String dataSource) 用于设置当前线程的数据源key。
    • public static String getDataSource() 获取当前线程的数据源key。
    • public static void clearDataSource() 清除当前线程的数据源key,通常在操作完成后调用以避免数据源泄漏。

工作原理简述
当应用程序需要执行SQL查询时,Spring会通过DynamicDataSource来决定使用哪个实际的数据源。DynamicDataSource利用ThreadLocal保证了在同一个线程内始终使用相同的数据源,通过调用setDataSource方法动态改变当前线程的数据源key,然后在每次数据库访问时,determineCurrentLookupKey方法会被调用以获取这个key,进而定位到具体的数据源。这种方式使得在运行时动态切换数据源成为可能,对于需要处理多租户、读写分离等场景非常有用。

数据源(sharding也在这里配置)

import bts.core.models.constant.ShardingConstants;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.driver.api.ShardingSphereDataSourceFactory;
import org.apache.shardingsphere.infra.config.algorithm.ShardingSphereAlgorithmConfiguration;
import org.apache.shardingsphere.sharding.api.config.ShardingRuleConfiguration;
import org.apache.shardingsphere.sharding.api.config.rule.ShardingTableRuleConfiguration;
import org.apache.shardingsphere.sharding.api.config.strategy.sharding.ComplexShardingStrategyConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
 
import javax.sql.DataSource;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
 
/**
 * sharding 数据源
 */
@Slf4j
@Configuration
@Order(3)
public class ShardingDataSourceConfig {
 
    @Autowired
    private DataSourceProperties properties;
 
    @Bean
    @Primary
    public DynamicDataSource dataSource() {
        log.info("druid多数据源初始化......");
        Map<Object, Object> targetDataSources = new HashMap<>(16);
        targetDataSources.put(DataSourceNames.FIRST, buildDataSource());
        targetDataSources.put(DataSourceNames.SECOND, properties.getSecond());
        targetDataSources.put(DataSourceNames.THIRD, properties.getThird());
        targetDataSources.put(DataSourceNames.FOUR, properties.getFour());
        targetDataSources.put(DataSourceNames.FIFTH, properties.getFifth());
        targetDataSources.put(DataSourceNames.SIXTH, properties.getSixth());
        targetDataSources.put(DataSourceNames.SEVENTH, properties.getSeventh());
        targetDataSources.put(DataSourceNames.EIGHT, properties.getEight());
        targetDataSources.put(DataSourceNames.NINTH, properties.getNinth());
        return new DynamicDataSource((DataSource) targetDataSources.get(DataSourceNames.FIRST), targetDataSources);
    }
 
    @Value("${spring.datasource.druid.first.url}")
    private String url;
    @Value("${spring.datasource.druid.first.username}")
    private String username;
    @Value("${spring.datasource.druid.first.password}")
    private String password;
    @Value("${spring.datasource.druid.frist.driverClassName}")
    private String driverClassName;
    public static Map<String, DataSource> dataSourceMap = new HashMap<>();
    private static String CELL_RECORD = "cell_record";
 
 
    @SneakyThrows
    private DataSource buildDataSource() {
        log.info("sharding jdbc数据源初始化");
        dataSourceMap.put(DataSourceNames.FIRST, properties.getFirst());
        // 配置 表规则 查询cell_record和cell_record_1表
        ShardingTableRuleConfiguration tableRuleConfig = new ShardingTableRuleConfiguration(CELL_RECORD, DataSourceNames.FIRST + "." + CELL_RECORD + "," + DataSourceNames.FIRST + "." + CELL_RECORD + "_1");
        // 配置分表策略
        // 设置分片为标准分片且指定分片列名称和分片算法名称
        String shardingColumn = "create_time";
        //复合分片策略配置
        tableRuleConfig.setTableShardingStrategy(new ComplexShardingStrategyConfiguration(shardingColumn, ShardingConstants.SHARDING_ALGORITHM_KEY));
        //标准分片策略配置
        //tableRuleConfig.setTableShardingStrategy(new StandardShardingStrategyConfiguration(shardingColumn, ShardingConstants.SHARDING_ALGORITHM_KEY));
        // 配置分片规则
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.getTables().add(tableRuleConfig);
        // 配置分表算法
        //复合分片算法
        shardingRuleConfig.getShardingAlgorithms().put(ShardingConstants.SHARDING_ALGORITHM_KEY, new ShardingSphereAlgorithmConfiguration(ShardingConstants.MONTH_COMPLEX, new Properties()));
        //标准分片算法
        //shardingRuleConfig.getShardingAlgorithms().put(ShardingConstants.SHARDING_ALGORITHM_KEY, new ShardingSphereAlgorithmConfiguration(ShardingConstants.MONTH_STANDARD, new Properties()));
        Properties properties = new Properties();
        //日志中打印简单风格的 SQL
        //properties.setProperty("sql-simple", "true");
        //日志内容包含:逻辑 SQL,真实 SQL 和 SQL 解析结果。
        properties.setProperty("sql-show", "true");
        return ShardingSphereDataSourceFactory.createDataSource(dataSourceMap, Collections.singleton(shardingRuleConfig), properties);
    }
}

这段代码是一个Spring配置类,用于配置Sharding-JDBC作为数据源,实现数据库分片(Sharding)。下面是代码的详细解析:

  1. 注解说明:

    • @Slf4j: Lombok注解,自动为类添加一个org.slf4j.Logger类型的成员变量log,用于日志输出。
    • @Configuration: 标记该类为Spring的配置类,用于声明Bean。
    • @Order(3): 指定配置类的加载顺序,数字越小,优先级越高,此处设置为3表示该配置在其他标记了@Order注解的配置之后加载,或者在没有指定顺序的配置之前加载。
  2. 成员变量与依赖注入:

    • @Autowired private DataSourceProperties properties;:自动注入了前面定义的DataSourceProperties类的实例,用于获取配置的多个数据源属性。
    • @Value注解:用于注入配置文件中的属性值,如数据库URL、用户名、密码等。
  3. 方法说明:

    • @Bean @Primary public DynamicDataSource dataSource() {...}:声明了一个Bean,它是数据源的实现,标记为@Primary意味着它是默认首选的数据源。此方法初始化了一个DynamicDataSource,结合了多个数据源(通过buildDataSource()方法构建的第一个数据源和其他从DataSourceProperties中获取的数据源),并使用了Map来存储这些数据源,键为数据源的名称。
  4. buildDataSource()方法:

    • 此方法内部创建了一个Sharding-JDBC的数据源,它基于分片规则配置来实现数据分片。
    • 首先,将第一个数据源放入dataSourceMap,这通常用于未分片的表或作为默认数据源。
    • 接着,配置了一个分表规则,以CELL_RECORD表为例,指定了实际物理表的名称,并设置了复合分片策略,使用create_time作为分片列,配合名为ShardingConstants.SHARDING_ALGORITHM_KEY的分片算法。
    • 配置了分片规则配置(ShardingRuleConfiguration),并添加了上面定义的表规则。
    • 最后,通过ShardingSphereDataSourceFactory.createDataSource()方法创建了一个ShardingSphere数据源实例,传入数据源映射、分片规则配置和一些额外属性(如开启SQL日志显示)。

总结来说,这段代码配置了一个支持分片的动态数据源,通过Sharding-JDBC实现了对数据库表的分片逻辑,可以根据配置的策略将数据分散到不同的物理表中,同时利用Spring的配置管理,方便地集成到应用中。

动态数据源切面

import bts.core.annotation.DataSource;
import bts.platform.config.datasources.DataSourceNames;
import bts.platform.config.datasources.DynamicDataSource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
 
/**
 * @Author zhanyangyang
 * @Description 动态数据源切面
 * @Date 17:55 2019-05-24
 * @Param
 * @return
 **/
@Aspect
@Component
public class DataSourceAspect implements Ordered {
    protected Logger logger = LoggerFactory.getLogger(getClass());
 
    @Pointcut("@annotation(bts.core.annotation.DataSource)")
    public void dataSourcePointCut() {
 
    }
 
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
 
        DataSource ds = method.getAnnotation(DataSource.class);
        if (ds == null) {
            DynamicDataSource.setDataSource(DataSourceNames.FIRST);
            logger.debug("set datasource is " + DataSourceNames.FIRST);
        } else {
            DynamicDataSource.setDataSource(ds.name());
            logger.debug("set datasource is " + ds.name());
        }
 
        try {
            return point.proceed();
        } finally {
            DynamicDataSource.clearDataSource();
            logger.debug("clean datasource");
        }
    }
 
    @Override
    public int getOrder() {
        return 1;
    }
}

这段代码是一个Spring AOP(面向切面编程)的实现,用于动态切换数据源。以下是代码的详细解释:

  1. 注解说明:

    • @Aspect: 标记该类为一个切面类,用于定义切面逻辑,如前置通知、后置通知、环绕通知等。
    • @Component: 表明该类是一个Spring组件,将会被Spring容器自动扫描并管理。
    • implements Ordered: 实现Ordered接口,允许定义该切面的执行顺序,其中getOrder()方法返回的值越小,执行优先级越高。
  2. 成员变量:

    • Logger logger = LoggerFactory.getLogger(getClass());:使用SLF4J的日志框架,创建一个日志记录器实例,用于记录日志信息。
  3. 切点定义 (@Pointcut):

    • @Pointcut("@annotation(bts.core.annotation.DataSource)"): 定义了一个切点表达式,匹配所有标注了bts.core.annotation.DataSource注解的方法。这意味着任何带有此注解的方法调用都会触发切面逻辑。
  4. 环绕通知 (@Around):

    • @Around("dataSourcePointCut()"): 声明一个环绕通知,应用在上面定义的切点上。
    • 方法体中:
      • 首先,通过ProceedingJoinPoint获取正在执行的方法签名和方法对象。
      • 然后,尝试从方法上获取DataSource注解,如果存在,则使用该注解指定的数据源名称;如果不存在,默认使用DataSourceNames.FIRST
      • 调用DynamicDataSource.setDataSource(...)来设置当前线程的数据源。
      • 使用point.proceed()执行被代理方法的实际逻辑。
      • finally块中,调用DynamicDataSource.clearDataSource()清除当前线程的数据源设置,并记录日志。
  5. getOrder()方法:

    • 返回值1表示此切面的执行优先级。如果有多个切面,优先级高的先执行。

综上,这段代码实现了一个AOP切面,其核心功能是在方法执行前后动态切换数据源。当被拦截的方法上标注了自定义的DataSource注解时,就使用该注解指定的数据源;若未标注,则使用默认数据源。通过这样的方式,可以在运行时根据业务需求灵活地切换数据源,非常适合多数据源环境下的数据库操作。

数据源注解

import java.lang.annotation.*;
 
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String name() default "";
}

这段代码定义了一个自定义注解DataSource,用于标记在方法级别上,指示该方法应该使用哪个数据源。下面是该注解定义的详细解析:

  1. @Target(ElementType.METHOD):

    • @Target是一个元注解,用于指定被修饰的注解可以应用在哪些程序元素上。这里设置为ElementType.METHOD,意味着DataSource注解只能应用于方法上。
  2. @Retention(RetentionPolicy.RUNTIME):

    • @Retention也是一个元注解,用于设定注解的生命周期。RetentionPolicy.RUNTIME表示这个注解不仅在编译期保留,还会在运行时保留,因此可以通过反射机制读取到这个注解的信息。这对于在运行时需要根据注解信息做出行为决策(比如本例中动态切换数据源)的场景非常重要。
  3. @Documented:

    • @Documented注解表明这个注解应该被JavaDoc工具文档化。也就是说,当生成项目的API文档时,这个注解及其说明也会被包含进去,有助于提高代码的可读性和文档的完整性。
  4. public @interface DataSource { ... }:

    • 这里定义了DataSource注解接口,它本身就是一个注解类型。里面定义了一个方法name(),这是一个注解元素,用于在使用注解时提供具体的值。
  5. String name() default "";:

    • name是注解的一个属性,类型为String,表示数据源的名称。default ""指定了该属性的默认值为空字符串。这意味着在使用@DataSource注解时,如果不显式指定name值,则默认为空字符串。例如,开发者可以这样使用此注解:
      @DataSource(name = "secondary")
      public List<User> getUsersFromSecondaryDataSource() {
          // 方法实现
      }
      
      如果省略name值,则默认使用空字符串作为数据源名称(这通常需要根据应用上下文做特殊处理)。

综上,@DataSource注解设计用于在运行时动态指定方法应该使用哪个数据源,为实现多数据源策略提供了便利和灵活性。

sharding切片算法

复杂分片算法

import bts.core.models.constant.ShardingConstants;
import org.apache.shardingsphere.sharding.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.sharding.api.sharding.complex.ComplexKeysShardingValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
 
/**
 * 复杂分片算法
 */
public class MonthComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm {
 
    private static final Logger logger = LoggerFactory.getLogger(MonthComplexShardingAlgorithm.class);
    private Properties props = new Properties();
 
    @Override
    public Collection<String> doSharding(Collection collection, ComplexKeysShardingValue complexKeysShardingValue) {
        String logicTableName = complexKeysShardingValue.getLogicTableName();
        List list = (List) complexKeysShardingValue.getColumnNameAndShardingValuesMap().get("create_time");
        Instant instant = null;
        Object o = list.get(0);
        if (o instanceof Timestamp) {
            Timestamp timestamp = (Timestamp) o;
            instant = timestamp.toInstant();
        }
        if (o instanceof Date) {
            Date date = (Date) o;
            instant = date.toInstant();
        }
        //2023-11-01 00:00:00 起新增的数据将存在到cell_record_1表
        LocalDateTime shardingDateTime = LocalDateTime.of(2023, 11, 1, 0, 0, 0);
        if (LocalDateTime.ofInstant(instant, ZoneId.systemDefault()).isAfter(shardingDateTime)) 		{
            return Arrays.asList(logicTableName + "_1");
        }
        if (LocalDateTime.ofInstant(instant, ZoneId.systemDefault()).isBefore(shardingDateTime)) {
            return Arrays.asList(logicTableName);
        }
        return null;
    }
 
 
    @Override
    public void init() {
        logger.info("初始化复杂分片算法");
    }
 
    @Override
    public String getType() {
        return ShardingConstants.MONTH_COMPLEX;
    }
 
    @Override
    public Properties getProps() {
        return props;
    }
 
    @Override
    public void setProps(Properties props) {
        this.props = props;
    }
}

这段代码定义了一个名为MonthComplexShardingAlgorithm的类,实现了Apache ShardingSphere的ComplexKeysShardingAlgorithm接口,用于实现一种基于时间(月份)的复杂分片策略。以下是代码的详细解释:

  1. 日志记录器初始化:
    • 使用LoggerFactory.getLogger()初始化一个日志记录器,用于记录MonthComplexShardingAlgorithm类的操作日志。
  2. 成员变量:
    • 定义了一个Properties类型的props变量,用于存储配置信息,并初始化为空Properties对象。
  3. 核心分片逻辑实现 (doSharding方法):
    • 接收逻辑表名和复杂的分片键值对集合。
    • 从键值对集合中获取名为"create_time"的列对应的值列表,这个值通常与创建时间有关。
    • 将获取到的时间戳或日期转换为Instant对象,进一步转换为本地日期时间LocalDateTime,以便于比较。
    • 判断传入的时间是否在2023年11月1日0点之后,如果是,则将数据路由到logicTableName_1(即逻辑表名后加上"_1"),否则保持原逻辑表名。
    • 注意,如果时间正好在分界点上,当前逻辑会返回null,这在实际应用中可能需要额外处理以避免逻辑漏洞。
  4. 初始化方法 (init):
    • 简单打印一条日志信息,标志着分片算法的初始化完成。
  5. 类型获取方法 (getType):
    • 返回一个字符串常量ShardingConstants.MONTH_COMPLEX,用以标识该分片算法的类型。
  6. 配置属性的获取与设置方法 (getPropssetProps):
    • 分别用于获取和设置该分片算法的配置属性,尽管在这个示例中没有具体使用这些属性,但它们为算法的配置扩展提供了可能性。

总之,这段代码定义了一个自定义的复杂分片算法,用于根据数据的创建时间将数据分散到不同的物理表中,特别地,它根据2023年11月1日作为一个分界点,决定数据应该存储在哪一个分片表中。这样的分片策略有利于数据管理和查询优化,特别是在处理大量按时间序列增长的数据时。

按月分片算法

import bts.core.models.constant.ShardingConstants;
import cn.hutool.core.date.DateUtil;
import com.google.common.collect.Range;
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.util.Collection;
import java.util.Date;
import java.util.Properties;
import java.util.Set;
 
/**
 * 按月分片算法
 */
public class MonthStandardShardingAlgorithm implements StandardShardingAlgorithm<Date> {
    private static final Logger logger = LoggerFactory.getLogger(MonthStandardShardingAlgorithm.class);
    private Properties props = new Properties();
 
 
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Date> preciseShardingValue) {
        // 获取到月份
        Date value = preciseShardingValue.getValue();
        String s = DateUtil.format(value, "yyyyMM");
        String name = preciseShardingValue.getLogicTableName() + "_" + s;
        if (collection.contains(name)) {
            return name;
        }
        throw new IllegalArgumentException("未找到匹配的数据表");
    }
 
    @Override
    public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Date> rangeShardingValue) {
        Range<Date> valueRange = rangeShardingValue.getValueRange();
        Date lowerEndpoint = valueRange.lowerEndpoint();
        Date upperEndpoint = valueRange.upperEndpoint();
 
        Set<String> tables = MonthPreciseShardingUtil.rangeShardingTables(collection, lowerEndpoint, upperEndpoint);
        logger.info("定位到分片表:{}", tables);
        return tables;
    }
 
    @Override
    public void init() {
        logger.info("初始化标准分片算法");
    }
 
    @Override
    public String getType() {
        return ShardingConstants.MONTH_STANDARD;
    }
 
    @Override
    public Properties getProps() {
        return props;
    }
 
    @Override
    public void setProps(Properties props) {
        this.props = props;
    }
}

这段代码定义了一个名为MonthStandardShardingAlgorithm的类,实现了Apache ShardingSphere的StandardShardingAlgorithm接口,专门用于实现基于日期月份的标准分片策略。以下是代码的详细解析:

  1. 精确分片逻辑实现 (doSharding方法,针对PreciseShardingValue):
    • 输入参数包括所有可用的物理表名集合和精确分片键值。
    • 提取分片键值(日期),使用Hutool的DateUtil.format方法格式化为"yyyyMM"格式,代表月份。
    • 构建目标物理表名:逻辑表名加月份后缀,如"table_202304"。
    • 检查构建的表名是否在可用表名集合中存在,存在则返回该表名;否则抛出异常,因为没有匹配的物理表。
  2. 范围分片逻辑实现 (doSharding方法,针对RangeShardingValue):
    • 输入参数同样包括所有可用的物理表名集合和范围分片键值。
    • 提取范围分片的起始和结束日期。
    • 调用MonthPreciseShardingUtil.rangeShardingTables方法(该方法未展示,假设是自定义方法)来计算在这个日期范围内的所有应分片的表名集合。
    • 记录日志并返回该集合。
  3. 初始化方法 (init):
    • 简单记录一条日志,表明算法已初始化。
  4. 类型获取方法 (getType):
    • 返回一个字符串常量ShardingConstants.MONTH_STANDARD,用于标识分片算法类型。
  5. 配置属性的获取与设置方法 (getPropssetProps):
    • 分别用于获取和设置分片算法的配置属性,尽管示例中未直接使用,但保留了扩展性。

综上所述,MonthStandardShardingAlgorithm类提供了一种按照日期的月份进行数据分片的策略,支持精确分片(针对单个日期)和范围分片(针对日期区间),能够有效地将数据分配到不同月份后缀的物理表中,适用于按时间序列组织数据的场景,有利于管理和查询优化。

按月分片算法工具类

import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
 
/**
 * 按月分片工具类
 */
public class MonthPreciseShardingUtil {
 
    private static final String TEST_RECORD = "cell_record_";
  
    private static final Integer ONE = 1;
    private static final Integer TWELVE = 12;
    private static final Integer THREE_HUNDRED_SIXTY_FIVE = 365;
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
 
    private MonthPreciseShardingUtil() {
    }
 
    /**
     * 根据时间上下界,过滤相应的表名
     *
     * @param originalTables 表名列表
     * @param lowerEndpoint  下界
     * @param upperEndpoint  上界
     * @return 表名列表
     */
    public static Set<String> rangeShardingTables(Collection<String> originalTables, Date lowerEndpoint, Date upperEndpoint) {
        Set<String> tables = new HashSet<>();
        // 获取时间范围内的 月份集合
        Calendar min = Calendar.getInstance();
        Calendar max = Calendar.getInstance();
        min.setTime(lowerEndpoint);
        max.setTime(upperEndpoint);
        Calendar curr = min;
        while (curr.before(max)) {
            Date time = curr.getTime();
            String name = TEST_RECORD + sdf.format(time);
            if (originalTables.contains(name)) {
                tables.add(name);
            }
            curr.add(Calendar.MONTH, 1);
            curr.set(Calendar.DAY_OF_MONTH, 1);
        }
 
        return tables;
    }
 
    /**
     * 根据返回,获取到范围涉及的月份
     *
     * @param lowerEndpoint 下限
     * @param upperEndpoint 上限
     * @return 月份集合
     */
    public static Set<Integer> getSuffixListForRange(LocalDateTime lowerEndpoint, LocalDateTime upperEndpoint) {
        Set<Integer> sets = new TreeSet<>();
        if (Duration.between(lowerEndpoint, upperEndpoint).toDays() > THREE_HUNDRED_SIXTY_FIVE) {
            // 时间跨度大于一年,直接返回12个月
            for (int i = ONE; i <= TWELVE; i++) {
                sets.add(i);
            }
            return sets;
        }
        int lowerEndpointMonth = lowerEndpoint.getMonth().getValue();
        int upperEndpointMonth = upperEndpoint.getMonth().getValue();
 
        if (lowerEndpointMonth <= upperEndpointMonth) {
            for (int i = lowerEndpointMonth; i <= upperEndpointMonth; i++) {
                sets.add(i);
            }
        } else {
            for (int i = ONE; i <= upperEndpointMonth; i++) {
                sets.add(i);
            }
            for (int i = lowerEndpointMonth; i <= TWELVE; i++) {
                sets.add(i);
            }
        }
        return sets;
    }
 
    /**
     * 根据返回,获取到范围涉及的月份
     *
     * @param lowerEndpoint 下限
     * @param upperEndpoint 上限
     * @return 月份集合
     */
    public static Set<Integer> getSuffixListForRange(LocalDate lowerEndpoint, LocalDate upperEndpoint) {
        return getSuffixListForRange(LocalDateTime.of(lowerEndpoint, LocalTime.MIN), LocalDateTime.of(upperEndpoint, LocalTime.MAX));
    }
}

这段代码定义了一个名为MonthPreciseShardingUtil的工具类,提供了几个静态方法用于按月分片相关的处理,主要服务于数据库分片场景,特别是针对那些需要根据时间范围来决定访问哪些分片表的情况。以下是各部分的详细解析:

  1. 常量定义:

    • TEST_RECORD:前缀字符串,通常用于拼接表名,如cell_record_YYYYMM
    • ONE, TWELVE, THREE_HUNDRED_SIXTY_FIVE:代表常数值1、12和365,用于简化代码中的数值表达。
  2. 私有构造器:

    • private MonthPreciseShardingUtil():工具类常见的设计模式,防止外部实例化。
  3. 日期格式化工具:

    • 使用SimpleDateFormat实例sdf,格式化日期为"yyyyMM"形式,用于生成表名的月份部分。
  4. 范围分片方法 (rangeShardingTables):

    • 输入参数为原始表名集合、时间下界和上界。
    • 通过遍历时间范围内的每个月份,生成对应的表名(格式如cell_record_YYYYMM),并检查这些表名是否存在于给定的原始表集合中,如果存在则添加到结果集合。
    • 返回包含符合条件的所有表名的HashSet。
  5. 获取月份范围方法 (getSuffixListForRange,两个重载版本):

    • 第一个版本接受LocalDateTime类型的下界和上界,计算两者间涉及到的所有月份并返回为一个TreeSet,确保集合有序且不重复。
      • 如果时间跨度超过一年,则简单返回1到12代表全年各月。
      • 否则,根据边界月份逻辑填充集合。
    • 第二个版本接受LocalDate类型参数,通过将其转换为包含当天开始和结束时间的LocalDateTime调用上述方法,简化日期处理逻辑。

整体而言,MonthPreciseShardingUtil类提供了按月粒度处理时间范围分片的功能,主要用于确定在给定时间跨度内需要访问哪些按月分片的数据库表,广泛应用于需按时间维度分片的数据库架构设计中,比如日志记录、历史数据归档等场景。

在ShardingSphere中注册自定义分片算法

路径src/main/resources/META-INF/services新增org.apache.shardingsphere.sharding.spi.ShardingAlgorithm文件

内容(分片算法的路径)

bts.core.configuration.sharding.MonthStandardShardingAlgorithm
bts.core.configuration.sharding.MonthComplexShardingAlgorithm

在这里插入图片描述

当ShardingSphere启动并需要使用分片算法时,它会扫描此配置文件并自动加载列出的所有实现类。这意味着你的自定义分片算法现在已成为ShardingSphere可识别并使用的分片策略之一,可以在分片规则配置中引用它们,以决定数据如何在不同分片中分布。

通过这种方式,SPI机制极大地增强了ShardingSphere的灵活性和可扩展性,允许用户根据业务需求轻松集成定制化的分片逻辑。


links:

ShardingJDBC-5.1.1+Druid-1.2.8整合动态数据源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学亮编程手记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值