項目需求如下,公司對外提供服務,公司本身有個主庫,另外公司會為每個新客戶創建一個數據庫,客戶的數據庫地址,用戶名,密碼,都保存在主數據庫中。由於不斷有新的客戶加入,所以要求,項目根據主數據庫中的信息,來動態創建數據源。
解決方案:
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類中的成員變量來保存數據源,想了解的朋友可以在網上仔細找找。
本方案只是一個簡單的方案,仍然存有很多問題。比如,只有創建數據源,沒有刪除;在集群模式下,仍然創建了大量的重復的數據源。同時,文章中有什么錯誤的地方,歡迎各路大神指出。