MyBatis 配置多數據源實現多個數據庫動態切換 V2.0

原來寫過一篇關於SSM多數據源配置的博客,爲什麼今天又要寫一篇呢?當然是因爲需求的變更(蹭訪問量),原來的博客中,多個數據源是配置在xml文件中的,每一個數據源都對應了一個會話管理器dataSource,這樣就把數據源的數量給訂死了,你有幾個會話管理器就有幾個數據源,不太方便,所以這次想達到一個動態添加刪除數據源的效果.

原來的博文請見 https://blog.csdn.net/qq_37612755/article/details/82908700 寫的反正也不咋地

1.創建存放數據源信息的實體類


/**
 * 數據源實體類
 */
public class SourceEntity {
    private String url;
    private String username;
    private String password;
    private String driver;
    private String type;
    private String beanKey;

    public SourceEntity(String type,String driver,String url, String username, String password,String beanKey ) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.driver = driver;
        this.type = type;
        this.beanKey = beanKey;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getDriver() {
        return driver;
    }

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getBeanKey() {
        return beanKey;
    }

    public void setBeanKey(String beanKey) {
        this.beanKey = beanKey;
    }
}

2.創建 CustomerContextHolder ,用於切換數據源信息和清除數據源信息

public class CustomerContextHolder {
    public static final String DATASOURCE_DEFAULT = "dataSource";
    public static final String DATASOURCE_REGION = "dataSourceRegion";

    private static ThreadLocal<String> contentHolder = new ThreadLocal<>();

    public static String getCustomerType() {
        return contentHolder.get();
    }

    public static void setCustomerType(String customerType) {
        contentHolder.set(customerType);
    }

    public static void clearCustomerType() {
        contentHolder.remove();
    }

}

3.在mybatis.xml 文件中添加下面的配置 ,dataSource 爲你的主數據庫,主數據庫還是使用xml配置的方式

<!--動態數據源配置-->
	<bean id="dynamicDataSource" class="com.mlkj.common.config.dataSourceConfig.DynamicDataSource">
			<property name="targetDataSources">
				<map key-type="java.lang.String">
					<!--現有數據源-->
					<entry value-ref="dataSource" key="dataSource"/>
				</map>
			</property>
			<!--默認數據源-->
			<property name="defaultTargetDataSource" ref="dataSource"/>
	</bean>

4.在 sqlSessionFactory 的  dataSource中引入 3 中的數據源配置

 	<!-- MyBatis begin -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dynamicDataSource"/>
        <property name="typeAliasesPackage" value="com.mlkj"/>
        <property name="typeAliasesSuperType" value="com.mlkj.common.persistence.BaseEntity"/>
        <property name="mapperLocations" value="classpath:/mappings/**/*.xml"/>
		<property name="configLocation" value="classpath:/mybatis-config.xml"></property>
    </bean>
    

5 .創建 DynamicDataSource  繼承 AbstractRoutingDataSource  多數據源支持類,重寫  以下方法,當數據在請求數據庫之前,會調用 determineCurrentLookupKey 方法獲取到要使用的數據源標誌,如果沒有拿到,則使用默認數據源


public class DynamicDataSource extends AbstractRoutingDataSource {
    @Autowired
    private ApplicationContext applicationContext;

    @Lazy
    @Autowired
    private DynamicDataSourceSummoner summoner;

    @Override
    protected String determineCurrentLookupKey() {
        System.out.println("當前線程:"+Thread.currentThread().getName()+"正在使用的數據源"+CustomerContextHolder.getCustomerType());
        return com.mlkj.common.utils.StringUtils.isNotBlank(CustomerContextHolder.getCustomerType())?CustomerContextHolder.getCustomerType():CustomerContextHolder.DATASOURCE_DEFAULT;
    }

   @Override
    protected DataSource determineTargetDataSource() {
        String beanKey = CustomerContextHolder.getCustomerType();
        //獲取當前線程中所使用的數據源標誌,然後去 applicationContext 中查看是否有對應的bean
        if (!StringUtils.hasText(beanKey) || applicationContext.containsBean(beanKey)) {
            return super.determineTargetDataSource();
        }else{
            //如果沒有對應的bean,則初始化
            summoner.registerDynamicDataSources();
        }
        return super.determineTargetDataSource();
    }


}

4. 創建 DynamicDataSourceSummoner 實現ApplicationListener<ContextRefreshedEvent>類,會在Spring 加載完成後執行,這時候我們將會話管理器註冊到bean中,Global.getConfig類爲讀取配置文件類,你們的可能不一樣

@Slf4j
@Component
public class DynamicDataSourceSummoner implements ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    private ConfigurableApplicationContext applicationContext;
    @Autowired
    private DynamicDataSource dynamicDataSource;

    private static boolean loaded = false;

    /**
     * Spring加載完成後執行
     */
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 防止重複執行
        if (!loaded) {
            loaded = true;
            try {
                registerDynamicDataSources();
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("數據源初始化失敗, Exception:" + e.getMessage());
            }
        }
    }

    /**
     * 從數據庫讀取租戶的DB配置,並動態注入Spring容器
     */
    public void registerDynamicDataSources() {
        // 把數據源bean註冊到容器中
        List<SourceEntity> entities = Lists.newArrayList();
        //由於這裏只有一個數據源,所以就直接從配置文件中讀取了,以後如果有多個可以改成從主數據庫中讀取
        SourceEntity entity = new SourceEntity(Global.getConfig("jdbc.typeRegion"),
                Global.getConfig("jdbc.driverRegion"), Global.getConfig("jdbc.urlRegion"),
                Global.getConfig("jdbc.usernameRegion"), Global.getConfig("jdbc.passwordRegion"), CustomerContextHolder.DATASOURCE_REGION);
        entities.add(entity);

        addDataSourceBeans(entities);
    }

    /**
     * 根據DataSource創建bean並註冊到容器中
     */
    private void addDataSourceBeans(List<SourceEntity> sourceEntities) {
        Map<Object, Object> targetDataSources = Maps.newLinkedHashMap();
        BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getBeanFactory();
        for (SourceEntity entity : sourceEntities) {

            // 如果該數據源已經在spring裏面註冊過,則不重新註冊
            if (applicationContext.containsBean(entity.getBeanKey())) {
                DruidDataSource existsDataSource = applicationContext.getBean(entity.getBeanKey(), DruidDataSource.class);
                if (isSameDataSource(existsDataSource, entity)) {
                    continue;
                }
            }
            //  組裝bean
            AbstractBeanDefinition beanDefinition = getBeanDefinition(entity);
            //  註冊bean
            beanFactory.registerBeanDefinition(entity.getBeanKey(), beanDefinition);
            //  放入map中,注意一定是剛纔創建bean對象
            DruidDataSource bean = null;
            try {
                bean = applicationContext.getBean(entity.getBeanKey(), DruidDataSource.class);
                targetDataSources.put(entity.getBeanKey(), bean);
                CacheUtils.put(RealDataUtils.IS_SOURCE_BREAK, false);
            } catch (Exception e) {
                e.printStackTrace();
                //如果報錯,刪除註冊的bean
                beanFactory.removeBeanDefinition(entity.getBeanKey());
                try {
                    Boolean isSourceBreak = (Boolean) CacheUtils.get(RealDataUtils.IS_SOURCE_BREAK);
                    if (isSourceBreak == null || !isSourceBreak) {
                        RealDataUtils.sendMessage("區域庫", e.getMessage(), new Date());
                        CacheUtils.put(RealDataUtils.IS_SOURCE_BREAK, true);
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }

                throw e;
            }
        }
        //  將創建的map對象set到 targetDataSources;
        dynamicDataSource.setTargetDataSources(targetDataSources);
        //  必須執行此操作,纔會重新初始化AbstractRoutingDataSource 中的 resolvedDataSources,也只有這樣,動態切換纔會起效
        dynamicDataSource.afterPropertiesSet();
    }

    /**
     * 組裝數據源spring bean
     */
    private AbstractBeanDefinition getBeanDefinition(SourceEntity entity) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(DruidDataSource.class);
        builder.getBeanDefinition().setAttribute("id", entity.getBeanKey());
        // 其他配置繼承defaultDataSource
        //builder.setParentName(CustomerContextHolder.DATASOURCE_REGION);
        builder.setInitMethodName("init");
        builder.setDestroyMethodName("close");
        builder.addPropertyValue("name", entity.getBeanKey());
        builder.addPropertyValue("url", entity.getUrl());
        builder.addPropertyValue("username", entity.getUsername());
        builder.addPropertyValue("password", entity.getPassword());
        //配置初始化大小、最小、最大
        builder.addPropertyValue("initialSize", Global.getConfig("jdbc.pool.init"));
        builder.addPropertyValue("minIdle", Global.getConfig("jdbc.pool.minIdle"));
        builder.addPropertyValue("maxActive", Global.getConfig("jdbc.pool.maxActive"));
        //配置獲取連接等待超時的時間
        builder.addPropertyValue("maxWait", "60000");
        // 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
        builder.addPropertyValue("timeBetweenEvictionRunsMillis", "60000");
        //連接重試時間
        builder.addPropertyValue("timeBetweenConnectErrorMillis", "5000");
        //配置一個連接在池中最小生存的時間,單位是毫秒
        builder.addPropertyValue("minEvictableIdleTimeMillis", "300000");
        builder.addPropertyValue("validationQuery", Global.getConfig("jdbc.testSql"));
        builder.addPropertyValue("testWhileIdle", "true");
        builder.addPropertyValue("testOnBorrow", "false");
        builder.addPropertyValue("testOnReturn", "false");
        //數據庫連接錯誤時關閉連接
        /*builder.addPropertyValue("breakAfterAcquireFailure", "true");
        builder.addPropertyValue("connectionErrorRetryAttempts", "0");*/
        //打開PSCache,並且指定每個連接上PSCache的大小(Oracle使用)
        //builder.addPropertyValue("poolPreparedStatements", "true");
        //builder.addPropertyValue("maxPoolPreparedStatementPerConnectionSize", "20");

        //以下配置建議在調試時使用,線上會影響性能
        //超過時間限制是否回收
        //builder.addPropertyValue("removeAbandoned", "true");
        //超時時間;單位爲秒。180秒=3分鐘
        //builder.addPropertyValue("removeAbandonedTimeout", "180");
        //關閉abanded連接時輸出錯誤日誌
        builder.addPropertyValue("logAbandoned", "true");
        //配置監控統計攔截的filters
        builder.addPropertyValue("filters", "stat");

        //builder.addPropertyValue("connectionProperties", DataSourceUtil.getConnectionProperties(entity.getDbPublicKey()));
        return builder.getBeanDefinition();
    }

    /**
     * 判斷Spring容器裏面的DataSource與數據庫的DataSource信息是否一致
     * 備註:這裏沒有判斷public_key,因爲另外三個信息基本可以確定唯一了
     */
    private boolean isSameDataSource(DruidDataSource existsDataSource, SourceEntity entity) {
        boolean sameUrl = Objects.equals(existsDataSource.getUrl(), entity.getUrl());
        if (!sameUrl) {
            return false;
        }
        boolean sameUser = Objects.equals(existsDataSource.getUsername(), entity.getUsername());
        if (!sameUser) {
            return false;
        }
        try {
            //String decryptPassword = ConfigTools.decrypt(entity.getDbPublicKey(), entity.getDbPassword());
            return Objects.equals(existsDataSource.getPassword(), entity.getPassword());
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("數據源密碼校驗失敗,Exception:{}" + e.getMessage());
            return false;
        }
    }

5 完整的mybatis.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd



		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd"
       default-lazy-init="true">

 	<!-- MyBatis begin -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dynamicDataSource"/>
        <property name="typeAliasesPackage" value="com.mlkj"/>
        <property name="typeAliasesSuperType" value="com.mlkj.common.persistence.BaseEntity"/>
        <property name="mapperLocations" value="classpath:/mappings/**/*.xml"/>
		<property name="configLocation" value="classpath:/mybatis-config.xml"></property>
    </bean>
    
    <!-- 掃描basePackage下所有以@MyBatisDao註解的接口 -->
    <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
        <property name="basePackage" value="com.mlkj"/>
        <property name="annotationClass" value="com.mlkj.common.persistence.annotation.MyBatisDao"/>
    </bean>
    
    <!-- 定義事務 -->
	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dynamicDataSource" />
	</bean>

	<!-- 配置 Annotation 驅動,掃描@Transactional註解的類定義事務  -->
	<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
    <!-- MyBatis end -->
    
	<!-- 數據源配置, 使用 BoneCP 數據庫連接池 -->
	<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
	    <!-- 數據源驅動類可不寫,Druid默認會自動根據URL識別DriverClass -->
	    <property name="driverClassName" value="${jdbc.driver}" />
	    
		<!-- 基本屬性 url、user、password -->
		<property name="url" value="${jdbc.url}" />
		<property name="username" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />

		<!-- 配置初始化大小、最小、最大 -->
		<property name="initialSize" value="${jdbc.pool.init}" />
		<property name="minIdle" value="${jdbc.pool.minIdle}" /> 
		<property name="maxActive" value="${jdbc.pool.maxActive}" />

		<!-- 配置獲取連接等待超時的時間 -->
		<property name="maxWait" value="60000" />
		
		<!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒 -->
		<property name="timeBetweenEvictionRunsMillis" value="60000" />
		
		<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
		<property name="minEvictableIdleTimeMillis" value="300000" />
		
		<property name="validationQuery" value="${jdbc.testSql}" />
		 
		<property name="testWhileIdle" value="true" />
		<property name="testOnBorrow" value="false" />
		<property name="testOnReturn" value="false" />


		<!-- 打開PSCache,並且指定每個連接上PSCache的大小(Oracle使用)
		<property name="poolPreparedStatements" value="true" />
		<property name="maxPoolPreparedStatementPerConnectionSize" value="20" /> -->

		<!--以下配置建議在調試時使用,線上會影響性能-->
		<!-- 超過時間限制是否回收 
		<property name="removeAbandoned" value="true" /> -->
		<!-- 超時時間;單位爲秒。180秒=3分鐘 
		<property name="removeAbandonedTimeout" value="180" /> -->
		<!-- 關閉abanded連接時輸出錯誤日誌 
		<property name="logAbandoned" value="true" /> -->

		<!-- 配置監控統計攔截的filters -->
	    <property name="filters" value="stat" /> 
	</bean>

	<!--動態數據源配置-->
	<bean id="dynamicDataSource" class="com.mlkj.common.config.dataSourceConfig.DynamicDataSource">
			<property name="targetDataSources">
				<map key-type="java.lang.String">
					<!--現有數據源-->
					<entry value-ref="dataSource" key="dataSource"/>
				</map>
			</property>
			<!--默認數據源-->
			<property name="defaultTargetDataSource" ref="dataSource"/>
	</bean>

</beans>

當我們需要切換數據源的時候,只需要在連接數據庫之前調用 CustomerContextHolder.setCustomerType(CustomerContextHolder.DATASOURCE_REGION); 就可以了, CustomerContextHolder.DATASOURCE_REGION爲你對應的數據源信息,數據庫查詢完成後在調用CustomerContextHolder.clearCustomerType(); 就可以切換會默認數據源

例子:

 //切換數據源
 CustomerContextHolder.setCustomerType(CustomerContextHolder.DATASOURCE_REGION);
 syncDataDao.findList(new SyncTable())
 //切回到默認數據源
 CustomerContextHolder.clearCustomerType();
        

 

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