SpringBoot @DS注解 和 DynamicDataSource自定义实现多数据源的2种实现方式

本文详细介绍了在SpringBoot项目中使用@DS注解和DynamicDataSource自定义实现多数据源的方法,包括配置、代码实现、原理和优缺点分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

在实际的项目中,我们经常会遇到需要操作多个数据源的情况,SpringBoot为我们提供了多种实现多数据源的方式。本文将介绍两种常见的方式:使用@DS注解实现多数据源的切换以及使用DynamicDataSource自定义实现多数据源的切换。

我们将分别介绍这两种方法的实现原理和代码实现,并对比它们的优劣势。

方式一、使用DynamicDataSource实现多数据源

1、在application.yml文件中配置多个数据源

# 数据源配置
spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        druid:
            # 主库数据源
            master:
                url: jdbc:mysql://xxx.xxx.xxx.101:3306/test1?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
                username: root
                password: root123
            # 从库数据源
            slave:
              # 从数据源开关/默认关闭
                enabled: true
                url: jdbc:mysql://xxx.xxx.xxx.102:3306/test2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
                username: root123
                password: root123
            oracle:
              enabled: true
              url: jdbc:oracle:thin:@//xxx.xxx.xxx.103:1521/test3?useUnicode=true&characterEncoding=AL32UTF8
              username: root
              password: root123

2、添加数据源到targetDataSources集合中

创建配置文件,把多个数据源添加到targetDataSources集合中,然后返回动态数据源配置。

/**
 * druid 配置多数据源
 * 
 * @author admin
 */
@Configuration
public class DruidConfig
{

    /**
     * master数据源的配置
     */
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(DruidProperties druidProperties)
    {
        // 创建Druid数据源
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        // 返回由Druid属性配置后的数据源
        return druidProperties.dataSource(dataSource);
    }

    /**
     * slave数据源的配置
     */
    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
    public DataSource slaveDataSource(DruidProperties druidProperties)
    {
        // 创建Druid数据源
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        // 返回由Druid属性配置后的数据源
        return druidProperties.dataSource(dataSource);
    }

    /**
     * oracle数据源的配置
     */
    @Bean
    @ConfigurationProperties("spring.datasource.druid.oracle")
    @ConditionalOnProperty(prefix = "spring.datasource.druid.oracle", name = "enabled", havingValue = "true")
    public DataSource oracleDataSource(DruidProperties druidProperties)
    {
        // 创建Druid数据源
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        // 设置Oracle数据库驱动类
        dataSource.setDriverClassName("oracle.jdbc.OracleDriver");
        // 返回由Druid属性配置后的数据源
        return druidProperties.dataSource(dataSource);
    }

    /**
     * 动态数据源配置
     */
    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource(DataSource masterDataSource)
    {
        // 目标数据源的映射
        Map<Object, Object> targetDataSources = new HashMap<>();
        // 将master数据源放入映射中
        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
        // 将slave数据源放入映射中
        setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
        // 将oracle数据源放入映射中
        setDataSource(targetDataSources, DataSourceType.ORACLE.name(), "oracleDataSource");
        // 返回动态数据源对象
        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)
        {
        }
    }
}

上述代码中DynamicDataSource继承了AbstractRoutingDataSource抽象类,AbstractRoutingDataSource抽象类是动态数据源的核心实现类,它包含了几个关键方法:

  • determineCurrentLookupKey():这是一个抽象方法,必须由具体的子类实现。它的作用是决定当前应该使用哪个数据源的标识。当应用程序需要访问数据库时,AbstractRoutingDataSource会调用这个方法来确定要使用的数据源。
  • setTargetDataSources(Map<Object, Object> targetDataSources):这个方法用于设置目标数据源的Map。Map中的键值对表示数据源的标识和对应的数据源实例。当AbstractRoutingDataSource需要根据标识选择数据源时,会根据这个Map来进行查找。
  • setDefaultTargetDataSource(Object defaultTargetDataSource):这个方法用于设置默认的数据源。当在无法确定要使用的数据源时,AbstractRoutingDataSource会使用默认的数据源。
  • afterPropertiesSet():这个方法用于在设置完属性后进行一些必要的初始化工作。例如,在设置完目标数据源和默认数据源后,需要调用这个方法来确保AbstractRoutingDataSource的正确初始化。

通过show dragrams我们可以看到AbstractRoutingDataSource抽象类的关系图:

再看DynamicDataSource的具体实现:

public class DynamicDataSource extends AbstractRoutingDataSource
{
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
    {
        // 调用父类构造函数设置默认数据源和目标数据源
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet(); // 初始化
    }

    @Override
    protected Object determineCurrentLookupKey()
    {
        // 获取当前数据源的标识符
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

它主要做的事情就是调用父类构造函数设置默认数据源和目标数据源,完成初始化,然后重写determineCurrentLookupKey() 方法,返回当前数据源的标识。

3、动态数据源的使用

在实际开发过程中,我们通常使用开发注解的方式完成多数据源的切换,现在我们创建一个注解@DataSource,使用注解:

    @DataSource(value = DataSourceType.ORACLE)
    public Object test()
    {
        return userMapper.selectUserList();
    }

注解核心实现:

    @Around("dataPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable
    {
        DataSource dataSource = getDataSource(point);
        // 获取数据源的名称
        if (StringUtils.isNotNull(dataSource)) {
            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
        }
        try {
            return point.proceed();
        }
        finally {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }

DynamicDataSourceContextHolder类实现,DynamicDataSourceContextHolder类使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本。然后根据set赋值获取自己当前所需的数据源,而不会影响其它线程所对应的副本。

具体实现如下:

public class DynamicDataSourceContextHolder {
    
    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
    
    /**
     *  使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> THREAD_LOCAL_DATA = new ThreadLocal<>();

    /**
     * 设置数据源
     */
    public static void setDataSourceType(String dsType) {
        THREAD_LOCAL_DATA.set(dsType);
    }

    /**
     * 获得数据源
     */
    public static String getDataSourceType() {
        return THREAD_LOCAL_DATA.get();
    }

    /**
     * 清空数据源
     */
    public static void clearDataSourceType()
    {
        THREAD_LOCAL_DATA.remove();
    }
}

4、动态数据源实现流程总结:

首先,在Spring Boot应用的配置文件中配置多个数据源,例如MySQL和Oracle数据源,并创建一个自定义的DynamicDataSource类,继承AbstractRoutingDataSource。

在DynamicDataSource类中,需要重写determineCurrentLookupKey方法,该方法根据当前线程上下文中保存的数据源类型来确定当前应该使用的数据源。

接着,可以通过AOP拦截器或其他方式,在每次数据库操作之前根据业务逻辑设置当前线程的数据源类型到ThreadLocal中。这样在调用数据库操作时,DynamicDataSource会根据ThreadLocal中保存的数据源类型来选择对应的数据源进行操作。

最后,通过自定义注解@DataSource(value = DataSourceType.ORACLE)来标记需要切换数据源的方法或类,利用AOP切面编程,在方法执行前根据注解值设置当前线程的数据源类型,从而实现动态数据源切换的功能。

方式二、使用@DS注解实现多数据源

1、添加pom依赖:

注解@DS是基于dynamic-datasource-spring-boot-starter 实现多数据源切换的,添加pom依赖:

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.2</version>
</dependency>

2、配置数据源:

在application.properties或application.yml文件中配置多个数据源的连接信息。

spring:
  datasource:
    dynamic:
      primary:  # 主数据源
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db1
        username: user
        password: password
      secondary:  # 第二个数据源
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db2
        username: user
        password: password

3、使用数据源:

在服务类或数据访问层中通过@DS注解来指定使用哪个数据源,例如:

@DS("primary")
public List<User> listUsersFromPrimaryDataSource() {
    return userDao.listUsers();
}

@DS("secondary")
public List<User> listUsersFromSecondaryDataSource() {
    return userDao.listUsers();
}

4、实现原理:

从DynamicDataSourceAutoConfiguration作为入口,我们可以看@DS注解的实现原理

其实通过源码跟踪,不难发现@DS实现数据源切换,也是根据 AOP 切面、注解、ThreadLocal 等方式实现。@DS注解的封装,可以在方法或类上直接指定数据源,而无需修改代码。这使得切换数据源变得更加简单和直接。

通过源码查看:

其实,通过上述源码查看,可大致概括@DS注解的实现原理,主要如下:

扫描数据源配置信息: 在启动时,DynamicDataSourceAutoConfiguration 会扫描项目中定义的数据源配置信息,例如在配置文件中定义的多个数据源的连接信息。

创建数据源对象: 根据扫描到的数据源配置信息,动态创建对应的数据源对象,可以是基于不同数据库的 DataSource 实现类,如 DruidDataSource、HikariDataSource 等。

数据源路由策略: 定义数据源的路由策略,即根据业务需求或者特定条件来决定使用哪个数据源。这可以通过 AOP 切面、注解、ThreadLocal 等方式实现。

数据源切换: 在需要访问数据库的地方,根据路由策略选择合适的数据源,并将该数据源设置为当前线程的数据源上下文中,以确保后续的数据库操作都使用选定的数据源。

数据源清理: 在数据源使用完毕后,需要及时清理数据源上下文,避免数据源泄漏或混乱。

总的来说,DynamicDataSourceAutoConfiguration 的实现原理主要涉及数据源的创建、路由策略的制定和数据源的切换管理。通过这些步骤,可以实现在运行时动态切换数据源,从而实现多数据源的灵活应用。

以上就是SpringBoot @DS注解 和 DynamicDataSource自定义实现多数据源的实现方式,可根据实际业务需要进行选择和调整。

### Spring Boot多数据源配置@DS注解无法找到Primary数据源的解决方案 在Spring Boot项目中,当使用`@DS`注解实现多数据源切换时,可能会遇到找不到Primary数据源的问题。这通常是由于未正确指定默认的数据源或者未按照框架的要求完成必要的配置所致。 #### 1. 配置默认数据源 根据描述,在多数据源场景下,默认数据源的作用至关重要。如果没有显式定义默认数据源,则可能导致运行时异常。可以通过以下方式设置默认数据源: - 使用`AbstractRoutingDataSource`中的`setDefaultTargetDataSource()`方法来设定默认数据源[^1]。 ```java @Configuration public class DataSourceConfig { @Bean public DataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) { DynamicDataSource dynamicDataSource = new DynamicDataSource(); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("master", masterDataSource); targetDataSources.put("slave", slaveDataSource); dynamicDataSource.setDefaultTargetDataSource(masterDataSource); // 设置默认数据源为主库 dynamicDataSource.setTargetDataSources(targetDataSources); // 添加其他数据源映射关系 return dynamicDataSource; } } ``` 上述代码片段展示了如何通过`DynamicDataSource`类(继承自`AbstractRoutingDataSource`)来配置多个目标数据源以及默认数据源[^3]。 --- #### 2. 正确标注Primary数据源 如果存在多个`@Configuration`类用于定义不同数据源实例,则需要明确标记其中一个为Primary数据源。这是为了避免Spring容器初始化过程中因歧义而抛出异常。 可以在主数据源对应的配置类上添加`@Primary`注解,确保其优先级最高: ```java @Bean @Primary public DataSource masterDataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl("jdbc:mysql://localhost:3306/master_db"); dataSource.setUsername("root"); dataSource.setPassword("password"); return dataSource; } @Bean public DataSource slaveDataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl("jdbc:mysql://localhost:3306/slave_db"); dataSource.setUsername("root"); dataSource.setPassword("password"); return dataSource; } ``` 此处利用了`@Primary`注解指定了`masterDataSource`作为主要数据源[^4]。 --- #### 3. 动态切换逻辑完善 对于动态切换数据源的需求,需依赖于线程上下文中存储的信息。例如,可通过工具类维护当前请求所使用的数据源名称,并将其传递给`determineCurrentLookupKey()`方法以供判断依据。 以下是典型实现示例: ```java // 工具类:负责保存/清除当前线程绑定的数据源类型 public class DataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static void setDataSourceType(String dsType) { contextHolder.set(dsType); } public static String getDataSourceType() { return contextHolder.get(); } public static void clearDataSourceType() { contextHolder.remove(); } } // 自定义数据源路由器 public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSourceType(); // 获取当前线程绑定的数据源键值 } } ``` 此部分实现了基于线程局部变量(`ThreadLocal`)的方式记录每次调用所需的具体数据源标识符。 --- #### 4. Mapper层配合@DS注解 最后一步是在MyBatis Plus集成环境下合理运用`@DS`注解标明各Mapper接口关联的目标数据库资源。例如: ```java @Mapper @DS("slave") // 明确指出该Mapper对应的是从库(slave) public interface SlaveUserMapper extends BaseMapper<User> {} ``` 注意,只有当业务逻辑中有明确需求指向特定数据源时才应采用这种方式;否则将沿用全局范围内的默认行为——即访问已声明好的Primary数据源。 --- ### 总结 综上所述,针对Spring Boot多数据源配置中`@DS`注解放生“找不到Primary数据源”的问题,可以从以下几个方面入手排查并修复: 1. **确认是否存在有效且唯一被设为Primary的数据源**; 2. **验证是否正确定义了抽象路由型数据源及其内部成员属性赋值过程**; 3. **检查实际执行路径下的线程环境变量状态是否匹配预期条件**。 只要遵循以上指导原则逐一调整相应环节即可顺利解决问题。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值