spring 動態創建數據源


  項目需求如下,公司對外提供服務,公司本身有個主庫,另外公司會為每個新客戶創建一個數據庫,客戶的數據庫地址,用戶名,密碼,都保存在主數據庫中。由於不斷有新的客戶加入,所以要求,項目根據主數據庫中的信息,來動態創建數據源。

  解決方案:

    spring提供了一個類,AbstractRoutingDataSource,可以創建多個數據庫,並在幾個數據庫中進行切換。建議讀者在讀本文之前先了解一下這個類的使用

    afterPropertiesSet(),

    determineCurrentLookupKey(),

    determineTargetDataSource(), 

    上面這3個方法是AbstractRoutingDataSource類中的3個方法,這個方案也是基於這3個方法來實現的,先看代碼

 

一個DynamicDataSource 類,主要負責保存和創建數據源

public class DynamicDataSource extends AbstractRoutingDataSource {
    private Logger log = Logger.getLogger(this.getClass());
    
    @Autowired
    private CenterDatebaseManager centerDatebaseManager;
    // 默認數據源,也就是主庫
    protected DataSource masterDataSource;
    // 保存動態創建的數據源
    private static final Map targetDataSource = new HashMap<>();
    
    @Override
    protected DataSource determineTargetDataSource() {
        // 根據數據庫選擇方案,拿到要訪問的數據庫
        String dataSourceName = determineCurrentLookupKey();
        if("dataSource".equals(dataSourceName)) {
            // 訪問默認主庫
            return masterDataSource;
        }
        
        // 根據數據庫名字,從已創建的數據庫中獲取要訪問的數據庫
        DataSource dataSource = (DataSource) targetDataSource.get(dataSourceName);
        if(null == dataSource) {
            // 從已創建的數據庫中獲取要訪問的數據庫,如果沒有則創建一個
            dataSource = this.selectDataSource(dataSourceName);
        }
        return dataSource;
    }
    
    @Override
    protected String determineCurrentLookupKey() {
        // TODO Auto-generated method stub
        String dataSourceName = Dbs.getDbType();
        if (dataSourceName == null || dataSourceName == "dataSource") {
            // 默認的數據源名字
            dataSourceName = "dataSource";
        }
        log.debug("use datasource : " + dataSourceName);
        return dataSourceName;
    }

    /*public void setTargetDataSource(Map targetDataSource) {
        this.targetDataSource = targetDataSource;
        super.setTargetDataSources(this.targetDataSource);
    }*/

    /*public Map getTargetDataSource() {
        return this.targetDataSource;
    }*/

    public void addTargetDataSource(String key, BasicDataSource dataSource) {
        this.targetDataSource.put(key, dataSource);
        //setTargetDataSources(this.targetDataSource);
    }

    
    /**
     * 該方法為同步方法,防止並發創建兩個相同的數據庫
     * 使用雙檢鎖的方式,防止並發
     * @param dbType
     * @return
     */
    private synchronized DataSource selectDataSource(String dbType) {
        // 再次從數據庫中獲取,雙檢鎖
        DataSource obj = (DataSource)this.targetDataSource.get(dbType);
        if (null != obj) {
            return obj;
        } 
        // 為空則創建數據庫
        BasicDataSource dataSource = this.getDataSource(dbType);
        if (null != dataSource) {
            // 將新創建的數據庫保存到map中
            this.setDataSource(dbType, dataSource);
            return dataSource;
        }else {
            throw new SystemException("創建數據源失敗!");
        }
    }
    
    /**
     * 查詢對應數據庫的信息
     * @param dbtype
     * @return
     */
    private BasicDataSource getDataSource(String dbtype) {
        String oriType = Dbs.getDbType();
        // 先切換回主庫
        Dbs.setDbType("dataSource");
        // 查詢所需信息
        CenterDatebase datebase = centerDatebaseManager.getById(dbtype);
        // 切換回目標庫
        Dbs.setDbType(oriType);
        
        String url = "jdbc:sqlserver://" + datebase.getIp() + ":1433"
                + ";DatabaseName=" + datebase.getDatabaseName();
        BasicDataSource dataSource = createDataSource(url,datebase.getUserName(),datebase.getPassword());
        return dataSource;
    }
    
    //創建SQLServer數據源
    private BasicDataSource createDataSource(String url,String userName,String password) {
        return createDataSource("com.microsoft.sqlserver.jdbc.SQLServerDriver", url, userName, password);
    }
    
    //創建數據源
    private BasicDataSource createDataSource(String driverClassName, String url,
            String username, String password) {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setTestWhileIdle(true);

        return dataSource;
    }

    public void setDataSource(String type, BasicDataSource dataSource) {
        this.addTargetDataSource(type, dataSource);
        Dbs.setDbType(type);
    }

/*    @Override
    public void setTargetDataSources(Map targetDataSources) {
        // TODO Auto-generated method stub
        super.setTargetDataSources(targetDataSources);
        // 重點:通知container容器數據源發生了變化
        afterPropertiesSet();
    }*/
    
    
    /**
     * 該方法重寫為空,因為AbstractRoutingDataSource類中會通過此方法將,targetDataSources變量中保存的數據源交給resolvedDefaultDataSource變量
     * 在本方案中動態創建的數據源保存在了本類的targetDataSource變量中。如果不重寫該方法為空,會因為targetDataSources變量為空報錯
     * 如果仍然想要使用AbstractRoutingDataSource類中的變量保存數據源,則需要在每次數據源變更時,調用此方法來為resolvedDefaultDataSource變量更新
     */
    @Override
    public void afterPropertiesSet() {
    }

    public DataSource getMasterDataSource() {
        return masterDataSource;
    }

    public void setMasterDataSource(DataSource masterDataSource) {
        this.masterDataSource = masterDataSource;
    }
    
    
}

一個Dbs類,主要負責切換數據源,保存當前線程要訪問的數據源

public class Dbs {
    private static final ThreadLocal<String> local = new ThreadLocal<String>();
       
    public static String getDbType(){
        return local.get();
    }
   
    public static void setDbType(String dbName){
        local.set(dbName);
    }
   
    public static void clear(){
        local.remove();
    }
}

spring 配置文件(不完整),只貼出了配置數據源的部分,其他部分正常配就行了

<!-- 引入jdbc配置文件 -->  
    <context:property-placeholder location="classpath:jdbc.properties" /> 
    
    <!--創建jdbc數據源 -->  
    <bean id="masterDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">  
        <property name="driverClassName" value="${jdbc.driverClassName}" />  
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />  
        <property name="password" value="${jdbc.password}" />  
        <property name="initialSize" value="10" /> 
        <property name="maxActive" value="50" /> 
        <property name="maxIdle" value="15" /> 
        <property name="minIdle" value="5" /> 
        <property name="removeAbandoned" value="true" /> 
        <property name="removeAbandonedTimeout" value="60" /> 
        <property name="maxWait" value="10000" /> 
        <property name="logAbandoned" value="true" /> 
    </bean>


<!--多數據源  -->
    <bean id="multipleDataSource"  class="com.zoneking.basis.dynamicDB.DynamicDataSource">
          <property name="masterDataSource"  ref="masterDataSource"></property>
    </bean>

 

  通過Dbs類中的 local 變量來記錄當前線程要訪問的數據源,在DynamicDataSource類中根據local 變量來取對應的數據源,沒有的話則創建一個。

  本方案完全拋棄了AbstractRoutingDataSource類中的成員變量,所以要將 afterPropertiesSet() 方法重寫為空。原因在注釋中簡單寫了一下,如果想了解具體細節的話,可以復制到本地運行,當然要改成能運行的,其實主要就是改寫創建數據庫時獲取,數據庫的地址,用戶名和密碼那部分。

  當時寫這個的時候參考了網上另外一篇文章,但是由於時間久遠,找不到那篇文章了。在那篇文章中,作者使用了AbstractRoutingDataSource類中的成員變量來保存數據源,想了解的朋友可以在網上仔細找找。

  本方案只是一個簡單的方案,仍然存有很多問題。比如,只有創建數據源,沒有刪除;在集群模式下,仍然創建了大量的重復的數據源。同時,文章中有什么錯誤的地方,歡迎各路大神指出。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM