使用场景
项目中需要用到非固定数据源动态切换,每个月会定时从主库(master_dbname)备份数据库(备份库master_dbname_202005),界面需要单独的功能模块使得户手动根据年月动态切切换数据源。这里是在老的单一数据源项目中配置,使得切换数据源之后,相应的功能不变,也就是多个数据源共用相同的接口,不需要改以前老的后台代码(要改的话要重构不划算,这里只是一小模块需要切换数据源,其它模块都是用的单一默认数据库),这里边主要涉及到数据源的添加和切换,针对这种场景springboot也为我们提供了很贴心的支持,核心主要是AbstractRoutingDataSource类,建议花点点时间看看源码,这里就不介绍了;废话不多说,直接上代码。
动态数据源的配置
1. application.properties中主数据库(默认数据库)配置
# 主数据库配置
spring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.master.jdbc-url= jdbc:mysql://localhost:3306/master_dbname?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.datasource.master.username=
spring.datasource.master.password=
spring.datasource.master.type=com.zaxxer.hikari.HikariDataSource
########################## HikariCP begin #######################
## 指定必须保持连接的最小值
spring.datasource.master.hikari.minimum-idle= 5
## 一个连接idle状态的最大时长(毫秒),超时则被释放(retired),默认为600000毫秒(10分钟)
spring.datasource.master.hikari.idle-timeout= 900000
## 指定连接池最大的连接数,包括使用中的和空闲的连接 ,缺省值:10;推荐的公式:((core_count * 2) + effective_spindle_count)
spring.datasource.master.hikari.maximum-pool-size= 50
## 一个连接的生命时长(毫秒),超时而且没被使用则被释放(retired),默认1800000毫秒(30分钟),
## 建议设置比数据库超时时长少30秒,参考MySQL wait_timeout参数(show variables like '%timeout%';)
spring.datasource.master.hikari.max-lifetime= 1800000
## 等待连接池分配连接的最大时长(毫秒),默认30000毫秒(30秒),如果小于250毫秒,则被重置回30秒
## 如果在没有连接可用的情况下超过此时间,则将抛出SQLException
spring.datasource.master.hikari.connection-timeout= 30000
## 使用Hikari connection pool时,多少毫秒检测一次连接泄露,默认0
spring.datasource.master.hikari.leak-detection-threshold= 0
#指定Hikari connection pool是否注册JMX MBeans.
spring.datasource.master.hikari.register-mbeans= true
## 设定连接校验的超时时间,默认5000毫秒(5秒),如果小于250毫秒,则会被重置回5秒
spring.datasource.master.hikari.validation-timeout= 5000
## 指定校验连接合法性执行的sql语句
spring.datasource.master.hikari.connection-test-query= select 1
##################### HikariCP end ############################
#jpa
spring.jpa.hibernate.ddl-auto= none
spring.jpa.show-sql= true
spring.jpa.properties.hibernate.show_sql= true
spring.jpa.properties.hibernate.format_sql= false
spring.jpa.properties.hibernate.use_sql_comments= false
spring.jpa.open-in-view= false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.properties.hibernate.ejb.interceptor=com.jade.interceptor.HibernateEjbInterceptor
2. 针对动态数据源配置一个上下文 用来维护用户对数据源的切换和管理
package com.jade.component.dynamicdatasource.context;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import com.jade.component.dynamicdatasource.constant.DynamicDataSourceConstants;
import lombok.extern.slf4j.Slf4j;
/**
* @author tiancj
* 数据源key上下文
*/
@Slf4j
@Component
public class DynamicDataSourceContextHolder {
/**
* 数据源Map
*/
public static Map<Object, Object> dataSourcesMap = new ConcurrentHashMap<>(10);
/**
* 动态数据源名称上下文
*/
public final static ThreadLocal<String> DS_CONTEXT_KEY_HOLDER = new ThreadLocal<String>();
/**
* 设置/切换 数据源
* @param key
*/
public static void setContextKey(String key) {
DS_CONTEXT_KEY_HOLDER.set(key);
log.info("#########切换至 {} 数据源#########", key);
}
/**
* 获取数据源名称
*/
public static String getContextKey() {
String key = DS_CONTEXT_KEY_HOLDER.get();
return StringUtils.isEmpty(key)?DynamicDataSourceConstants.DS_KEY_MASTER:key;
}
/**
* 移除当前数据库名称
*/
public static void removeContextKey() {
log.info("#########移除 {} 数据源#########", DS_CONTEXT_KEY_HOLDER.get());
DS_CONTEXT_KEY_HOLDER.remove();
}
}
3. 定义动态数据源 这里继承AbstractRoutingDataSource类 重写determineCurrentLookupKey方法 此方法提供路由策略
/**
* @author tiancj
* 配置动态数据源路由策略
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getContextKey();
}
}
4. 配置动态数据源的初始化(实际上只配置主数据源,必须要配一个数据源哦),这里注意是手动配置数据源,加上 exclude = { DataSourceAutoConfiguration.class };同时注意加上了@Primary和@DependsOn({DynamicDataSourceConstants.DS_KEY_MASTER,"springBeanUtil","dynamicDataSourceContextHolder"})
package com.jade.component.dynamicdatasource.config;
import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import com.jade.component.dynamicdatasource.constant.DynamicDataSourceConstants;
import com.jade.component.dynamicdatasource.context.DynamicDataSourceContextHolder;
import com.jade.platform.core.util.SpringBeanUtil;
/**
* @author tiancj
* 配置动态数据源
*/
@Configuration
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })
public class DynamicDataSourceConfig {
/**
* 配置主数据源
* @return
*/
@Bean(DynamicDataSourceConstants.DS_KEY_MASTER)
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 配置动态数据源(默认)
* @return
*/
@Bean
@Primary
@DependsOn({DynamicDataSourceConstants.DS_KEY_MASTER,"springBeanUtil","dynamicDataSourceContextHolder"})
public DataSource dynamicDataSource() {
DynamicDataSourceContextHolder.dataSourcesMap.put(DynamicDataSourceConstants.DS_KEY_MASTER,SpringBeanUtil.getBean(DynamicDataSourceConstants.DS_KEY_MASTER));
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(DynamicDataSourceContextHolder.dataSourcesMap);
dynamicDataSource.setDefaultTargetDataSource(SpringBeanUtil.getBean(DynamicDataSourceConstants.DS_KEY_MASTER));
return dynamicDataSource;
}
}
5. 配置一下动态数据源切换的常量(随你怎么弄)
package com.jade.component.dynamicdatasource.constant;
/**
* @author tiancj
* 数据源配置常量(默认)
*/
public class DynamicDataSourceConstants {
/**
* 主库 key
*/
public final static String DS_KEY_MASTER = "master";
}
上面动态数据源配置完了,下面就是怎么切换数据源的事了,这里使用切面在相关包中方法调用之前切换好数据源
1. 首先定义一个数据源切换工具类,注意afterPropertiesSet()方法。
package com.jade.component.dynamicdatasource.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import javax.annotation.PostConstruct;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.jade.component.dynamicdatasource.config.DynamicDataSource;
import com.jade.component.dynamicdatasource.context.DynamicDataSourceContextHolder;
import com.jade.platform.core.exception.ServiceException;
import com.jade.platform.core.util.DateUtil;
import com.jade.platform.core.util.SpringBeanUtil;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
/**
* @author tiancj 动态数据源工具类
*/
@Slf4j
@Component
public class DynamicDataSourceUtil {
@Value("${spring.datasource.master.jdbc-url}")
private String url;
@Value("${spring.datasource.master.username}")
private String ds_userName;
@Value("${spring.datasource.master.password}")
private String ds_userPassword;
private static String JDBC_DB_NAME;
private static String JDBC_PREFIX_URL;
private static String JDBC_SUFFIX_URL;
private static String JDBC_DS_USER_NAME;
private static String JDBC_DS_USER_PASSWORD;
@PostConstruct
public void init() {
String[] split = url.split("\\?");
JDBC_DB_NAME = split[0].substring(split[0].lastIndexOf("/")+1);
JDBC_PREFIX_URL = split[0].substring(0, split[0].lastIndexOf("/")+1);
JDBC_SUFFIX_URL = "?"+split[1];
JDBC_DS_USER_NAME = ds_userName;
JDBC_DS_USER_PASSWORD = ds_userPassword;
}
/**
* 动态新增数据源(服务器IP固定)
* @param dsKey
*/
private static void setDataSource(String dsKey) {
String url = JDBC_PREFIX_URL +JDBC_DB_NAME +"_"+ dsKey + JDBC_SUFFIX_URL;
if (checkDsIsLive(url, JDBC_DS_USER_NAME, JDBC_DS_USER_PASSWORD, dsKey,JDBC_DB_NAME)) {//数据源是否有效
// 创建数据源
HikariDataSource hkDataSource = new HikariDataSource();
hkDataSource.setJdbcUrl(url);
hkDataSource.setUsername(JDBC_DS_USER_NAME);
hkDataSource.setPassword(JDBC_DS_USER_PASSWORD);
// 配置数据源
DynamicDataSource dynamicDataSource = (DynamicDataSource) SpringBeanUtil.getBean("dynamicDataSource");
DynamicDataSourceContextHolder.dataSourcesMap.put(dsKey, hkDataSource);
dynamicDataSource.afterPropertiesSet();
//切换数据源
DynamicDataSourceContextHolder.setContextKey(dsKey);
log.info("#########已添加 {} 数据源#########", JDBC_DB_NAME+"_"+dsKey);
}else {
throw new ServiceException(null,"数据库 "+JDBC_DB_NAME+"_"+dsKey+" 不存在!");
}
}
/**
* 判断数据库是否有效
* @param url
* @param userName
* @param password
* @param dsKey
* @param dbName
* @return boolean
*/
private static boolean checkDsIsLive(String url,String userName,String password,String dsKey,String dbName) {
String checkDSIsExist= "show databases like '"+dbName+"_"+dsKey+"'";
boolean flag = false;
try {
Connection connection = DriverManager.getConnection(url, userName, password);
Statement stat = connection.createStatement();
ResultSet result = stat.executeQuery(checkDSIsExist);
if (result.next()) {
flag = true;
}
} catch (SQLException e) {
e.printStackTrace();
}
return flag;
}
/**
* @param key 格式:yyyyMM
* @throws ServiceException
*/
public static void changeDataSource(String key) throws ServiceException {
if (StringUtils.isNotEmpty(key)) {
if (DateUtil.checkYMFormat(key)) {
Object dataSource = DynamicDataSourceContextHolder.dataSourcesMap.get(key);
if (dataSource == null) {//创建新的数据源并切换
setDataSource(key);
}else {//如果已存在只切换数据源
DynamicDataSourceContextHolder.setContextKey(key);
}
}else {
throw new ServiceException(null, "年月格式不正确!");
}
}
}
/**
* 清空当前请求的数据源 key context
* @throws ServiceException
*/
public static void clearDataSourceToDefault() throws ServiceException{
DynamicDataSourceContextHolder.removeContextKey();
}
/**
* 获取主数据库名称
* @return String
*/
public static String getDbName() {
return JDBC_DB_NAME;
}
}
2. 弄一个切面根据请求传参控制切换哪个数据源 &:dataSource=“202005”(Map中数据源的key) ,如果参数为空则用默认数据源(主数据源)。
package com.jade.component.dynamicdatasource.aspect;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.jade.component.dynamicdatasource.util.DynamicDataSourceUtil;
import lombok.extern.slf4j.Slf4j;
/**
* @author tiancj
* 配置数据源需要动态切换的Controller
*/
@Aspect
@Component
@Slf4j
public class HandlerDynamicDataSourceAop {
@Pointcut("(execution(* com.jade.userdefined.statistics.web.controller.*.*(..))) || (execution(* com.jade.userdefined.query.web.controller.*.*(..))) || (execution(* com.jade.userdefined.information.web.controller.*.*(..))) || (execution(* com.jade.userdefined.export.web.controller.*.*(..)))")
public void reqDynamicDataSource() {}
@Before("reqDynamicDataSource()")
public void doBefore(JoinPoint joinPoint) {
log.info("----------------------- 进入数据源动态切换AOP HandlerDynamicDataSourceAop start -----------------------");
HttpServletRequest req = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
String dataSource = req.getParameter("dataSource");
if (StringUtils.isNotEmpty(dataSource)) {
DynamicDataSourceUtil.changeDataSource(dataSource);
}
//获取注解上的数据源的值的信息
log.info("AOP动态切换数据源,className:"+joinPoint.getTarget().getClass().getName()+"methodName:"+joinPoint.getSignature().getName()+";dataSourceKey:"+dataSource==""?"默认数据源":dataSource);
}
@AfterReturning("reqDynamicDataSource()")
public void doAfterReturn(JoinPoint joinPoint) {
log.info("----------------------- 进入数据源动态切换AOP HandlerDynamicDataSourceAop end -----------------------");
HttpServletRequest req = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
String dataSource = req.getParameter("dataSource");
if (StringUtils.isNotEmpty(dataSource)) {
DynamicDataSourceUtil.clearDataSourceToDefault();
}
//获取注解上的数据源的值的信息
log.info("恢复至默认数据源");
}
}
最后说一下事务配置 这里不像mybatis一样可以配在dao层 配置如下
package com.jade.config;
import javax.persistence.EntityManagerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* 事务配置加载
* @author tiancj
*
*/
@Configuration
@ImportResource(locations={"classpath:transaction.xml"})
@EnableTransactionManagement
public class TransactionConfiguration {
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new
HibernateJpaVendorAdapter();
return hibernateJpaVendorAdapter;
}
@Bean
@Autowired
public PlatformTransactionManager
transactionManager(EntityManagerFactory emf) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(emf);
return txManager;
}
}
- 到此完结 不懂可以看一下源码 核心东西AbstractRoutingDataSource类 它通过determineTargetDataSource方法路由数据源而此方法中又通过determineCurrentLookupKey方法来选定数据源 所以我们控制了determineCurrentLookupKey方法就是掌握了数据源路由;