动态数据源
SpringBoot 配置多数据源并动态切换
Spring Boot 中的多数据源配置方案
SpringBoot 多数据源配置/连接两个数据库
介绍
在实际开发中,往往会出现一个服务连接多个数据库的需求,这时候就需要在项目中进行灵活切换数据源来完成多个数据库操作。多数据源可以理解为多数据库,甚至可以是多个不同类型的数据库,比如一个是MySql,一个是Oracle。随着项目的扩大,有时需要数据库的拆分或者引入另一个数据库,这时就需要配置多个数据源。
实现思路
DataSource
是和线程绑定的,动态数据源的配置主要是通过继承AbstractRoutingDataSource
类实现的,实现在AbstractRoutingDataSource
类中的 protected Object determineCurrentLookupKey()
方法来获取数据源,所以我们需要先创建一个多线程 线程数据隔离的类来存放DataSource
,然后在determineCurrentLookupKey()
方法中通过这个类获取当前线程的DataSource
,在AbstractRoutingDataSource
类中,DataSource
是通过Key-value
的方式保存的,我们可以通过ThreadLocal
来保存Key
,从而实现数据源的动态切换。
引入mysql和jdbcTemplate依赖
| <dependency> |
| <groupId>mysql</groupId> |
| <artifactId>mysql-connector-java</artifactId> |
| </dependency> |
| <dependency> |
| <groupId>org.springframework.boot</groupId> |
| <artifactId>spring-boot-starter-jdbc</artifactId> |
| </dependency> |
| <dependency> |
| <groupId>org.springframework</groupId> |
| <artifactId>spring-aspects</artifactId> |
| </dependency> |
| <dependency> |
| <groupId>com.alibaba</groupId> |
| <artifactId>druid</artifactId> |
| <version>1.0.31</version> |
| </dependency> |
数据源配置
| spring.datasource.driver-class-name=com.mysql.jdbc.Driver |
| spring.datasource.url=jdbc:mysql://localhost:3306/db1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 |
| spring.datasource.username=root |
| spring.datasource.password=root |
| |
| spring.datasource.db2.driver-class-name=com.mysql.jdbc.Driver |
| spring.datasource.db2.url=jdbc:mysql://localhost:3306/db2?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 |
| spring.datasource.db2.username=root |
| spring.datasource.db2.password=root |
| |
| mybatis.type-aliases-package=com.example.demo02.pojo.domain |
| mybatis.mapper-locations=classpath:mapper/*.xml |
| mybatis.configuration.map-underscore-to-camel-case=true |
创建数据源枚举类
| @Getter |
| public enum DataSourceEnum { |
| |
| DEFAULT_DATASOURCE("defaultDataSource"), |
| DB2_DATASOURCE("db2DataSource"); |
| |
| String dataSourName; |
| |
| DataSourceEnum(String dataSourName) { |
| this.dataSourName = dataSourName; |
| } |
| } |
数据源切换处理
| public class DynamicDataSourceContextHolder { |
| |
| |
| |
| |
| |
| private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); |
| |
| |
| |
| |
| |
| |
| public static void setDataSourceType(String dataSourceType) { |
| System.out.printf("切换到{%s}数据源", dataSourceType); |
| CONTEXT_HOLDER.set(dataSourceType); |
| } |
| |
| |
| |
| |
| |
| |
| public static String getDataSourceType() { |
| return CONTEXT_HOLDER.get(); |
| } |
| |
| |
| |
| |
| public static void clearDataSourceType() { |
| CONTEXT_HOLDER.remove(); |
| } |
| } |
继承AbstractRoutingDataSource
| |
| |
| |
| |
| |
| |
| 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(); |
| } |
| } |
注入数据源
| import org.springframework.beans.factory.annotation.Qualifier; |
| import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; |
| 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; |
| |
| |
| |
| |
| @Configuration |
| public class DataSourceConfig { |
| |
| |
| |
| |
| |
| |
| @Bean("defaultDataSource") |
| @Primary |
| @ConfigurationProperties(prefix = "spring.datasource") |
| public DataSource defaultDataSource() { |
| return DataSourceBuilder.create().build(); |
| } |
| |
| |
| |
| |
| |
| |
| @Bean("db2DataSource") |
| @ConfigurationProperties(prefix = "spring.datasource.db2") |
| public DataSource db2DataSource() { |
| return DataSourceBuilder.create().build(); |
| } |
| |
| @Bean(name = "dataSource") |
| @Primary |
| public DynamicDataSource dataSource(@Qualifier("defaultDataSource") DataSource defaultDataSource, |
| @Qualifier("db2DataSource") DataSource db2DataSource) { |
| Map<Object, Object> targetDataSources = new HashMap<>(); |
| targetDataSources.put(DataSourceEnum.DEFAULT_DATASOURCE.getDataSourName(), defaultDataSource); |
| targetDataSources.put(DataSourceEnum.DB2_DATASOURCE.getDataSourName(), db2DataSource); |
| return new DynamicDataSource(defaultDataSource, targetDataSources); |
| } |
| |
| |
| |
| |
| |
| } |
自定义多数据源切换注解
| @Target({ElementType.TYPE, ElementType.METHOD}) |
| @Retention(RetentionPolicy.RUNTIME) |
| public @interface DataSource { |
| |
| DataSourceEnum value() default DataSourceEnum.DEFAULT_DATASOURCE; |
| } |
AOP拦截类的实现
通过AOP在执行sql语句前拦截,并切换到自定义注解指定的数据源上。有一点需要注意,自定义数据源注解与 @Transaction
注解同一个方法时会先执行 @Transaction
,即获取数据源在切换数据源之前,所以会导致自定义注解失效,因此需要使用 @Order
(@Order的value越小,就越先执行),保证该AOP在 @Transactional
之前执行。
| import org.aspectj.lang.JoinPoint; |
| import org.aspectj.lang.annotation.After; |
| import org.aspectj.lang.annotation.Aspect; |
| import org.aspectj.lang.annotation.Before; |
| import org.aspectj.lang.annotation.Pointcut; |
| import org.springframework.core.annotation.Order; |
| import org.springframework.stereotype.Component; |
| |
| |
| |
| |
| |
| |
| @Aspect |
| @Component |
| @Order(-1) |
| public class DataSourceAspect { |
| |
| |
| |
| |
| @Pointcut("execution(* com.example.demo02.mapper..*.*(..))") |
| public void dsPointCut() { |
| |
| } |
| |
| @Before("dsPointCut()") |
| public void changeDataSource(JoinPoint joinPoint) { |
| boolean datasource = false; |
| DataSource targetDataSource = null; |
| for (Class clazz : joinPoint.getTarget().getClass().getInterfaces()) { |
| datasource = clazz.isAnnotationPresent(DataSource.class); |
| if (datasource) { |
| targetDataSource = (DataSource) clazz.getAnnotation(DataSource.class); |
| break; |
| } |
| } |
| if (datasource) { |
| String dataSource = targetDataSource.value().getDataSourName(); |
| |
| if (!DynamicDataSourceContextHolder.containsDataSource(dataSource)) { |
| log.error("datasource[{}] does not exist, will use default data source > {}" |
| , targetDataSource.value(), joinPoint.getSignature()); |
| } else { |
| log.debug("Use DataSource: {} > {}", dataSource, joinPoint.getSignature()); |
| DynamicDataSourceContextHolder.setDataSourceType(dataSource); |
| } |
| } else { |
| |
| DynamicDataSourceContextHolder.setDataSourceType(DataSourceEnum.DEFAULT_DATASOURCE.getDataSourName()); |
| } |
| } |
| |
| @After("@annotation(targetDataSource)") |
| public void restoreDataSource(JoinPoint joinPoint, DataSource targetDataSource) { |
| log.debug("Revert DataSource: {} > {}", targetDataSource.value(), joinPoint.getSignature()); |
| DynamicDataSourceContextHolder.clearDataSourceType(); |
| } |
| } |
启动类
需要在启动类的 @SpringBootApplication
注解中移除DataSource自动配置类,否则会默认自动配置,而不会使用我们自定义的DataSource,并且启动会有循环依赖的错误。
| @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) |
| @MapperScan(basePackages = "com.example.demo02.mapper") |
| public class Demo02Application { |
| |
| public static void main(String[] args) { |
| SpringApplication.run(Demo02Application.class, args); |
| } |
| |
| } |
mybatis配置
| import org.apache.ibatis.session.SqlSessionFactory; |
| import org.mybatis.spring.SqlSessionFactoryBean; |
| import org.mybatis.spring.SqlSessionTemplate; |
| import org.mybatis.spring.annotation.MapperScan; |
| import org.springframework.beans.factory.annotation.Qualifier; |
| import org.springframework.beans.factory.annotation.Value; |
| import org.springframework.context.annotation.Bean; |
| import org.springframework.context.annotation.DependsOn; |
| import org.springframework.core.io.support.PathMatchingResourcePatternResolver; |
| import org.springframework.jdbc.datasource.DataSourceTransactionManager; |
| import org.springframework.stereotype.Component; |
| |
| import javax.sql.DataSource; |
| |
| |
| |
| |
| @Component |
| @DependsOn("dataSource") |
| @MapperScan(basePackages = {"com.example.demo02.mapper"}, sqlSessionFactoryRef = "sqlSessionFactory") |
| public class MybatisConfig { |
| |
| @Value("${mybatis.mapper-locations}") |
| private String mapperLocation; |
| |
| @Bean("sqlSessionFactory") |
| public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception { |
| SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); |
| sqlSessionFactoryBean.setDataSource(dataSource); |
| sqlSessionFactoryBean.setMapperLocations( |
| new PathMatchingResourcePatternResolver().getResources(mapperLocation)); |
| |
| return sqlSessionFactoryBean.getObject(); |
| } |
| |
| @Bean("sqlSessionTemplate") |
| public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) { |
| return new SqlSessionTemplate(sqlSessionFactory); |
| } |
| |
| @Bean("transactionManager") |
| public DataSourceTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) { |
| return new DataSourceTransactionManager(dataSource); |
| } |
| } |
🌰测试数据源切换
| import org.apache.ibatis.annotations.Param; |
| import org.springframework.stereotype.Repository; |
| |
| |
| |
| |
| |
| @Repository |
| public interface StudentMapper { |
| |
| |
| |
| |
| |
| |
| String getNameById(@Param("id") int id); |
| } |
| import org.springframework.beans.factory.annotation.Autowired; |
| import org.springframework.stereotype.Service; |
| |
| |
| |
| |
| |
| @Service |
| public class StudentServiceImpl implements StudentService { |
| |
| @Autowired |
| private StudentMapper studentMapper; |
| |
| @Autowired |
| private StudentDB2Mapper studentDB2Mapper; |
| |
| @Override |
| public String getNameById(int id) { |
| return studentMapper.getNameById(id); |
| } |
| |
| @Override |
| public String getUserNameByIdDB2(int id) { |
| |
| return studentDB2Mapper.getUserNameByIdDB2(id); |
| } |
| } |
| import org.springframework.beans.factory.annotation.Autowired; |
| import org.springframework.web.bind.annotation.GetMapping; |
| import org.springframework.web.bind.annotation.PathVariable; |
| import org.springframework.web.bind.annotation.RequestMapping; |
| import org.springframework.web.bind.annotation.RestController; |
| |
| |
| |
| |
| |
| @RestController |
| @RequestMapping("/student") |
| public class StudentController { |
| |
| @Autowired |
| private StudentService studentService; |
| |
| |
| @GetMapping("/get/{id}") |
| public RestResult getNameById(@PathVariable("id") int id) { |
| String name = studentService.getNameById(id); |
| |
| return RestResult.successResult(name); |
| } |
| |
| @GetMapping("/get-db2/{id}") |
| public RestResult getUserNameByIdDB2(@PathVariable("id") int id) { |
| String name = studentService.getUserNameByIdDB2(id); |
| return RestResult.successResult(name); |
| } |
| } |

遇到问题
Springboot报错:Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required
配置数据源多配置@Primary导致找不到主数据源而不能创建mybatis相关bean
| Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'studentMapper' defined in file [F:\code\demo02\target\classes\com\example\demo02\mapper\StudentMapper.class]: Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步