Springboot基于注解实现方法级别多数据源切换
一、分析
- 为什么需要数据源切换
- 基于功能的不同将一个系统拆封成多个微服务,每个微服务对应一个数据库
- 客服、运营人员对整个系统操作需要一个后台管理平台,后台管理平台涉及多个微服务的数据管理,就需要操作多个数据库
- 后台操作时可能需要查询用户服务,从用户数据库获取用户信息;同时需要根据用户信息,查询订单服务,从订单数据库获取用户订单信息,就需要从用户数据库切换到订单数据库。
- 实现思路
- 每个方法上通过注解指定要连接的数据库,进入方法,就切换到指定的数据库(本文基于这中方式实现)
- 基于包实现数据库切换,指定每个包下需要使用的数据库调用这个包下任一类的方法时,都切换到包对应的数据库
二、代码实现
-
nacos或项目中配置文件中定义多数据源配置,通用配置
-
配置多数据源和通用配置
# 数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: # 主库数据源 master: url: jdbc:mysql://xxx:3306/master库名?autoReconnect=true&useCompression=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai username: super_user password: 123456 # message库 message: # 从数据源开关/默认关闭 enabled: true url: jdbc:mysql://xxx:3306/message库名?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&useTimezone=true&serverTimezone=Asia/Shanghai username: super_admin password: 123456 jackson: time-zone: GMT+8 # user库 user: # 从数据源开关/默认关闭 enabled: true url: jdbc:mysql://xxx:3306/user库名?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&useTimezone=true&serverTimezone=Asia/Shanghai username: super_admin password: 123456 jackson: time-zone: GMT+8 # 初始连接数 initialSize: 5 # 最小连接池数量 minIdle: 10 # 最大连接池数量 maxActive: 20 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 maxEvictableIdleTimeMillis: 900000 # 配置检测连接是否有效 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true # 设置白名单,不填则允许所有访问 allow: url-pattern: /druid/* # 控制台管理用户名和密码 login-username: login-password: filter: stat: enabled: true # 慢SQL记录 log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true
-
-
数据库通用属性配置类
-
配置数据库连接的通用配置,不管是哪个数据源都是用这些通用的配置,使用@Value注解给属性注入值
-
提供dataSource(DruidDataSource datasource)方法,传入的datasource对象设置通用属性后返回
@Configuration public class DruidProperties { @Value("${spring.datasource.druid.initialSize}") private int initialSize; @Value("${spring.datasource.druid.minIdle}") private int minIdle; @Value("${spring.datasource.druid.maxActive}") private int maxActive; @Value("${spring.datasource.druid.maxWait}") private int maxWait; @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") private int timeBetweenEvictionRunsMillis; @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") private int minEvictableIdleTimeMillis; @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") private int maxEvictableIdleTimeMillis; @Value("${spring.datasource.druid.validationQuery}") private String validationQuery; @Value("${spring.datasource.druid.testWhileIdle}") private boolean testWhileIdle; @Value("${spring.datasource.druid.testOnBorrow}") private boolean testOnBorrow; @Value("${spring.datasource.druid.testOnReturn}") private boolean testOnReturn; public DruidDataSource dataSource(DruidDataSource datasource) { /** 配置初始化大小、最小、最大 */ datasource.setInitialSize(initialSize); datasource.setMaxActive(maxActive); datasource.setMinIdle(minIdle); /** 配置获取连接等待超时的时间 */ datasource.setMaxWait(maxWait); /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */ datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); /** * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 */ datasource.setValidationQuery(validationQuery); /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */ datasource.setTestWhileIdle(testWhileIdle); /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ datasource.setTestOnBorrow(testOnBorrow); /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ datasource.setTestOnReturn(testOnReturn); return datasource; } }
-
-
多数据源配置类
-
注解说明:
- @Configuration:放在类上,表示一个配置类
- @Bean:在方法上,①将方法生成的对象添加到spring容器中、②给方法中的参数注入实例对象
- @ConfigurationProperties(“spring.datasource.druid.master”):作用在方法上,取配置文件中spring.datasource.druid.master前缀的配置,赋值给方法返回对象DataSource对象的属性。
- @ConditionalOnProperty:如果配置文件中的属性和值和该注解中指定的属性和值一致则创建对象
-
bean说明
- DataSource:构建不同的数据源对象,添加到spring容器中
- DynamicDataSource:构建动态数据源类,将所有数据源名称和Datasource对象添加到map中,通过构造方法将默认数据源和map添加到动态数据源类中,并将动态数据源类添加到spring容器中
@Configuration public class DruidConfig { @Bean @ConfigurationProperties("spring.datasource.druid.master") public DataSource masterDataSource(DruidProperties druidProperties) { DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); } /** * message数据库 * * @param druidProperties * @return */ @Bean @ConfigurationProperties("spring.datasource.druid.message") @ConditionalOnProperty(prefix = "spring.datasource.druid.message", name = "enabled", havingValue = "true") public DataSource messageDataSource(DruidProperties druidProperties) { DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); } /** * user数据库 * * @param druidProperties * @return */ @Bean @ConfigurationProperties("spring.datasource.druid.user") @ConditionalOnProperty(prefix = "spring.datasource.druid.user", name = "enabled", havingValue = "true") public DataSource userDataSource(DruidProperties druidProperties) { DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); } @Bean @Primary public DynamicDataSource dataSource(DataSource masterDataSource, DataSource messageDataSource, DataSource userDataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); //DataSourceType见下文多数据源枚举类 targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); targetDataSources.put(DataSourceType.MESSAGE.name(), messageDataSource); targetDataSources.put(DataSourceType.USER.name(), userDataSource); return new DynamicDataSource(masterDataSource, targetDataSources); } }
-
多数据源枚举类
/** * 数据源枚举类 * * @author csgosp */ public enum DataSourceType { /** * 主库 */ MASTER, /** * message库 */ MESSAGE, /** * user库 */ USER }
-
-
定义自定义注解@interface DataSource
-
自定义注解中一个属性,就是数据源名称,默认值是MASTER数据源名称
-
需要指定数据源的方法上添加该注解,注解中指定数据源名称,方法中就可以使用指定的数据源
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface DataSource { /** * 切换数据源名称 */ public DataSourceType value() default DataSourceType.MASTER; }
-
-
定义切面类,处理方法上的@DataSource(value = DataSourceType.MESSAGE)注解
-
方法上添加了注解,方法中怎么才能使用到注解中指定的数据源呢?
- 通过切点表达式,指定拦截加了@DataSource注解的方法
- 定义一个环绕通知,在目标方法执行之前先获取方法上的@DataSource注解,如果注解不为null,获取注解中的value属性值(即指定数据源的名称)
- 调用DynamicDataSourceContextHolder的setDataSourceType方法将数据源名称添加到ThreadLocal中(后面详解),可以暂且理解添加到一个容器中
- 执行方法,执行方法的时候,方法中需要用到数据源,这时就会调用上文中提到的DynamicDataSource重写的determineCurrentLookupKey方法查询数据源名称。
- DynamicDataSource类determineCurrentLookupKey方法中调用DynamicDataSourceContextHolder对象的getDataSourceType方法从ThreadLocal中查询的需要的数据源名称,根据数据源名称构建数据库连接,然后执行方法中需要用到数据库的业务逻辑代码
- 目标方法执行完成之后,调用clearDataSourceType方法,从DynamicDataSourceContextHolder对象的属性ThreadLocal中清除数据源
@Aspect @Order(1) @Component public class DataSourceAspect { protected Logger logger = LoggerFactory.getLogger(getClass()); @Pointcut("@annotation(com.ms.common.util.annotation.DataSource)" + "|| @within(com.ms.common.util.annotation.DataSource)") public void dsPointCut() { } @Around("dsPointCut()") 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(); } } /** * 获取需要切换的数据源 */ public DataSource getDataSource(ProceedingJoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); Class<? extends Object> targetClass = point.getTarget().getClass(); DataSource targetDataSource = targetClass.getAnnotation(DataSource.class); if (StringUtils.isNotNull(targetDataSource)) { return targetDataSource; } else { Method method = signature.getMethod(); return method.getAnnotation(DataSource.class); } } }
-
-
DynamicDataSourceContextHolder类
-
上文中已经说到,数据源名称的添加,获取,删除都是通过这个类的不同方法来实现的
-
数据源名称添加到哪里?从哪里获取获取,从哪里删除?
- 每个线程有自己的ThreadLocal,将数据源名称添加到ThreadLocal中是可行的
-
A方法通过@DataSource注解指定A数据源名称,B方法通过@DataSource注解指定B数据源名称
- A方法中调用B方法,B数据源名称添加到ThreadLocal中会覆盖掉A数据源名称,但是B方法执行完成,会清除掉ThreadLocal中的B数据源名称,这时回到A方法继续执行,A方法从ThreadLocal中获取数据源名称,A数据源名称因为调用B方法时被B数据源名称覆盖,导致A方法找不到A数据源名称而执行失败。
-
如何解决目标方法执行完成后,原方法找不到数据源问题?
- ThreadLocal中使用栈这种数据结构
- DynamicDataSource添加到spring容器中时,通过构造方法指定了默认数据源名称,会现将默认数据源名称添加到栈底。
- 调用A方法的时候会将A数据源名称压栈
- A方法中调用B方法的时候,会将B数据源名称压栈
- B方法执行完成之后,B数据源名称从栈中弹出
- 回到A方法继续执行,A方法从栈中获取到的仍然是A数据源名称,能够继续连接A数据源,处理业务
- A方法执行完成之后,A数据源名称从栈中弹出,整个服务开始使用默认数据源MASTER
public class DynamicDataSourceContextHolder { public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); /** * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 */ private static final ThreadLocal<Stack<String>> CONTEXT_HOLDER = new ThreadLocal<>(); /** * 设置数据源的变量 */ public static synchronized void setDataSourceType(String dsType) { Stack<String> stack = CONTEXT_HOLDER.get() == null ? new Stack<>() : CONTEXT_HOLDER.get(); stack.push(dsType); log.info("切换到{}数据源", dsType); CONTEXT_HOLDER.set(stack); } /** * 获得数据源的变量 */ public static synchronized String getDataSourceType() { Stack<String> stack = CONTEXT_HOLDER.get(); if (stack == null || stack.empty()) { return null; } return stack.peek(); } /** * 清空数据源变量 */ public static void clearDataSourceType() { Stack<String> stack = CONTEXT_HOLDER.get(); String pop = stack.pop(); log.info("释放{}数据源", pop); } }
-
-
动态数据源实现类
-
动态数据源类,继承AbstractRoutingDataSource类,重写determineCurrentLookupKey方法;
-
连接数据库的时候会根据determineCurrentLookupKey方法返回的数据源名称来创建指定的数据源连接
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(); } }
-