此文将整合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框架的一些注解来配置和管理数据源属性。下面是对这段代码中主要部分的逐行解释:
-
@Data
:这是Lombok库提供的注解,它是一个组合注解,相当于添加了@Getter
,@Setter
,@RequiredArgsConstructor
,@ToString
,@EqualsAndHashCode
这几个注解。它的作用是为类中的所有字段自动生成getter/setter方法、equals()、hashCode()、toString()方法,以及一个无参构造器(如果类中有final字段或者未初始化的非null字段,则会生成一个包含这些字段的构造器)。 -
@Configuration
:Spring框架的注解,表明这个类是一个配置类,相当于XML配置文件中的<beans>
标签。Spring会扫描带有此注解的类,并读取其中的bean定义信息,用于创建和管理bean实例。 -
@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
属性上。 -
public class DataSourceProperties { ... }
:定义了一个名为DataSourceProperties
的公共类。
5-13行. 这段代码定义了9个DruidDataSource
类型的字段,分别命名为first
到ninth
。DruidDataSource
是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
类,目的是实现数据源的动态切换功能。下面是代码的详细解析:
-
成员变量:
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
定义了一个线程局部变量,用于在每个线程中保存当前应该使用的数据源的key。ThreadLocal保证了每个线程都有自己的独立副本,从而避免了线程安全问题。
-
构造函数:
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {...}
构造函数接收两个参数:一个是默认的目标数据源(defaultTargetDataSource
),另一个是目标数据源集合(targetDataSources
)。在这个构造函数中,它通过调用父类的方法设置了默认数据源和可选数据源,并调用了afterPropertiesSet
完成必要的初始化。
-
重写
determineCurrentLookupKey
方法:@Override protected Object determineCurrentLookupKey() {...}
此方法是从AbstractRoutingDataSource
继承而来并被重写,用于确定当前要使用哪个数据源。在此实现中,它直接返回了通过getDataSource()
方法获取的数据源key。
-
静态方法:
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)。下面是代码的详细解析:
-
注解说明:
@Slf4j
: Lombok注解,自动为类添加一个org.slf4j.Logger
类型的成员变量log
,用于日志输出。@Configuration
: 标记该类为Spring的配置类,用于声明Bean。@Order(3)
: 指定配置类的加载顺序,数字越小,优先级越高,此处设置为3表示该配置在其他标记了@Order
注解的配置之后加载,或者在没有指定顺序的配置之前加载。
-
成员变量与依赖注入:
@Autowired private DataSourceProperties properties;
:自动注入了前面定义的DataSourceProperties
类的实例,用于获取配置的多个数据源属性。@Value
注解:用于注入配置文件中的属性值,如数据库URL、用户名、密码等。
-
方法说明:
@Bean @Primary public DynamicDataSource dataSource() {...}
:声明了一个Bean,它是数据源的实现,标记为@Primary
意味着它是默认首选的数据源。此方法初始化了一个DynamicDataSource
,结合了多个数据源(通过buildDataSource()
方法构建的第一个数据源和其他从DataSourceProperties
中获取的数据源),并使用了Map
来存储这些数据源,键为数据源的名称。
-
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(面向切面编程)的实现,用于动态切换数据源。以下是代码的详细解释:
-
注解说明:
@Aspect
: 标记该类为一个切面类,用于定义切面逻辑,如前置通知、后置通知、环绕通知等。@Component
: 表明该类是一个Spring组件,将会被Spring容器自动扫描并管理。implements Ordered
: 实现Ordered接口,允许定义该切面的执行顺序,其中getOrder()
方法返回的值越小,执行优先级越高。
-
成员变量:
Logger logger = LoggerFactory.getLogger(getClass());
:使用SLF4J的日志框架,创建一个日志记录器实例,用于记录日志信息。
-
切点定义 (
@Pointcut
):@Pointcut("@annotation(bts.core.annotation.DataSource)")
: 定义了一个切点表达式,匹配所有标注了bts.core.annotation.DataSource
注解的方法。这意味着任何带有此注解的方法调用都会触发切面逻辑。
-
环绕通知 (
@Around
):@Around("dataSourcePointCut()")
: 声明一个环绕通知,应用在上面定义的切点上。- 方法体中:
- 首先,通过
ProceedingJoinPoint
获取正在执行的方法签名和方法对象。 - 然后,尝试从方法上获取
DataSource
注解,如果存在,则使用该注解指定的数据源名称;如果不存在,默认使用DataSourceNames.FIRST
。 - 调用
DynamicDataSource.setDataSource(...)
来设置当前线程的数据源。 - 使用
point.proceed()
执行被代理方法的实际逻辑。 - 在
finally
块中,调用DynamicDataSource.clearDataSource()
清除当前线程的数据源设置,并记录日志。
- 首先,通过
-
getOrder()方法:
- 返回值
1
表示此切面的执行优先级。如果有多个切面,优先级高的先执行。
- 返回值
综上,这段代码实现了一个AOP切面,其核心功能是在方法执行前后动态切换数据源。当被拦截的方法上标注了自定义的DataSource
注解时,就使用该注解指定的数据源;若未标注,则使用默认数据源。通过这样的方式,可以在运行时根据业务需求灵活地切换数据源,非常适合多数据源环境下的数据库操作。
数据源注解
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String name() default "";
}
这段代码定义了一个自定义注解DataSource
,用于标记在方法级别上,指示该方法应该使用哪个数据源。下面是该注解定义的详细解析:
-
@Target(ElementType.METHOD)
:@Target
是一个元注解,用于指定被修饰的注解可以应用在哪些程序元素上。这里设置为ElementType.METHOD
,意味着DataSource
注解只能应用于方法上。
-
@Retention(RetentionPolicy.RUNTIME)
:@Retention
也是一个元注解,用于设定注解的生命周期。RetentionPolicy.RUNTIME
表示这个注解不仅在编译期保留,还会在运行时保留,因此可以通过反射机制读取到这个注解的信息。这对于在运行时需要根据注解信息做出行为决策(比如本例中动态切换数据源)的场景非常重要。
-
@Documented
:@Documented
注解表明这个注解应该被JavaDoc工具文档化。也就是说,当生成项目的API文档时,这个注解及其说明也会被包含进去,有助于提高代码的可读性和文档的完整性。
-
public @interface DataSource { ... }
:- 这里定义了
DataSource
注解接口,它本身就是一个注解类型。里面定义了一个方法name()
,这是一个注解元素,用于在使用注解时提供具体的值。
- 这里定义了
-
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
接口,用于实现一种基于时间(月份)的复杂分片策略。以下是代码的详细解释:
- 日志记录器初始化:
- 使用
LoggerFactory.getLogger()
初始化一个日志记录器,用于记录MonthComplexShardingAlgorithm
类的操作日志。
- 使用
- 成员变量:
- 定义了一个
Properties
类型的props
变量,用于存储配置信息,并初始化为空Properties
对象。
- 定义了一个
- 核心分片逻辑实现 (
doSharding
方法):- 接收逻辑表名和复杂的分片键值对集合。
- 从键值对集合中获取名为"create_time"的列对应的值列表,这个值通常与创建时间有关。
- 将获取到的时间戳或日期转换为
Instant
对象,进一步转换为本地日期时间LocalDateTime
,以便于比较。 - 判断传入的时间是否在2023年11月1日0点之后,如果是,则将数据路由到
logicTableName_1
(即逻辑表名后加上"_1"),否则保持原逻辑表名。 - 注意,如果时间正好在分界点上,当前逻辑会返回
null
,这在实际应用中可能需要额外处理以避免逻辑漏洞。
- 初始化方法 (
init
):- 简单打印一条日志信息,标志着分片算法的初始化完成。
- 类型获取方法 (
getType
):- 返回一个字符串常量
ShardingConstants.MONTH_COMPLEX
,用以标识该分片算法的类型。
- 返回一个字符串常量
- 配置属性的获取与设置方法 (
getProps
和setProps
):- 分别用于获取和设置该分片算法的配置属性,尽管在这个示例中没有具体使用这些属性,但它们为算法的配置扩展提供了可能性。
总之,这段代码定义了一个自定义的复杂分片算法,用于根据数据的创建时间将数据分散到不同的物理表中,特别地,它根据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
接口,专门用于实现基于日期月份的标准分片策略。以下是代码的详细解析:
- 精确分片逻辑实现 (
doSharding
方法,针对PreciseShardingValue
):- 输入参数包括所有可用的物理表名集合和精确分片键值。
- 提取分片键值(日期),使用Hutool的
DateUtil.format
方法格式化为"yyyyMM"格式,代表月份。 - 构建目标物理表名:逻辑表名加月份后缀,如"table_202304"。
- 检查构建的表名是否在可用表名集合中存在,存在则返回该表名;否则抛出异常,因为没有匹配的物理表。
- 范围分片逻辑实现 (
doSharding
方法,针对RangeShardingValue
):- 输入参数同样包括所有可用的物理表名集合和范围分片键值。
- 提取范围分片的起始和结束日期。
- 调用
MonthPreciseShardingUtil.rangeShardingTables
方法(该方法未展示,假设是自定义方法)来计算在这个日期范围内的所有应分片的表名集合。 - 记录日志并返回该集合。
- 初始化方法 (
init
):- 简单记录一条日志,表明算法已初始化。
- 类型获取方法 (
getType
):- 返回一个字符串常量
ShardingConstants.MONTH_STANDARD
,用于标识分片算法类型。
- 返回一个字符串常量
- 配置属性的获取与设置方法 (
getProps
和setProps
):- 分别用于获取和设置分片算法的配置属性,尽管示例中未直接使用,但保留了扩展性。
综上所述,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
的工具类,提供了几个静态方法用于按月分片相关的处理,主要服务于数据库分片场景,特别是针对那些需要根据时间范围来决定访问哪些分片表的情况。以下是各部分的详细解析:
-
常量定义:
TEST_RECORD
:前缀字符串,通常用于拼接表名,如cell_record_YYYYMM
。ONE
,TWELVE
,THREE_HUNDRED_SIXTY_FIVE
:代表常数值1、12和365,用于简化代码中的数值表达。
-
私有构造器:
private MonthPreciseShardingUtil()
:工具类常见的设计模式,防止外部实例化。
-
日期格式化工具:
- 使用
SimpleDateFormat
实例sdf
,格式化日期为"yyyyMM"形式,用于生成表名的月份部分。
- 使用
-
范围分片方法 (
rangeShardingTables
):- 输入参数为原始表名集合、时间下界和上界。
- 通过遍历时间范围内的每个月份,生成对应的表名(格式如
cell_record_YYYYMM
),并检查这些表名是否存在于给定的原始表集合中,如果存在则添加到结果集合。 - 返回包含符合条件的所有表名的HashSet。
-
获取月份范围方法 (
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: