hsql+mysql多數據源動態切換

業務背景

最近有個需求:須要先啓動一個web服務,而後這個服務去部署mysql,redis等相關服務,而且該服務要動態的把mysql在不重啓項目的狀況加載進去。javascript

項目思路

項目先使用內存數據庫或者內存進行數據暫存(這裏我選擇了內存數據庫hsql),啓動先加載hsqldb數據源,而後根據mysql部署狀況動態的添加到相應的數據源java

框架選型

springboot-2.11+mysql+hsqlmysql

1.pom文件(由於是直接貼出來的,有些不須要的請自行忽略)git

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis-spring-boot-starter.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.tomcat</groupId>
                    <artifactId>tomcat-jdbc</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- spring data jpa -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!-- Swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${io.springfox.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${io.springfox.version}</version>
        </dependency>

        <!-- MySQL Connector-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-connetcor.version}</version>
            <!--<scope>runtime</scope>-->
        </dependency>

        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.2</version>
            <scope>compile</scope>
            <optional>true</optional>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>

        <dependency>
            <groupId>commons-httpclient</groupId>
            <artifactId>commons-httpclient</artifactId>
            <version>${commons-httpclient.version}</version>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>${commons-lang.version}</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
        </dependency>

        <!-- 內存數據庫hsqldb -->
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.70</version>
        </dependency>
        
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.2.1</version>
        </dependency>
    </dependencies>

首先須要定義一個數據源切換註解,調用時代表使用哪個數據源github

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    DataSourceTypeEnum value() default DataSourceTypeEnum.MYSQL;
}

既然申明瞭註解,那麼咱們就須要去攔截並切換到對應的數據源,這時候就申明一個aop進行攔截處理web

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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

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

    @Autowired
    private HttpServletRequest request;

    /** * 攔截類做用域的註解 * @param dataSource */
    @Before("@within(dataSource) ")
    public void beforeDataSource(DataSource dataSource) {
        String value = dataSource.value().getDb();
        DataSourceContextHolder.setDataSource(value);

        try {
            logger.info("當前請求:{},當前使用的數據源爲:{}",request.getRequestURI(), value);
        }catch (Exception e){
        }
    }

    /** * 攔截method做用域的註解 * @param dataSource */
    @Before("@annotation(dataSource) ")
    public void beforeDataSourceMethod(JoinPoint joinPoint,DataSource dataSource) {
        String value = dataSource.value().getDb();
        DataSourceContextHolder.setDataSource(value);
        try {
            logger.info("當前請求:{},當前使用的數據源爲:{}",request.getRequestURI(), value);
        }catch (Exception e){
        }
    }

    /** * 按需在結束以後處理一些業務 * @param dataSource */
    @After("@within(dataSource) ")
    public void afterDataSource(DataSource dataSource) {
        // todo something with yourself;
    }

    @After("@annotation(dataSource) ")
    public void afterDataSourceMethod(DataSource dataSource) {
        // todo something with yourself;
    }
}

細心的朋友應該已經發現這裏用到了DataSourceContextHolder,而且設置了對應的數據源,那這個是作什麼的呢?咱們繼續往下看redis

import java.util.Map;

public class DataSourceContextHolder {
    // 存放當前線程使用的數據源類型
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    //把當前事物下的鏈接塞入,用於事物處理
    private static ThreadLocal<Map<String, ConnectWarp>> connectionThreadLocal = new ThreadLocal<>();

    // 設置數據源
    public static void setDataSource(String type){
        contextHolder.set(type);
    }

    // 獲取數據源
    public static String getDataSource(){
        return contextHolder.get();
    }

    // 清除數據源
    public static void clearDataSource(){
        contextHolder.remove();
    }


    // 設置鏈接
    public static void setConnection(Map<String, ConnectWarp> connections){
        connectionThreadLocal.set(connections);
    }

    // 獲取鏈接
    public static Map<String, ConnectWarp> getConnection(){
        return connectionThreadLocal.get();
    }

    // 清除鏈接
    public static void clearConnection(){
        connectionThreadLocal.remove();
    }

經過上面的代碼咱們發現它好像沒作什麼業務處理,只是單純的申明瞭線程變量,以及基本的賦值和查詢,那麼數據源真正的切換是如何實現的呢?別急,咱們立刻接近真相了spring

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

public class DynamicDataSource extends AbstractRoutingDataSource {

    private static Map<Object, Object> dataSourceMap = new HashMap<Object, Object>();

    private static DynamicDataSource instance;

    private static byte[] lock = new byte[0];

    public static DynamicDataSource getInstance() {
        if (instance == null) {
            synchronized (lock) {
                if (instance == null) {
                    instance = new DynamicDataSource();
                }
            }
        }
        return instance;
    }
    
    /** * 重寫setTargetDataSources,經過入參targetDataSources進行數據源的添加 * @param targetDataSources */
    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        dataSourceMap.putAll(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        String dataSource = DataSourceContextHolder.getDataSource();
        return dataSource;
    }

    /** * 獲取到原有的多數據源,並從該數據源基礎上添加一個或多個數據源後,經過上面的setTargetDataSources進行加載 * * @return */
    public Map<Object, Object> getDataSourceMap() {
        return dataSourceMap;
    }


    /** * 開啓事物的時候,把鏈接放入 線程中,後續crud 都會拿對應的鏈接操做 * * @param key * @param connection */
    public void bindConnection(String key, Connection connection) {
        Map<String, ConnectWarp> connectionMap = DataSourceContextHolder.getConnection();
        if (connectionMap == null) {
            connectionMap = new HashMap<>();
        }
        ConnectWarp connectWarp = new ConnectWarp(connection);

        connectionMap.put(key, connectWarp);
        DataSourceContextHolder.setConnection(connectionMap);

    }


    /** * 提交事物 * * @throws SQLException */
    protected void doCommit() throws SQLException {
        Map<String, ConnectWarp> stringConnectionMap = DataSourceContextHolder.getConnection();
        if (stringConnectionMap == null) {
            return;
        }
        for (String dataSourceName : stringConnectionMap.keySet()) {
            ConnectWarp connection = stringConnectionMap.get(dataSourceName);
            connection.commit(true);
            connection.close(true);
        }
        DataSourceContextHolder.clearConnection();
    }

    /** * 撤銷事物 * * @throws SQLException */
    protected void rollback() throws SQLException {
        Map<String, ConnectWarp> stringConnectionMap = DataSourceContextHolder.getConnection();
        if (stringConnectionMap == null) {
            return;
        }
        for (String dataSourceName : stringConnectionMap.keySet()) {
            ConnectWarp connection = stringConnectionMap.get(dataSourceName);
            connection.rollback();
            connection.close(true);
        }
        DataSourceContextHolder.clearConnection();
    }


    /** * 若是 在connectionThreadLocal 中有 說明開啓了事物,就從這裏面拿 * * @return * @throws SQLException */
    @Override
    public Connection getConnection() throws SQLException {
        Map<String, ConnectWarp> stringConnectionMap = DataSourceContextHolder.getConnection();
        if (stringConnectionMap == null) {
            //沒開事物 直接走
            return determineTargetDataSource().getConnection();
        } else {
            //開了事物,從當前線程中拿,並且拿到的是 包裝過的connect 只有我能關閉O__O "…
            String currentName = (String) determineCurrentLookupKey();
            return stringConnectionMap.get(currentName);
        }

    }
}

這個類裏面最核心的就是setTargetDataSources和determineCurrentLookupKey兩個方法。首先setTargetDataSources重寫,保證咱們可以動態的添加數據源;而後determineCurrentLookupKey重寫,保證咱們可以從咱們動態添加的數據源裏面取出相應的數據源。
到這裏呢就差很少快成功了,只差最後一步了,咱們來看看這關鍵的最後一步:添加到spring管理sql

import com.github.pagehelper.PageInterceptor;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

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


@Configuration
public class DataSourceConfig {


    @Autowired
    private HsqlConfig hsqlConfig;

    @Bean("hsqlDataSource")
    @Primary
    public DataSource hsqlDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        BeanUtils.copyProperties(hsqlConfig, dataSource);
        dataSource.setJdbcUrl(hsqlConfig.getJdbcUrl());
        return dataSource;
    }

    @Bean
    public DynamicDataSource dataSource(@Qualifier("hsqlDataSource") DataSource hsqlDataSource) {
        Map<Object, Object> map = new HashMap<>();
        map.put(DataSourceTypeEnum.HSQL.getDb(), hsqlDataSource);

        DynamicDataSource dynamicDataSource =  DynamicDataSource.getInstance();
        dynamicDataSource.setTargetDataSources(map);

        return dynamicDataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {

        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dynamicDataSource);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        //分頁插件
        Interceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        properties.setProperty("helperDialect", "mysql");
        properties.setProperty("offsetAsPageNum", "true");
        properties.setProperty("rowBoundsWithCount", "true");
        properties.setProperty("reasonable", "true");
        properties.setProperty("supportMethodsArguments", "true");
        interceptor.setProperties(properties);

        Interceptor[] plugins = {interceptor};
        factoryBean.setPlugins(plugins);
        return factoryBean.getObject();

    }

    @Bean
    @Qualifier("transactionManager")
    public PlatformTransactionManager transactionManager(DynamicDataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }

}

到這裏這個多數據動態添加已經切換就已經OK了,而後有些很細心很細心的朋友發現了,我裏面對connect也作了寫處理,這個是重寫了事務機制,這個咱們下期繼續分析。數據庫

相關文章
相關標籤/搜索