springboot+mybatis數據源動態切換與加載

一.普及知識

  • 一個數據源,也就代表一個數據庫,=數據的源頭
  • 數據源實例:一個數據庫連接,就代表一個數據源實例對象;
  • 多數據源實例:多個數據庫連接對象;

二.尋找解決辦法

  • 我們的項目使用SpringBoot+Mybatis開發的領域層,默認只連接一個數據庫;
  • 網上查詢大部分的做法都是多數據源之間動態切換,也就是說在配置文件中提前配置好幾個數據庫連接信息,自己獲取配置文件中的這些配置,然後在springBoot啓動的使用想辦法自動創建這 幾個數據源實例
  • 在後續需要切換數據庫的時候,只需要指定對應的數據源key,進行動態切換即可;
  • 可是我們的需求並不是這樣的,我們需要根據外部的變量進行動態創建數據源實例,然後在切換到該數據源上
  • 對於多數據源的切換和加載,以下這篇文件講的非常到位:
    基於Spring Boot實現Mybatis的多數據源切換和動態數據源加載
  • 所以我的項目主要需要解決的是多數據源動態加載,當然有了動態加載,動態切換就很簡單了;

pom.xml需要添加

 

        <!-- 引入aop -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- druid數據源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.6</version>
        </dependency>

三.在 application.yml 中配置多個數據庫連接信息如下:

 

db:
  default:
    #url: jdbc:mysql://localhost:3306/product_master?useSSL=false&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&autoReconnect=true
    driver-class-name: com.mysql.jdbc.Driver
    url-base: jdbc:mysql://
    host: localhost
    port: 3306
    dbname: ljyun_share
    url-other: ?useSSL=false&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&autoReconnect=true
    username: common
    password: common

  #藍景商城數據庫
  dbMall:
    driver-class-name: com.mysql.jdbc.Driver
    url-base: jdbc:mysql://
    host: localhost
    port: 3306
    dbname: db_mall
    url-other: ?useSSL=false&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&autoReconnect=true
    username: common
    password: common
  #雲平臺私有庫
  privateDB:
    driver-class-name: com.mysql.jdbc.Driver
    url-base: jdbc:mysql://
    host: localhost
    port: 3306
    dbname: ljyun_{id}_merchant
    url-other: ?useSSL=false&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&autoReconnect=true
    username: common
    password: common

四.項目目錄

多數據源-1.jpg

五.動態數據設置以及獲取,本類屬於單例;

  • DynamicDataSource 需要繼承 AbstractRoutingDataSource

 

package domain.dbs;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * 動態數據設置以及獲取,本類屬於單例
 * @author lxf 2018-09-29
 */
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    //單例句柄
    private static DynamicDataSource instance;
    private static byte[] lock=new byte[0];
    //用於存儲已實例的數據源map
    private static Map<Object,Object> dataSourceMap=new HashMap<Object, Object>();

    /**
     * 獲取當前數據源
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        logger.info("Current DataSource is [{}]", DynamicDataSourceContextHolder.getDataSourceKey());
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }

    /**
     * 設置數據源
     * @param targetDataSources
     */
    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        dataSourceMap.putAll(targetDataSources);
        super.afterPropertiesSet();// 必須添加該句,否則新添加數據源無法識別到
    }

    /**
     * 獲取存儲已實例的數據源map
     * @return
     */
    public Map<Object, Object> getDataSourceMap() {
        return dataSourceMap;
    }

    /**
     * 單例方法
     * @return
     */
    public static synchronized DynamicDataSource getInstance(){
        if(instance==null){
            synchronized (lock){
                if(instance==null){
                    instance=new DynamicDataSource();
                }
            }
        }
        return instance;
    }

    /**
     * 是否存在當前key的 DataSource
     * @param key
     * @return 存在返回 true, 不存在返回 false
     */
    public static boolean isExistDataSource(String key) {
        return dataSourceMap.containsKey(key);
    }
}

六.數據源配置類

  • DataSourceConfigurer 在tomcat啓動時觸發,在該類中生成多個數據源實例並將其注入到 ApplicationContext 中;
  • 該類通過使用 @Configuration@Bean 註解,將創建好的多數據源實例自動注入到 ApplicationContext上下中,供後期切換數據庫用;

 

package domain.dbs;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 數據源配置類,在tomcat啓動時觸發,在該類中生成多個數據源實例並將其注入到 ApplicationContext 中
 * @author lxf 2018-09-27
 */

@Configuration
@EnableConfigurationProperties(MybatisProperties.class)
public class DataSourceConfigurer {
    //日誌logger句柄
    private final Logger logger = LoggerFactory.getLogger(getClass());
    //自動注入環境類,用於獲取配置文件的屬性值
    @Autowired
    private Environment evn;

    private MybatisProperties mybatisProperties;
    public DataSourceConfigurer(MybatisProperties properties) {
        this.mybatisProperties = properties;
    }


    /**
     * 創建數據源對象
     * @param dbType 數據庫類型
     * @return data source
     */
    private DruidDataSource createDataSource(String dbType) {
        //如果不指定數據庫類型,則使用默認數據庫連接
        String dbName = dbType.trim().isEmpty() ? "default" : dbType.trim();
        DruidDataSource dataSource = new DruidDataSource();
        String prefix = "db." + dbName +".";
        String dbUrl = evn.getProperty( prefix + "url-base")
                        + evn.getProperty( prefix + "host") + ":"
                        + evn.getProperty( prefix + "port") + "/"
                        + evn.getProperty( prefix + "dbname") + evn.getProperty( prefix + "url-other");
        logger.info("+++default默認數據庫連接url = " + dbUrl);
        dataSource.setUrl(dbUrl);
        dataSource.setUsername(evn.getProperty( prefix + "username"));
        dataSource.setPassword(evn.getProperty( prefix + "password"));
        dataSource.setDriverClassName(evn.getProperty( prefix + "driver-class-name"));
        return dataSource;
    }

    /**
     * spring boot 啓動後將自定義創建好的數據源對象放到TargetDataSources中用於後續的切換數據源用
     *             (比如:DynamicDataSourceContextHolder.setDataSourceKey("dbMall"),手動切換到dbMall數據源
     * 同時指定默認數據源連接
     * @return 動態數據源對象
     */
    @Bean
    public DynamicDataSource dynamicDataSource() {
        //獲取動態數據庫的實例(單例方式)
        DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
        //創建默認數據庫連接對象
        DruidDataSource defaultDataSource = createDataSource("default");
        //創建db_mall數據庫連接對象
        DruidDataSource mallDataSource = createDataSource("dbMall");

        Map<Object,Object> map = new HashMap<>();
        //自定義數據源key值,將創建好的數據源對象,賦值到targetDataSources中,用於切換數據源時指定對應key即可切換
        map.put("default", defaultDataSource);
        map.put("dbMall", mallDataSource);
        dynamicDataSource.setTargetDataSources(map);
        //設置默認數據源
        dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);

        return dynamicDataSource;
    }

    /**
     * 配置mybatis的sqlSession連接動態數據源
     * @param dynamicDataSource
     * @return
     * @throws Exception
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(
            @Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        bean.setMapperLocations(mybatisProperties.resolveMapperLocations());
        bean.setTypeAliasesPackage(mybatisProperties.getTypeAliasesPackage());
        bean.setConfiguration(mybatisProperties.getConfiguration());
        return bean.getObject();
    }
    @Bean(name = "sqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(
            @Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory)
            throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    /**
     * 將動態數據源添加到事務管理器中,並生成新的bean
     * @return the platform transaction manager
     */
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }
}

七.通過 ThreadLocal 獲取和設置線程安全的數據源 key

  • DynamicDataSourceContextHolder類的實現

 

package domain.dbs;

/**
 * 通過 ThreadLocal 獲取和設置線程安全的數據源 key
 */
public class DynamicDataSourceContextHolder {

    /**
     * Maintain variable for every thread, to avoid effect other thread
     */
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
        /**
         * 將 default 數據源的 key 作爲默認數據源的 key
         */
//        @Override
//        protected String initialValue() {
//            return "default";
//        }
    };


    /**
     * To switch DataSource
     *
     * @param key the key
     */
    public static synchronized void setDataSourceKey(String key) {
        contextHolder.set(key);
    }

    /**
     * Get current DataSource
     *
     * @return data source key
     */
    public static String getDataSourceKey() {
        return contextHolder.get();
    }

    /**
     * To set DataSource as default
     */
    public static void clearDataSourceKey() {
        contextHolder.remove();
    }
}

八.AOP實現在DAO層做動態數據源切換(本項目沒有用到

 

package domain.dbs;

/**
 * 動態數據源切換的切面,切 DAO 層,通過 DAO 層方法名判斷使用哪個數據源,實現數據源切換
 * 關於切面的 Order 可以可以不設,因爲 @Transactional 是最低的,取決於其他切面的設置,
 * 並且在 org.springframework.core.annotation.AnnotationAwareOrderComparator 會重新排序
 *
 * 注意:本項目因爲是外部傳遞進來的雲編號,根據動態創建數據源實例,並且進行切換,而這種只用dao層切面的方式,
 *    適用於進行多個master/slave讀寫分類用的場景,所以我們的項目用不到這種方式(我們如果使用這種方式,
 *      就需要修改daoAai入參方式,在前置處理器獲取dao的方法參數,根據參數切換數據庫,這樣就需要修改dao接口,
 *      以及對應mapper.xml,需要了解動態代理的知識,所以目前我們沒有使用該方式,目前我們使用的是
 *      在service或controller層手動切庫)
 */

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

//@Aspect
//@Component
public class DynamicDataSourceAspect {
    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

    private final String[] QUERY_PREFIX = {"select"};

    @Pointcut("execution( * domain.dao.impl.*.*(..))")
    public void daoAspect() {
    }

    @Before("daoAspect()")
    public void switchDataSource(JoinPoint point) {
        Object[] params = point.getArgs();
        System.out.println(params.toString());
        String param = (String) params[0];
        for (Object string:params
             ) {
            System.out.println(string.toString());
        }
        System.out.println("###################################################");
        System.out.println(point.getSignature().getName());
        Boolean isQueryMethod = isQueryMethod(point.getSignature().getName());
        //DynamicDataSourceContextHolder.setDataSourceKey("slave");
        if (isQueryMethod) {
            DynamicDataSourceContextHolder.setDataSourceKey("slave");
            logger.info("Switch DataSource to [{}] in Method [{}]",
                    DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());
        }
    }

    @After("daoAspect())")
    public void restoreDataSource(JoinPoint point) {
        DynamicDataSourceContextHolder.clearDataSourceKey();
        logger.info("Restore DataSource to [{}] in Method [{}]",
                DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());
    }

    private Boolean isQueryMethod(String methodName) {
        for (String prefix : QUERY_PREFIX) {
            if (methodName.startsWith(prefix)) {
                return true;
            }
        }
        return false;
    }
}

九.SwitchDB手動切換數據庫類

  • ControllerService 需要切換數據庫的使用,需要使用 SwitchDB.change() 方法.

 

package domain.dbs;

import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.transaction.PlatformTransactionManager;

import java.util.HashMap;
import java.util.Map;

/**
 * 切換數據庫類
 * @author lxf 2018-09-28
 */
@Configuration
@Slf4j
public class SwitchDB {
    @Autowired
    private Environment evn;
    //私有庫數據源key
    private static String  ljyunDataSourceKey = "ljyun_" ;

    @Autowired
    DynamicDataSource dynamicDataSource;

    @Autowired
    private PlatformTransactionManager transactionManager;

    /**
     * 切換數據庫對外方法,如果私有庫id參數非0,則首先連接私有庫,否則連接其他已存在的數據源
     * @param dbName 已存在的數據庫源對象
     * @param ljyunId 私有庫主鍵
     * @return 返回當前數據庫連接對象對應的key
     */
    public String change(String dbName,int ljyunId)
    {
        if( ljyunId == 0){
            toDB(dbName);
        }else {
            toYunDB(ljyunId);
        }
        //獲取當前連接的數據源對象的key
        String currentKey = DynamicDataSourceContextHolder.getDataSourceKey();
        log.info("=====當前連接的數據庫是:" + currentKey);
        return currentKey;
    }

    /**
     * 切換已存在的數據源
     * @param dbName
     */
    private void toDB(String dbName)
    {
        //如果不指定數據庫,則直接連接默認數據庫
        String dbSourceKey = dbName.trim().isEmpty() ? "default" : dbName.trim();
        //獲取當前連接的數據源對象的key
        String currentKey = DynamicDataSourceContextHolder.getDataSourceKey();
        //如果當前數據庫連接已經是想要的連接,則直接返回
        if(currentKey == dbSourceKey) return;
        //判斷儲存動態數據源實例的map中key值是否存在
        if( DynamicDataSource.isExistDataSource(dbSourceKey) ){
            DynamicDataSourceContextHolder.setDataSourceKey(dbSourceKey);
            log.info("=====普通庫: "+dbName+",切換完畢");
        }else {
            log.info("切換普通數據庫時,數據源key=" + dbName + "不存在");
        }
    }

    /**
     * 創建新的私有庫數據源
     * @param ljyunId
     */
    private void  toYunDB(int ljyunId){
        //組合私有庫數據源對象key
        String dbSourceKey = ljyunDataSourceKey+String.valueOf(ljyunId);
        //獲取當前連接的數據源對象的key
        String currentKey = DynamicDataSourceContextHolder.getDataSourceKey();
        if(dbSourceKey == currentKey) return;

        //創建私有庫數據源
        createLjyunDataSource(ljyunId);

        //切換到當前數據源
        DynamicDataSourceContextHolder.setDataSourceKey(dbSourceKey);
        log.info("=====私有庫: "+ljyunId+",切換完畢");
    }

    /**
     * 創建私有庫數據源,並將數據源賦值到targetDataSources中,供後切庫用
     * @param ljyunId
     * @return
     */
    private DruidDataSource createLjyunDataSource(int ljyunId){
        //創建新的數據源
        if(ljyunId == 0)
        {
            log.info("動態創建私有庫數據時,私有庫主鍵丟失");
        }
        String yunId = String.valueOf(ljyunId);
        DruidDataSource dataSource = new DruidDataSource();
        String prefix = "db.privateDB.";
        String dbUrl = evn.getProperty( prefix + "url-base")
                + evn.getProperty( prefix + "host") + ":"
                + evn.getProperty( prefix + "port") + "/"
                + evn.getProperty( prefix + "dbname").replace("{id}",yunId) + evn.getProperty( prefix + "url-other");
        log.info("+++創建雲平臺私有庫連接url = " + dbUrl);
        dataSource.setUrl(dbUrl);
        dataSource.setUsername(evn.getProperty( prefix + "username"));
        dataSource.setPassword(evn.getProperty( prefix + "password"));
        dataSource.setDriverClassName(evn.getProperty( prefix + "driver-class-name"));

        //將創建的數據源,新增到targetDataSources中
        Map<Object,Object> map = new HashMap<>();
        map.put(ljyunDataSourceKey+yunId, dataSource);
        DynamicDataSource.getInstance().setTargetDataSources(map);
        return dataSource;
    }
}

十.Service中根據外部變量手動切換數據庫,使用SwitchDB.change()

  • TestTransaction實現

 

package domain.service.impl.exhibition;

import domain.dao.impl.ExhibitionDao;
import domain.dbs.DynamicDataSource;
import domain.dbs.DynamicDataSourceContextHolder;
import domain.dbs.SwitchDB;
import domain.domain.DomainResponse;
import domain.domain.Exhibition;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import java.util.HashMap;
import java.util.Map;

/**
 * 測試切庫後的事務類
 * @author lxf 2018-09-28
 */
@Service
@Slf4j
public class TestTransaction {
    @Autowired
    private ExhibitionDao dao;
    @Autowired
    private SwitchDB switchDB;

    @Autowired
    DynamicDataSource dynamicDataSource;

    public DomainResponse testProcess(int kaiguan, int ljyunId, String dbName){
        switchDB.change(dbName,ljyunId);
        //獲取當前已有的數據源實例
        System.out.println("%%%%%%%%"+dynamicDataSource.getDataSourceMap());
        return process(kaiguan,ljyunId,dbName);
    }

    /**
     * 事務測試
     * 注意:(1)有@Transactional註解的方法,方法內部不可以做切換數據庫操作
     *      (2)在同一個service其他方法調用帶@Transactional的方法,事務不起作用,(比如:在本類中使用testProcess調用process())
     *         可以用其他service中調用帶@Transactional註解的方法,或在controller中調用.
     * @param kaiguan
     * @param ljyunId
     * @param dbName
     * @return
     */
    //propagation 傳播行爲 isolation 隔離級別  rollbackFor 回滾規則
    @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT,timeout=36000,rollbackFor=Exception.class)
    public DomainResponse process(int kaiguan, int ljyunId, String dbName ) {
        String currentKey = DynamicDataSourceContextHolder.getDataSourceKey();
        log.info("=====service當前連接的數據庫是:" + currentKey);
            Exhibition exhibition = new Exhibition();
            exhibition.setExhibitionName("A-001-003");
            //return new DomainResponse<String>(1, "新增成功", "");
            int addRes = dao.insert(exhibition);
            if(addRes>0 && kaiguan==1){
                exhibition.setExhibitionName("B-001-002");
                int addRes2 = dao.insert(exhibition);
                return new DomainResponse<String>(1, "新增成功", "");
            }else
            {
                Map<String,String> map = new HashMap<>();
                String a = map.get("hello");
                //log.info("-----a="+a.replace("a","b"));
                //手動回滾事務
                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                return new DomainResponse<String>(0, "新增錯誤,事務已回滾", "");
            }
        }
}

十一.切庫與事務

  • 需要在 DataSourceConfigurer 類中添加如下配置,讓事務管理器與動態數據源對應起來;

      /**
       * 將動態數據源添加到事務管理器中,並生成新的bean
       * @return the platform transaction manager
       */
      @Bean
      public PlatformTransactionManager transactionManager() {
          return new DataSourceTransactionManager(dynamicDataSource());
      }
    ``` 



轉載:
鏈接:https://www.jianshu.com/p/7f1b785cd986
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章