多数据源切换(一)
业务背景
当前SpringBoot应用(SpringBoot + Mybatis)需要连接使用两个数据库源,其中一个是master数据源,另一个是external数据源,在应用中需要实现两个数据源的切换。
功能实现方案
一种方案是在指定文件夹下使用指定数据源:使用Mybatis的 SqlSessionFactory 注入指定数据源,这样在使用指定文件夹下面的Mapper.xml时会切换为指定数据源,就完成了多数据源的切换。
另一种方案是自定义注解:根据自定义注解上的value使用对应的数据源,使用AOP动态的切换当前使用的数据源。
本文介绍实现的就是第二种方案:AOP + 自定义注解实现数据源动态切换
功能实现思路
首先了解Spring对于数据库动态切换提供了哪些支持,通过搜索很容易了解到Spring提供AbstractRoutingDataSource类来实现数据源的动态切换。下面是官方对于AbstractRoutingDataSource的说明:
根据各种数据源的lookup key调用指定数据源,look key通常(但不是必须)通过一些线程绑定的事务上下文来确定。
这样就可以根据实现AbstractRoutingDataSource这个虚拟类:设置当前线程中的look key来实现切换数据源。
功能实现
创建数据库及表结构
创建两个数据库 master_db 和 external_db ,分别在两个数据库中创建表t_master_user和t_external_user,建表语句如下:
CREATE TABLE `t_master_user` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(50) DEFAULT NULL COMMENT '姓名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
CREATE TABLE `t_external_user` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(50) DEFAULT NULL COMMENT '姓名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
切换数据源代码
-
设置当前线程的数据源上下文,动态切换数据源
package com.zhjw.config.db; import lombok.extern.slf4j.Slf4j; import java.util.Stack; /** * Created by zhjw on 2023/3/6 * <p> * 设置当前线程的数据源上下文,动态切换数据源 * <p> * {@link DynamicRoutingDataSource} 实现Spring的AbstractRoutingDataSource接口,设置当前线程使用的数据源 * * @author zhjw * @date 2023/3/6 */ @Slf4j public class DataSourceContextHolderWrapper { /** * 默认数据源 */ public static final String DEFAULT_DS = "master"; private static final ThreadLocal<Stack<String>> contextHolder = new ThreadLocal<>(); /** * 设置数据源名 * * @param dbType DB名 */ public static void setDB(String dbType) { if (contextHolder.get() == null) { contextHolder.set(new Stack<>()); } contextHolder.get().push(dbType); log.debug("Set db to {}", dbType); } /** * 获取数据源名 * * @return DB名 */ public static String getDB() { if (contextHolder.get() != null && !contextHolder.get().empty()) { log.debug("Get db as {}", contextHolder.get().peek()); return contextHolder.get().peek(); } return DEFAULT_DS; } /** * 清除数据源名 */ public static void clearDB() { if (contextHolder.get() != null && !contextHolder.get().empty()) { log.debug("Pop db {}", contextHolder.get().pop()); } if (contextHolder.get() != null && contextHolder.get().empty()) { contextHolder.remove(); } } }
- 实现Spring的AbstractRoutingDataSource接口,设置当前线程使用的数据源
package com.zhjw.config.db; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * Created by zhjw on 2023/3/6 * <p> * 实现Spring的AbstractRoutingDataSource接口,设置当前线程使用的数据源 * </p> * * @author zhjw * @date 2023/3/6 */ public class DynamicRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolderWrapper.getDB(); } }
-
读取多数据源配置并封装为Bean
package com.zhjw.config.db; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.pool.DruidDataSourceFactory; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; 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 org.springframework.context.annotation.PropertySource; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; import java.util.Properties; /** * Created by zhjw on 2023/3/6 * <p> * 多数据源配置类 * </p> * * @author zhjw * @date 2023/3/6 */ @Slf4j @Configuration @PropertySource(value = "classpath:application.properties") public class MultipleDataSourceConfig { /** * 读取properties配的数据源信息 * * @return */ @Bean("dataSourcePropertiesMap") @ConfigurationProperties(prefix = "spring.datasource") public Map<String, Properties> dataSourcePropertiesMap() { return new HashMap(8); } /** * 读取properties配的数据源信息,装配为Bean * * @param dataSourcePropertiesMap 读取properties配的数据源信息 */ @Bean("dataSourceBeanMap") public Map dataSourceBeanMap(@Qualifier("dataSourcePropertiesMap") Map<String, Properties> dataSourcePropertiesMap) throws Exception { Map dataSourceBeanMap = new HashMap(8); if (CollectionUtils.isEmpty(dataSourcePropertiesMap)) { return dataSourceBeanMap; } //Properties装配Bean for (String key : dataSourcePropertiesMap.keySet()) { Properties properties = dataSourcePropertiesMap.get(key); DruidDataSource druidDataSource = new DruidDataSource(); DruidDataSourceFactory.config(druidDataSource,properties); dataSourceBeanMap.put(key, druidDataSource); } return dataSourceBeanMap; } @Resource private Map dataSourceBeanMap; /** * 动态数据源: 通过AOP在不同数据源之间动态切换 * * @return 数据源 */ @Primary @Bean(name = "dynamicRoutingDataSource") public DataSource dynamicRoutingDataSource() { //实现Spring的AbstractRoutingDataSource接口,设置当前线程使用的数据源 DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource(); // 默认数据源:不指定使用数据源,则使用默认数据源 if (dataSourceBeanMap.get(DataSourceContextHolderWrapper.DEFAULT_DS) == null) { throw new IllegalArgumentException("Multiple datasource must define default master datasource!"); } dynamicRoutingDataSource.setDefaultTargetDataSource(dataSourceBeanMap.get(DataSourceContextHolderWrapper.DEFAULT_DS)); // 配置多数据源 dynamicRoutingDataSource.setTargetDataSources(dataSourceBeanMap); return dynamicRoutingDataSource; } /** * 配置@Transactional注解事务 * * @return 平台事务管理器 */ @Bean public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dynamicRoutingDataSource()); } }
-
自定义切换数据库注解@DS
package com.zhjw.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Created by zhjw on 2023/3/6 * <p> * 切换数据库注解 * </p> * * @author zhjw * @date 2023/3/6 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface DS { /** * 数据库名称 */ String value() default "master"; }
-
实现@DS注解切面(这里添加**@Order**注解为下一节内容)
package com.zhjw.aspect; import com.zhjw.annotation.DS; import com.zhjw.config.db.DataSourceContextHolderWrapper; import lombok.extern.slf4j.Slf4j; 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.aspectj.lang.reflect.MethodSignature; import org.springframework.core.annotation.Order; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * Created by zhjw on 2023/3/6 * * 实现@DS注解切面 * * <p> * 提升该切面实例化的优先级是为了不影响@Transactional注解生效: * 不指定优先级的话{@link AbstractRoutingDataSource#determineTargetDataSource()}方法执行在本切面之前 * 在{@link AbstractRoutingDataSource#determineTargetDataSource()}方法的194行 * 会导致该事务一直使用默认的datasource,而不会切换 * </p> * * @author zhjw * @date 2023/3/6 */ @Component @Aspect @Slf4j //@Order(100) public class DynamicDataSourceAspect { /** * 加载数据源的切入点 */ @Pointcut("@annotation(com.zhjw.annotation.DS) || @within(com.zhjw.annotation.DS)") public void pointCut() { } /** * 加载当前线程使用数据源 * * @param point */ @Before("pointCut()") public void beforeSwitchDS(JoinPoint point) { //获得当前访问的class Class<?> clazz = point.getTarget().getClass(); //获得访问的方法名 String methodName = point.getSignature().getName(); //得到方法的参数的类型 Class[] argClass = ((MethodSignature) point.getSignature()).getParameterTypes(); String dataSource = DataSourceContextHolderWrapper.DEFAULT_DS; try { // 得到访问的方法对象 Method method = clazz.getMethod(methodName, argClass); // 就近原则:方法上注解大于类上注解 /** 判断类上是否存在@DS注解 **/ if (clazz.isAnnotationPresent(DS.class)) { DS annotation = clazz.getAnnotation(DS.class); // 取出注解中的数据源名 dataSource = annotation.value(); } /**判断方法上是否存在@DS注解 **/ if (method.isAnnotationPresent(DS.class)) { DS annotation = method.getAnnotation(DS.class); // 取出注解中的数据源名 dataSource = annotation.value(); } } catch (Exception e) { log.error("@DS AOP failed to dynamic data source ,cause by :{}", e); } // 切换数据源 DataSourceContextHolderWrapper.setDB(dataSource); } /** * 清除当前线程使用数据源 * * @param point */ @After("pointCut()") public void afterSwitchDS(JoinPoint point) { DataSourceContextHolderWrapper.clearDB(); } }
业务数据测试代码
-
application.properties配置内容如下:
#端口号 server.port=8080 spring.datasource.master.url=jdbc:mysql://127.0.0.1:3306/master_db?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai spring.datasource.master.username=root spring.datasource.master.password=123456 spring.datasource.external.url=jdbc:mysql://127.0.0.1:3306/external_db?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai spring.datasource.external.username=root spring.datasource.external.password=123456 #mybatis 配置 mybatis.mapper-locations=classpath:mybatis/*/*.xml mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
-
SpringBoot启动类添加==(exclude = DataSourceAutoConfiguration.class)==,去除自动配置信息
-
在需要切换数据源的ServiceImpl类 或者 方法上加上@DS自定义注解,以及对应的数据库信息
详细的业务测试代码已上传到我的Gitee上,如对您有帮助还请给项目一个Star,十分感谢!
调取localhost:8080//insert/union 接口
查看结果
切换数据源代码封装为boot-start的jar包
为了代码的复用性,将切换数据源代码封装为boot-start的jar包,其他项目使用时,直接引入jar包
<dependency>
<groupId>com.zhjw</groupId>
<artifactId>multiple-datasource-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
思考题?
假如在externalServiceImpl#insert方法上加上@Transactional 事务注解,则无法切换数据源,只会使用主数据源,如下图所示:
探究原因及解决方案在:
多数据源切换(二)