大家好,我是小富~
项目开发中经常会遇到多数据源同时使用的场景,比如冷热数据的查询等情况,我们可以使用类似现成的工具包来解决问题,但在多数据源的使用中通常伴随着定制化的业务,所以一般的公司还是会自行实现多数据源切换的功能,接下来一起使用实现自定义注解的形式来实现一下。
pom.xml文件引入必要的Jar
pom.xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.6</version> </parent> <groupId>com.dynamic</groupId> <artifactId>springboot-dynamic-datasource</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <mybatis.plus.version>3.5.3.1</mybatis.plus.version> <mysql.connector.version>8.0.32</mysql.connector.version> <druid.version>1.2.6</druid.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- springboot核心包 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mysql驱动包 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>${mysql.connector.version}</version> </dependency> <!-- lombok工具包 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- MyBatis Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis.plus.version}</version> </dependency> <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.7</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.6</version>
</parent>
<groupId>com.dynamic</groupId>
<artifactId>springboot-dynamic-datasource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<mybatis.plus.version>3.5.3.1</mybatis.plus.version>
<mysql.connector.version>8.0.32</mysql.connector.version>
<druid.version>1.2.6</druid.version>
</properties>
<dependencies>
<dependency>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- springboot核心包 -->
<artifactId>spring-boot-starter-web</artifactId>
<!-- mysql驱动包 -->
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.connector.version}</version>
<!-- lombok工具包 -->
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<!-- MyBatis Plus -->
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.plus.version}</version>
<!-- druid -->
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
我们应用ThreadLocal来管理数据源信息,通过其中内容的get,set,remove方法来获取、设置、删除当前线程对应的数据源。
/** * ThreadLocal存放数据源变量 * * @author 公众号:程序员小富 * @date 2023/11/27 11:02 */public class DataSourceContextHolder { private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>(); /** * 获取当前线程的数据源 * * @return 数据源名称 */ public static String getDataSource() { return DATASOURCE_HOLDER.get(); } /** * 设置数据源 * * @param dataSourceName 数据源名称 */ public static void setDataSource(String dataSourceName) { DATASOURCE_HOLDER.set(dataSourceName); } /** * 删除当前数据源 */ public static void removeDataSource() { DATASOURCE_HOLDER.remove(); }}
/**
* ThreadLocal存放数据源变量
*
* @author 公众号:程序员小富
* @date 2023/11/27 11:02
*/
public class DataSourceContextHolder {
private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
* 获取当前线程的数据源
* @return 数据源名称
public static String getDataSource() {
return DATASOURCE_HOLDER.get();
}
* 设置数据源
* @param dataSourceName 数据源名称
public static void setDataSource(String dataSourceName) {
DATASOURCE_HOLDER.set(dataSourceName);
* 删除当前数据源
public static void removeDataSource() {
DATASOURCE_HOLDER.remove();
创建 DynamicDataSource 类并继承 AbstractRoutingDataSource,这样我们就可以重置当前的数据库路由,实现切换成想要执行的目标数据库。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import javax.sql.DataSource;import java.util.Map;/** * 重置当前的数据库路由,实现切换成想要执行的目标数据库 * * @author 公众号:程序员小富 * @date 2023/11/27 11:02 */public class DynamicDataSource extends AbstractRoutingDataSource { public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) { super.setDefaultTargetDataSource(defaultDataSource); super.setTargetDataSources(targetDataSources); } /** * 这一步是关键,获取注册的数据源信息 * @return */ @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); }}
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;
* 重置当前的数据库路由,实现切换成想要执行的目标数据库
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
* 这一步是关键,获取注册的数据源信息
* @return
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
在 application.yml 中配置数据库信息,使用dynamic_datasource_1、dynamic_datasource_2两个数据库
dynamic_datasource_1
dynamic_datasource_2
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: master: url: jdbc:mysql://127.0.0.1:3306/dynamic_datasource_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 12345 driver-class-name: com.mysql.cj.jdbc.Driver slave: url: jdbc:mysql://127.0.0.1:3306/dynamic_datasource_2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 12345 driver-class-name: com.mysql.cj.jdbc.Driver
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
url: jdbc:mysql://127.0.0.1:3306/dynamic_datasource_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 12345
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://127.0.0.1:3306/dynamic_datasource_2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
再将多个数据源注册到DataSource.
DataSource
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import javax.sql.DataSource;import java.util.HashMap;import java.util.Map;/** * 注册多个数据源 * * @author 公众号:程序员小富 * @date 2023/11/27 11:02 */@Configurationpublic class DateSourceConfig { @Bean @ConfigurationProperties("spring.datasource.druid.master") public DataSource dynamicDatasourceMaster() { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.druid.slave") public DataSource dynamicDatasourceSlave() { return DruidDataSourceBuilder.create().build(); } @Bean(name = "dynamicDataSource") @Primary public DynamicDataSource createDynamicDataSource() { Map<Object, Object> dataSourceMap = new HashMap<>(); // 设置默认的数据源为Master DataSource defaultDataSource = dynamicDatasourceMaster(); dataSourceMap.put("master", defaultDataSource); dataSourceMap.put("slave", dynamicDatasourceSlave()); return new DynamicDataSource(defaultDataSource, dataSourceMap); }}
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import java.util.HashMap;
* 注册多个数据源
@Configuration
public class DateSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource dynamicDatasourceMaster() {
return DruidDataSourceBuilder.create().build();
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource dynamicDatasourceSlave() {
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource createDynamicDataSource() {
Map<Object, Object> dataSourceMap = new HashMap<>();
// 设置默认的数据源为Master
DataSource defaultDataSource = dynamicDatasourceMaster();
dataSourceMap.put("master", defaultDataSource);
dataSourceMap.put("slave", dynamicDatasourceSlave());
return new DynamicDataSource(defaultDataSource, dataSourceMap);
在启动类的@SpringBootApplication注解中排除DataSourceAutoConfiguration,否则会报错。
@SpringBootApplication
DataSourceAutoConfiguration
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
到这多数据源的基础配置就结束了,接下来测试一下
创建两个库dynamic_datasource_1、dynamic_datasource_2,库中均创建同一张表 t_dynamic_datasource_data。
CREATE TABLE `t_dynamic_datasource_data` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `source_name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`));
CREATE TABLE `t_dynamic_datasource_data` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`source_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
);
dynamic_datasource_1.t_dynamic_datasource_data表中插入
insert into t_dynamic_datasource_data (source_name) value ('dynamic_datasource_master');
dynamic_datasource_2.t_dynamic_datasource_data表中插入
insert into t_dynamic_datasource_data (source_name) value ('dynamic_datasource_slave');
这里我准备了一个接口来验证,传入的 datasourceName 参数值就是刚刚注册的数据源的key。
/** * 动态数据源切换 * * @author 公众号:程序员小富 * @date 2023/11/27 11:02 */@RestControllerpublic class DynamicSwitchController { @Resource private DynamicDatasourceDataMapper dynamicDatasourceDataMapper; @GetMapping("/switchDataSource/{datasourceName}") public String switchDataSource(@PathVariable("datasourceName") String datasourceName) { DataSourceContextHolder.setDataSource(datasourceName); DynamicDatasourceData dynamicDatasourceData = dynamicDatasourceDataMapper.selectOne(null); DataSourceContextHolder.removeDataSource(); return dynamicDatasourceData.getSourceName(); }}
* 动态数据源切换
@RestController
public class DynamicSwitchController {
@Resource
private DynamicDatasourceDataMapper dynamicDatasourceDataMapper;
@GetMapping("/switchDataSource/{datasourceName}")
public String switchDataSource(@PathVariable("datasourceName") String datasourceName) {
DataSourceContextHolder.setDataSource(datasourceName);
DynamicDatasourceData dynamicDatasourceData = dynamicDatasourceDataMapper.selectOne(null);
DataSourceContextHolder.removeDataSource();
return dynamicDatasourceData.getSourceName();
传入参数master时:127.0.0.1:9004/switchDataSource/master
传入参数slave时:127.0.0.1:9004/switchDataSource/slave
通过执行结果,我们看到传递不同的数据源名称,已经实现了查询对应的数据库数据。
上边已经成功实现了手动切换数据源,但这种方式顶多算是半自动,下边我们来使用注解方式实现动态切换。
我们先定一个名为DS的注解,作用域为METHOD方法上,由于@DS中设置的默认值是:master,因此在调用主数据源时,可以不用进行传值。
DS
/** * 定于数据源切换注解 * * @author 公众号:程序员小富 * @date 2023/11/27 11:02 */@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface DS { // 默认数据源master String value() default "master";}
* 定于数据源切换注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
// 默认数据源master
String value() default "master";
定义了@DS注解后,紧接着实现注解的AOP逻辑,拿到注解传递值,然后设置当前线程的数据源
@DS
import com.dynamic.config.DataSourceContextHolder;import lombok.extern.slf4j.Slf4j;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.springframework.stereotype.Component;import java.lang.reflect.Method;import java.util.Objects;/** * 实现@DS注解的AOP切面 * * @author 公众号:程序员小富 * @date 2023/11/27 11:02 */@Aspect@Component@Slf4jpublic class DSAspect { @Pointcut("@annotation(com.dynamic.aspect.DS)") public void dynamicDataSource() { } @Around("dynamicDataSource()") public Object datasourceAround(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); DS ds = method.getAnnotation(DS.class); if (Objects.nonNull(ds)) { DataSourceContextHolder.setDataSource(ds.value()); } try { return point.proceed(); } finally { DataSourceContextHolder.removeDataSource(); } }}
import com.dynamic.config.DataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
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.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Objects;
* 实现@DS注解的AOP切面
@Aspect
@Component
@Slf4j
public class DSAspect {
@Pointcut("@annotation(com.dynamic.aspect.DS)")
public void dynamicDataSource() {
@Around("dynamicDataSource()")
public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DS ds = method.getAnnotation(DS.class);
if (Objects.nonNull(ds)) {
DataSourceContextHolder.setDataSource(ds.value());
try {
return point.proceed();
} finally {
再添加两个接口测试,使用@DS注解标注,使用不同的数据源名称,内部执行相同的查询条件,看看结果如何?
@DS(value = "master")@GetMapping("/dbMaster")public String dbMaster() { DynamicDatasourceData dynamicDatasourceData = dynamicDatasourceDataMapper.selectOne(null); return dynamicDatasourceData.getSourceName();}
@DS(value = "master")
@GetMapping("/dbMaster")
public String dbMaster() {
@DS(value = "slave")@GetMapping("/dbSlave")public String dbSlave() { DynamicDatasourceData dynamicDatasourceData = dynamicDatasourceDataMapper.selectOne(null); return dynamicDatasourceData.getSourceName();}
@DS(value = "slave")
@GetMapping("/dbSlave")
public String dbSlave() {
通过执行结果,看到通过应用@DS注解也成功的进行了数据源的切换。
在动态切换数据源的时候有一个问题是要考虑的,那就是事务管理是否还会生效呢?
我们做个测试,新增一个接口分别插入两条记录,其中在插入第二条数据时将值设置超过了字段长度限制,会产生Data too long for column异常。
Data too long for column
/** * 验证一下事物控制 */// @Transactional(rollbackFor = Exception.class) @DS(value = "slave") @GetMapping("/dbTestTransactional") public void dbTestTransactional() { DynamicDatasourceData datasourceData = new DynamicDatasourceData(); datasourceData.setSourceName("test"); dynamicDatasourceDataMapper.insert(datasourceData); DynamicDatasourceData datasourceData1 = new DynamicDatasourceData(); datasourceData1.setSourceName("testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest"); dynamicDatasourceDataMapper.insert(datasourceData1); }
* 验证一下事物控制
// @Transactional(rollbackFor = Exception.class)
@GetMapping("/dbTestTransactional")
public void dbTestTransactional() {
DynamicDatasourceData datasourceData = new DynamicDatasourceData();
datasourceData.setSourceName("test");
dynamicDatasourceDataMapper.insert(datasourceData);
DynamicDatasourceData datasourceData1 = new DynamicDatasourceData();
datasourceData1.setSourceName("testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest");
dynamicDatasourceDataMapper.insert(datasourceData1);
经过测试发现执行结果如下,即便实现动态切换数据源,本地事务依然可以生效。
不加上@Transactional注解第一条记录可以插入,第二条插入失败
@Transactional
加上@Transactional注解两条记录都不会插入成功
本文案例地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot101/通用功能/springboot-config-order
原文链接:https://www.cnblogs.com/chengxy-nds/p/17926002.html
本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728