springboot+jpa非固定数据源动态切换

  • 使用场景

项目中需要用到非固定数据源动态切换,每个月会定时从主库(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 dataSourcesMap = new ConcurrentHashMap<>(10);
    
    /**
     * 动态数据源名称上下文
     */
    public final static ThreadLocal DS_CONTEXT_KEY_HOLDER  = new ThreadLocal();
   
    /**
     * 设置/切换 数据源
     * @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方法就是掌握了数据源路由;

你可能感兴趣的:(springboot)