多資料來源的動態配置與載入使用兼架構互動的問題調試

來源:互聯網
上載者:User


        我遇到的問題是這樣的。項目使用 Spring + Hibernate + proxool 實現資料庫連接管理和訪問。 需求是實現多資料來源的動態配置和載入使用。 思路是:

          1.   用一個類  AdvancedDataSourceInitizer 實現ApplicationListener 介面,當 ContextRefreshEvent 事件被發布時, 自動從資料庫中讀取資料庫配置,轉化為 ProxoolDataSource 對象,並存入到一個 Map<dataSourceName,
ProxoolDataSource> 中;  

package opstools.moonmm.support.listener;import java.util.List;import java.util.Map;import javax.sql.DataSource;import opstools.framework.datasource.MultiDataSource;import opstools.moonmm.clusterconfig.entity.ClusterConfig;import opstools.moonmm.clusterconfig.service.ClusterConfigService;import opstools.moonmm.support.utils.DBUtil;import org.springframework.beans.BeansException;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;import org.springframework.context.ApplicationEvent;import org.springframework.context.ApplicationListener;import org.springframework.jdbc.datasource.lookup.MapDataSourceLookup;public class AdvancedDataSourceInitializer implements ApplicationListener, ApplicationContextAware {    private   String             desiredEventClassName;    protected ApplicationContext applicationContext;    public void onApplicationEvent(ApplicationEvent event) {        if (shouldStart(event)) {                        Map<String, DataSource> cachedMap = (Map<String, DataSource>)applicationContext.getBean("dataSources");            ClusterConfigService clusterConfigService = (ClusterConfigService)applicationContext.getBean("clusterConfigService");            List<ClusterConfig> cclist = clusterConfigService.getAllClusterConfigInstances();                        DBUtil.addCachedDatasources(cachedMap, cclist);            MapDataSourceLookup dsLookup =  (MapDataSourceLookup) applicationContext.getBean("dataSourceLookup");            dsLookup.setDataSources(cachedMap);            MultiDataSource mds = (MultiDataSource) applicationContext.getBean("dataSource");            mds.setTargetDataSources(cachedMap);              mds.afterPropertiesSet();        }    }    protected Class<?> getDesiredType() {        try {            return Class.forName(desiredEventClassName);        } catch (ClassNotFoundException e) {            throw new RuntimeException(e);        }    }    public String getDesiredEventClassName() {        return desiredEventClassName;    }    public void setDesiredEventClassName(String desiredEventClassName) {        this.desiredEventClassName = desiredEventClassName;    }    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {        this.applicationContext = applicationContext;    }    protected boolean shouldStart(ApplicationEvent event){        Class<?> clazz = getDesiredType();        return clazz.isInstance(event);    }}

       DBUtil.java : 用於將資料庫配置轉化為 ProxoolDataSource 對象, 歸入串連池管理

package opstools.moonmm.support.utils;import java.util.List;import java.util.Map;import javax.sql.DataSource;import opstools.moonmm.clusterconfig.entity.ClusterConfig;import opstools.moonmm.monitorconfig.entity.MonitorConfig;import org.logicalcobwebs.proxool.ProxoolDataSource;public class DBUtil {        private DBUtil() {}        private static final String MYSQL_DRIVER = "com.mysql.jdbc.Driver";        public static DataSource cluconfig2DataSource(ClusterConfig cc)    {        ProxoolDataSource ds = new ProxoolDataSource();        String url = "jdbc:mysql://"+cc.getDbIp()+":"+cc.getDbPort()+"/"+cc.getDbName();        ds.setDriver(MYSQL_DRIVER);        ds.setAlias(cc.getDataSource());        ds.setDriverUrl(url);        ds.setUser(cc.getDbUser());        ds.setPassword(cc.getDbPassword());        ds.setPrototypeCount(5);        ds.setMinimumConnectionCount(10);        ds.setMaximumConnectionCount(50);        return ds;    }    public static DataSource moniconfig2DataSource(MonitorConfig mc)    {        ProxoolDataSource ds = new ProxoolDataSource();        String url = "jdbc:mysql://"+ mc.getIp() +":"+ mc.getPort() + "/" + mc.getMonitordbName();        ds.setDriver(MYSQL_DRIVER);        ds.setAlias(mc.getNickname());        ds.setDriverUrl(url);        ds.setUser(mc.getUser());        ds.setPassword(mc.getPassword());        ds.setPrototypeCount(5);        ds.setMinimumConnectionCount(10);        ds.setMaximumConnectionCount(50);        return ds;    }            public static void addCachedDatasources(Map<String, DataSource> cachedMap, List<ClusterConfig> cclist)    {        for (ClusterConfig cc: cclist) {            cachedMap.put(cc.getDataSource(), cluconfig2DataSource(cc));        }    }    }

       2.  用一個類 SpringEventPublisher 實現 ApplicationContextAware, 用於擷取 applicationContext 執行個體 ;  當應用啟動時,以及增刪更新資料庫配置時, 發布 ContextRefreshEvent 事件, 觸發動態載入資料來源的行為;       

package opstools.moonmm.support.listener;import org.springframework.beans.BeansException;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;import org.springframework.context.event.ContextRefreshedEvent;public class SpringEventPublisher implements ApplicationContextAware {    private ApplicationContext appContext;        @Override    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {        this.appContext = applicationContext;    }        public void publishContextRefreshEvent()    {        appContext.publishEvent(new ContextRefreshedEvent(appContext));     }}

          3.  用一個類MultiDataSource 繼承 AbstractRoutingDataSource 來定位和切換資料來源。         

package opstools.framework.datasource;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;public class MultiDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DataSourceHolder.getCurrentDataSource();}}

package opstools.framework.datasource;public class DataSourceHolder {private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();public static String getCurrentDataSource() {return (String) contextHolder.get();}   public static void setDataSource(String dataSource){contextHolder.set(dataSource);}public static void setDefaultDataSource(){contextHolder.set(null);}public static void clearCustomerType() {   contextHolder.remove();   }  }

     上述三個類的BEAN執行個體都可以直接配置在Spring 檔案中。                     

        <util:map id="dataSources"><entry key="master" value-ref="masterDataSource" /></util:map><bean id="dataSourceLookup"class="org.springframework.jdbc.datasource.lookup.MapDataSourceLookup"></bean><bean id="dataSource" class="opstools.framework.datasource.MultiDataSource"><property name="targetDataSources" ref="dataSources"/><property name="defaultTargetDataSource" ref="masterDataSource" /><property name="dataSourceLookup" ref="dataSourceLookup" /></bean><bean id="sessionFactory"class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"><property name="dataSource" ref="dataSource" /><property name="configLocation" value="classpath:hibernate.cfg.xml" /><property name="packagesToScan" value="opstools.*.*.entity" /><property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" /><property name="namingStrategy"><bean class="org.hibernate.cfg.ImprovedNamingStrategy"></bean></property></bean><bean id="transactionManager"class="org.springframework.orm.hibernate3.HibernateTransactionManager"><property name="sessionFactory" ref="sessionFactory" /></bean><bean id="dataSourceInitializer" class="opstools.moonmm.support.listener.AdvancedDataSourceInitializer">        <property name="desiredEventClassName" value="org.springframework.context.event.ContextRefreshedEvent"/>    </bean>        <bean id="eventPublisher" class="opstools.moonmm.support.listener.SpringEventPublisher">    </bean>

          

           可是在實際使用中,卻無法正確切換資料來源,總是只能切換到第一個使用的資料來源。 後經查證, 發現必須設定 Proxool 別名,及串連數。

           public static ProxoolDataSource cluconfig2DataSource(ClusterConfig cc)  {

                       ProxoolDataSource pds = new ProxoolDataSource();

                       pds.setDriverUrl(...);

                       ...    

                       pds.setAlias(cc.getDataSource());   // 必須有這一行及下面幾行, 否則難以起作用。

                       pds.setMinimumConnectionCount(5);

                       pds.setMaximumConnectionCount(50);

                       pds.setPrototypeCount(10);   

           }  


        整個調試過程如下:

        首先,前提是準備好源碼,可以使用 Eclipse 的 MAVEN 外掛程式下載。選中指定的JAR包,右鍵 Maven ---> Download sources ,放在指定 \.m2\repository 目錄下。 Windows 下一般放在 Documents and settings\使用者目錄\.m2\repository\
;  Linux 下一般放在 ~/.m2/repository/ 。 當單步調試時,若缺乏相應類的源碼包, 會出現 Source Look up 介面及按鈕, 點擊添加源碼包之後,該介面就會變成相應類的源碼介面。建議使用項目構建工具 Maven  等,而不是手工從官網上搜尋下載。

        由於架構互動的代碼很多地方都可能出問題,因此, 只能採用單步調試; 但一行行執行太慢, 因此,需要根據出錯特徵進行分析,設定一些關鍵斷點。比如,這裡的關鍵點有: 設定 dataSourceName 的地方(驗證確實傳入了正確的資料來源的 key ),  擷取 DataSource的地方(驗證確實定位得到了相對應的資料來源對象),擷取
Connection 的地方(驗證確實獲得了正確的資料庫連接)等。注意,使用 Debug 模式運行,就是有小蟲的那個表徵圖,而不是右箭頭表徵圖。 通過單步調試,可以知道擷取 proxool 資料庫連接的具體過程如下(畫成UML順序圖表更佳):

        DataSourceHolder.setDataSource(dataSourceName) --->  AbstractRoutingDataSource.determineTargetDataSource(dataSourceName) ---> ProxoolDataSource ---> ProxoolDataSource.getConnection() --->
ConnectionPool.getConnection() ---> proxyConnections.getConnection(nextAvailableConnection)

      發現在這裡拋出了 IndexOutOfBoundsException 異常。 proxyConnections 中並未含有剛剛切換的資料來源的串連,而我假定的是, 應該由 Proxool 自動預先建立若干個串連放在相應串連池裡面的。 在代碼裡設定了串連數後,成功了; 其後還出現一次類似錯誤, 是通過設定別名而解決的。

        因為假定Proxool 會預先自動建立預設串連數的(靜態設定檔中沒有設定串連數是可用的,網上諸多文章也講到存在預設串連數的),並且以為別名是無關緊要的, 沒想到在這裡出了錯。 所以說,不能隨便作假設,但 Proxool 切換資料來源依賴於別名,這一點也挺讓人吃驚。

        為什麼ProxoolDataSource 的別名如此重要呢? 因為 proxool 使用 alias 識別不同資料庫的串連池。 有代碼為證:

        ProxoolDataSource.getConnection() 擷取資料庫連接的方法:

 /**     * @see javax.sql.DataSource#getConnection()     */    public Connection getConnection() throws SQLException {        ConnectionPool cp = null;        try {            if (!ConnectionPoolManager.getInstance().isPoolExists(alias)) {                registerPool();            }            cp = ConnectionPoolManager.getInstance().getConnectionPool(alias);            return cp.getConnection();        } catch (ProxoolException e) {            LOG.error("Problem getting connection", e);            throw new SQLException(e.toString());        }    }

       串連池管理器用於擷取串連池的代碼 ConnectionPoolManager.getConnectionPool , 使用一個MAP 來存放串連池,其中 Key 是串連池的別名,Value 是串連池執行個體

class ConnectionPoolManager {    private static final Object LOCK = new Object();    private Map connectionPoolMap = new HashMap();    private Set connectionPools = new HashSet();    private static ConnectionPoolManager connectionPoolManager = null;    private static final Log LOG = LogFactory.getLog(ProxoolFacade.class);    public static ConnectionPoolManager getInstance() {        if (connectionPoolManager == null) {            synchronized (LOCK) {                if (connectionPoolManager == null) {                    connectionPoolManager = new ConnectionPoolManager();                }            }        }        return connectionPoolManager;    }    private ConnectionPoolManager() {    }    /**     * Get the pool by the alias     * @param alias identifies the pool     * @return the pool     * @throws ProxoolException if it couldn't be found     */    protected ConnectionPool getConnectionPool(String alias) throws ProxoolException {        ConnectionPool cp = (ConnectionPool) connectionPoolMap.get(alias);        if (cp == null) {            throw new ProxoolException(getKnownPools(alias));        }        return cp;    }    /**     * Convenient method for outputing a message explaining that a pool couldn't     * be found and listing the ones that could be found.     * @param alias identifies the pool     * @return a description of the wht the pool couldn't be found     */    protected String getKnownPools(String alias) {        StringBuffer message = new StringBuffer("Couldn't find a pool called '" + alias + "'. Known pools are: ");        Iterator i = connectionPoolMap.keySet().iterator();        while (i.hasNext()) {            message.append((String) i.next());            message.append(i.hasNext() ? ", " : ".");        }        return message.toString();    }    /**     * Whether the pool is already registered     * @param alias how we identify the pool     * @return true if it already exists, else false     */    protected boolean isPoolExists(String alias) {        return connectionPoolMap.containsKey(alias);    }    /** @return an array of the connection pools */    protected ConnectionPool[] getConnectionPools() {        return (ConnectionPool[]) connectionPools.toArray(new ConnectionPool[connectionPools.size()]);    }    protected ConnectionPool createConnectionPool(ConnectionPoolDefinition connectionPoolDefinition) throws ProxoolException {        ConnectionPool connectionPool = new ConnectionPool(connectionPoolDefinition);        connectionPools.add(connectionPool);        connectionPoolMap.put(connectionPoolDefinition.getAlias(), connectionPool);        return connectionPool;    }    protected void removeConnectionPool(String name) {        ConnectionPool cp = (ConnectionPool) connectionPoolMap.get(name);        if (cp != null) {            connectionPoolMap.remove(cp.getDefinition().getAlias());            connectionPools.remove(cp);        } else {            LOG.info("Ignored attempt to remove either non-existent or already removed connection pool " + name);        }    }    public String[] getConnectionPoolNames() {        return (String[]) connectionPoolMap.keySet().toArray(new String[connectionPoolMap.size()]);    }}

       這就解釋了,為什麼Proxool 與別名的關係如此緊密。


       調試架構互動的問題還需要耐心。 因為出錯的具體地方可能分布在任何意料之外的位置,有可能在認為不相關的地方直接跳過了, 需要返回去再定位之前的位置,反覆如此,直到一步步接近出錯的位置。比如,開始在定位問題的時候, 並沒有做很詳細的分析,而是較隨意地單步加跳躍執行,從 Spring 源碼跳轉到 Proxool
的源碼 跳轉到  Hibernate 的源碼再跳回到 Spring , 不亦樂乎, 後來終於發現了一點小線索,逐步縮小範圍,最終定位到問題所在。 今天一整天的功夫就用來調試切換資料來源所出現的這兩個問題。這多少說明, 使用開發架構會增大調試的難度, 增加一些維護的成本。


       主要收穫是: 終於成功調試了一個關於架構互動的問題 :-) 


聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.