SpringBoot采用Dynamic-Datasource方式实现多JDBC数据源

本文详细介绍了如何在SpringBoot项目中使用Dynamic-Datasource实现多JDBC数据源配置,包括配置多个数据源、事务管理策略、DS注解的应用以及与Mybatis-Plus和SpringAOP的集成。通过例子展示了如何在服务中动态选择数据源并进行事务控制。

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

1. Dynamic-Datasource实现多JDBC数据源配置

1.1 特性

  • 提供本地多数据源事务方案。由AOP实现
  • 支持数据源分组,适用于多种场景,如多库读写分离、一主多从(实现了数据库负载均衡算法)、混合模式
  • 提供自定义数据源来源方案(如全从数据库加载)
  • 提供项目启动后动态增加移除数据源方案
  • 支持数据库敏感配置信息加密(可自定义)ENC()
  • 支持每个数据库独立初始化表结构schema和数据库database
  • 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)
  • 支持自定义注解,需继承DS(3.2.0+)
  • 提供并简化对Druid、HikariCp、BeeCp、Dbcp2的快速集成
  • 提供对Mybatis-Plus、Quartz、ShardingJdbc、P6sy、Jndi等组件的集成方案
  • 提供Mybatis环境下的纯读写分离方案
  • 提供使用spel动态参数解析数据源方案。内置spel、session、header,支持自定义
  • 支持多层数据源嵌套切换。(ServiceA >>> ServiceB >>> ServiceC)
  • 支持Seata分布式事务

1.2 Mysql数据准备

分别创建read_db.user和write_db.user,并向read_db.user写入数据

mysql> create database read_db;
Query OK, 1 row affected (0.14 sec)

mysql> create database write_db;
Query OK, 1 row affected (0.01 sec)

mysql> create table read_db.user (
    -> id bigint(20) auto_increment not null comment '主键ID',
    -> name varchar(30) null default null comment '姓名',
    -> primary key (id)
    -> );
Query OK, 0 rows affected, 1 warning (0.29 sec)

mysql> 
mysql> insert into read_db.user (id, name) values
    -> (1, 'read_name1'),
    -> (2, 'read_name2'),
    -> (3, 'read_name3'),
    -> (4, 'read_name4'),
    -> (5, 'read_name5');
Query OK, 5 rows affected (0.16 sec)
Records: 5  Duplicates: 0  Warnings: 0

mysql> 
mysql> create table write_db.user (
    -> id bigint(20) auto_increment not null comment '主键ID',
    -> name varchar(30) null default null comment '姓名',
    -> primary key (id)
    -> );
Query OK, 0 rows affected, 1 warning (0.04 sec)

mysql> 

2.2 通过Dynamic-Datasource实现多JDBC数据源

2.2.1 pom.xml依赖

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.31</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.15</version>
        </dependency>

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

可以看到自动添加了spring-boot-starter-jdbc、spring-boot-starter-aop依赖
dynamic-datasource-spring-boot-starter

2.2.2 application.properties配置

通过dynamic方式,指定了datasource1和datasource2两个DataSource

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# 设置默认的数据源。默认是master
spring.datasource.dynamic.primary=datasource1
# 严格匹配数据源, 默认false。true表示未匹配到指定数据源时抛异常, false表示使用默认数据源
spring.datasource.dynamic.strict=true

# 用于读的数据库
spring.datasource.dynamic.datasource.datasource1.url=jdbc:mysql://192.168.28.12:3306/read_db
spring.datasource.dynamic.datasource.datasource1.username=root
spring.datasource.dynamic.datasource.datasource1.password=Root_123
spring.datasource.dynamic.datasource.datasource1.driver-class-name=com.mysql.cj.jdbc.Driver

# 用于写的数据库
spring.datasource.dynamic.datasource.datasource2.url=jdbc:mysql://192.168.28.12:3306/write_db
spring.datasource.dynamic.datasource.datasource2.username=root
spring.datasource.dynamic.datasource.datasource2.password=Root_123
spring.datasource.dynamic.datasource.datasource2.driver-class-name=com.mysql.cj.jdbc.Driver

2.2.3 使用@DS注解选择DataSource

可以在Service的类或方法上(优先级更高)使用@DS注解选择DataSource

2.2.4 使用@Transactional + @DSTransactional实现事务

可以在Service的类或方法上(优先级更高)使用@Transactional实现事务。对异常的捕获后,通过TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()进行手动回滚

@Transactional注解会让@DS的多数据源切换失效,虽然可以通过@Transactional(propagation = Propagation.REQUIRES_NEW)开启一个内部子事务来使@DS切换数据源,但是大部分场景不适用

@DSTransactional能和@DS多数据源切换配合使用,@DSTransactional注解最好使用在方法上(类似好像不生效),@DSTransactional注解不能对异常进行捕获手动回滚,只能自动回滚然后抛出异常

2.3 动态数据源测试

2.3.1 创建User类

package com.hh.springboottest.myController;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
public class User {

    private Long id;
    private String name;

}

2.3.2 Mapper接口实现

package com.hh.springboottest.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hh.springboottest.myController.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {

}

2.3.3 Service实现

Service接口实现

package com.hh.springboottest.service;

import com.hh.springboottest.myController.User;

public interface UserService {

    public User getUser(Long id);

    public void saveMultiUser();
}

ServiceImpl实现类。说明如下:

  • @DS(“datasource1”)注解,让userMapper.selectById使用了datasource1数据源
  • @DS(“datasource2”)注解,让userMapper.insert使用了datasource2数据源
  • @Transactional注解,当userMapper.insert(user1)执行成功,userMapper.insert(user2)执行失败,会回滚userMapper.insert(user1)的操作
package com.hh.springboottest.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.hh.springboottest.mapper.UserMapper;
import com.hh.springboottest.myController.User;
import com.hh.springboottest.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;

    @DS("datasource1")
    @Override
    public User getUser(Long id) {
        return userMapper.selectById(id);
    }

    @Transactional
    @DS("datasource2")
    @Override
    public void saveMultiUser() {
        User user2 = new User(2L, "write_name2");
        userMapper.insert(user2);


        User user3 = new User(3L, "write_name3");
        userMapper.insert(user3);
    }

}

2.3.4 测试

package com.hh.springboottest;

import com.hh.springboottest.myController.User;
import com.hh.springboottest.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
public class MyApplicationTest {

    @Autowired
    UserService userService;

    @Test
    public void dynamicDataSourceTest() {

        User user = userService.getUser(1L);
        log.info("获取到的用户为:{}", user);

        userService.saveMultiUser();
    }

}

运行程序,结果如下:

2022-11-24 09:16:15.733  INFO 33760 --- [           main] com.hh.springboottest.MyApplicationTest  : 获取到的用户为:User(id=1, name=read_name1)
2022-11-24 09:16:15.857  INFO 33760 --- [ionShutdownHook] c.b.d.d.DynamicRoutingDataSource         : dynamic-datasource start closing ....
2022-11-24 09:16:15.861  INFO 33760 --- [ionShutdownHook] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} closing ...
2022-11-24 09:16:15.866  INFO 33760 --- [ionShutdownHook] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} closed
2022-11-24 09:16:15.866  INFO 33760 --- [ionShutdownHook] com.alibaba.druid.pool.DruidDataSource   : {dataSource-2} closing ...
2022-11-24 09:16:15.867  INFO 33760 --- [ionShutdownHook] com.alibaba.druid.pool.DruidDataSource   : {dataSource-2} closed
2022-11-24 09:16:15.867  INFO 33760 --- [ionShutdownHook] c.b.d.d.DynamicRoutingDataSource         : dynamic-datasource all closed success,bye

同时查看write_db.user表,数据如下:

mysql> select * from write_db.user;
+----+--------------+
| id | name         |
+----+--------------+
|  1 | write_name1  |
|  2 | write_name2  |
|  3 | write_name3  |
+----+--------------+
3 rows in set (0.11 sec)

mysql> 

2.4 removeDataSource删除DataSource

通过DynamicRoutingDataSource的removeDataSource方法删除DataSource

package com.hh.springboottest.service.impl;

import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.sql.DataSource;

@Service
public class DataSourceServiceImpl implements DataSourceService {

    @Autowired
    DataSource dataSource;

    public void removeDataSource(String dataSourceName) {
        DynamicRoutingDataSource dynamicRoutingDataSource =
                (DynamicRoutingDataSource) dataSource;
        dynamicRoutingDataSource.removeDataSource(dataSourceName);
    }

}
### 动态数据源配置概述 在 Spring Boot 2.6.9 中实现动态数据源配置,可以通过自定义 `DataSource` 配置类以及结合 AOP 或者其他机制来切换不同的数据库连接。虽然 Spring Boot 的自动配置文件路径发生了变化[^1],但这并不会影响手动配置数据源方式。 以下是基于 Spring Boot 2.6.9 实现动态数据源的一个完整示例: --- ### 配置步骤与代码示例 #### 1. 添加依赖 确保项目中的 `pom.xml` 文件包含必要的 JDBC 和数据库驱动依赖项: ```xml <dependencies> <!-- Spring Boot Starter Data JPA --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- MySQL Driver (或其他数据库驱动) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- HikariCP Connection Pool --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </dependency> </dependencies> ``` #### 2. 数据源配置类 创建一个用于管理数据源的配置类,并通过上下文存储当前使用的数据源名称。 ```java import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSourceName(); // 获取线程本地变量中的数据源名称 } } ``` #### 3. 上下文持有器 为了在线程间传递数据源信息,可以使用 ThreadLocal 存储当前的数据源键值。 ```java public class DataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static void setDataSourceName(String dataSourceName) { contextHolder.set(dataSourceName); } public static String getDataSourceName() { return contextHolder.get(); } public static void clearDataSourceName() { contextHolder.remove(); } } ``` #### 4. 主配置类 注册所有的数据源并将其注入到 `DynamicDataSource` 中。 ```java import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration public class DataSourceConfig { @Bean(name = "dataSourceOne") public DataSource primaryDataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db_one"); dataSource.setUsername("root"); dataSource.setPassword("password"); return dataSource; } @Bean(name = "dataSourceTwo") public DataSource secondaryDataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db_two"); dataSource.setUsername("root"); dataSource.setPassword("password"); return dataSource; } @Bean public DataSource dynamicDataSource( @Qualifier("dataSourceOne") DataSource one, @Qualifier("dataSourceTwo") DataSource two) { Map<Object, Object> dataSourcesMap = new HashMap<>(); dataSourcesMap.put("db_one", one); dataSourcesMap.put("db_two", two); DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setDefaultTargetDataSource(one); // 设置默认数据源 dynamicDataSource.setTargetDataSources(dataSourcesMap); dynamicDataSource.afterPropertiesSet(); return dynamicDataSource; } } ``` #### 5. 切换逻辑 可以在服务层或者拦截器中设置当前要使用的数据源。 ```java @Service public class MyService { @Autowired private UserRepository userRepository; public List<User> getUsersFromDbOne() { DataSourceContextHolder.setDataSourceName("db_one"); // 切换至 db_one try { return userRepository.findAll(); } finally { DataSourceContextHolder.clearDataSourceName(); // 清理上下文 } } public List<User> getUsersFromDbTwo() { DataSourceContextHolder.setDataSourceName("db_two"); // 切换至 db_two try { return userRepository.findAll(); } finally { DataSourceContextHolder.clearDataSourceName(); // 清理上下文 } } } ``` --- ### 总结 上述方法展示了如何在 Spring Boot 2.6.9 中实现动态数据源切换的功能。尽管 Spring Boot 3.x 对自动配置进行了调整,但在较低版本(如 2.6.9)中仍然适用传统的数据源配置方式。此外,Spring Cloud 版本的选择也需注意兼容性问题[^2][^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值