# (二):MySQL數據庫連接不夠用(TooManyConnections)問題的一次分析和解決案例
最近,項目中遇到了數據庫連接不夠的問題。
**異常信息**
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException:
?Data source rejected establishment of connection,? message from server: "Too many connections"
根據更詳細的錯誤信息,我定位到報錯的函數位置。
**關鍵函數**
~~~
/**?
?*?判斷數據庫是否存在?
?*/??
public?boolean?hasDatabaseByKey(String?name)?{??
?boolean?containsKey?=?dataSourceMap.containsKey(name);??
?if?(containsKey)?{??
??
??try?{??
??????//根據數據庫名稱,把BoneCP數據源中的數據庫參數取出來,用戶名、密碼、URL??
???Object?obj?=?dataSourceMap.get(name);??
???com.jolbox.bonecp.BoneCPDataSource?dataSource?=?(com.jolbox.bonecp.BoneCPDataSource)?obj;??
??
???String?password?=?dataSource.getPassword();??
???String?username?=?dataSource.getUsername();??
???String?url?=?dataSource.getJdbcUrl();??
???//建立新的數據庫連接--這一行代碼拋出“Too?many?connections”異常??
???Connection?con?=?DriverManager.getConnection(url,?username,?password);??
???//關閉連接??
???if?(con?!=?null)?{??
????con.close();??
???}??
???return?true;??
??
??}?catch?(Exception?e)?{??
???LOG.error("Database?name?error:"?+?name);??
???e.printStackTrace();??
??}??
?}??
?return?false;??
}??
~~~
?
**是否正常關閉了數據庫連接**
上述代碼的主要功能是,根據前端傳入的數據庫名字,如“test”,檢測該數據庫的配置是否正確。
最初想到的,上面的數據庫連接con可能沒有正常關閉。
經過單步跟蹤debug,和使用MySQL的 show processlist命令,發現所有的連接確實是正常關閉的。
因此,上述代碼的功能是沒有問題的。
**功能沒有問題,但是上述代碼還是存在其它問題的**
a.數據庫連接應該在finally里關閉。
b.這種檢測方法,每次都需要打開一個連接,比較耗費時間。
一種好的方法是,在系統初始化的時候,檢查所有的數據源配置是否正確,把結果
用Map保存起來,("test",true)表明test數據庫配置正確;
或者每一次檢查,先從緩存中取,如果存在直接使用,否則,打開連接,進行檢查,然后保存結果到緩存中。
**問題根源**
項目中使用了多個數據庫, 數據源公共的配置如下。
~~~
<bean id="abstractDataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close">
<property name="username" value="root" />
<property name="password" value="xxxx" />
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="maxConnectionsPerPartition" value="50"/>
<property name="minConnectionsPerPartition" value="2"/>
</bean>
~~~
**最近新增了10個數據源配置。**
也就是說,現在新增的數據庫連接,最高可以達到50*(10+1)=550個了。(不是50*1=50個)
而數據庫MySQL的默認max_connections是100。
因此,我們在訪問Web項目,然后頻繁切換項目的時候,數據庫連接池中的數目,已經達到了100。
這個時候,我們再去手動創建數據庫連接,就會失敗。
**上文代碼的更多配置信息**
~~~
@Resource??
private?Map?dataSourceMap;??
~~~
~~~
<!-- 配置多數據源映射關系 -->
<bean id="dataSourceMap" class="java.util.HashMap">
<constructor-arg>
<map key-type="java.lang.String">
<entry key="demo1">
<bean parent="abstractDataSource" >
<property name="jdbcUrl"
value="jdbc:mysql://ip:3306/demo1?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull" />
</bean>
</entry>
<entry key="demo2">
<bean parent="abstractDataSource" >
<property name="jdbcUrl"
value="jdbc:mysql://ip:3306/demo2?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull" />
</bean>
</entry>
</map>
</constructor-arg>
</bean>
~~~
**改進后的代碼**
~~~
private?static?Map?databaseStatusMap?=?new?HashMap();??
??/**?
??*?判斷數據庫是否存在?
??*/??
?public?boolean?hasDatabaseByKey(String?name)?{??
??//數據源中是否含有這個數據庫名稱??
??boolean?containsKey?=?dataSourceMap.containsKey(name);??
??//不存在這個數據名字,直接false??
??if?(!containsKey)?{??
???return?false;??
??}??
??
??//首先從緩存中拿,不為null,表明已經驗證過了??
??Boolean?status=databaseStatusMap.get(name);??
??if(status?!=?null){??
???return?status;??
??}??
????
??//緩存中不存在,第1次驗證??
??boolean?databaseConfigSucceed?=?false;??
??Connection?con?=?null;??
??try?{??
???Object?obj?=?dataSourceMap.get(name);??
???com.jolbox.bonecp.BoneCPDataSource?dataSource?=?(com.jolbox.bonecp.BoneCPDataSource)?obj;??
??
???String?password?=?dataSource.getPassword();??
???String?username?=?dataSource.getUsername();??
???String?url?=?dataSource.getJdbcUrl();??
??
???//驗證數據庫連接,把結果存到緩存中??
???con?=?DriverManager.getConnection(url,?username,?password);??
?????
???//這個地方con不可能為null,要么是一個正常的連接,要么拋出異常??
???databaseConfigSucceed?=?true;??
???databaseStatusMap.put(name,?true);??
???/*?
????if?(con?!=?null)?{?
????databaseConfigSucceed?=?true;?
????databaseStatusMap.put(name,?true);?
???}??
???else{?
????databaseConfigSucceed?=?false;?
????databaseStatusMap.put(name,?false);?
???}*/??
????
??}?catch?(Exception?e)?{??
???//拋出異常,下次不會去重新檢查,可能會存在bug,如果第1次檢測的時候,網斷了或者MySQL掛了或者數據庫連接過多,并不能代表配置不正確??
???//小概率事件,暫時不考慮??
???LOG.error("Database?name?error:"?+?name);??
???databaseConfigSucceed=false;??
???databaseStatusMap.put(name,?false);??
???e.printStackTrace();??
??}?finally?{??
???if?(con?!=?null)?{??
????try?{??
?????//驗證完數據庫連接,需要手動關閉??
?????con.close();??
????}?catch?(SQLException?e)?{??
?????e.printStackTrace();??
????}??
???}??
??}??
??return?databaseConfigSucceed;??
?}??
~~~
**小概率事件**
如果在執行?DriverManager.getConnection(url, username, password);獲取數據庫連接的時候,發生了異常。
如果恰好是第1次檢測的時候,網斷了或者MySQL掛了或者數據庫連接過多,并不能代表配置不正確。
拋出異常,下次就不會去重新檢查,可能會存在bug。
不過,系統第一次訪問項目的時候,正好出故障的可能性很低。
可能存在的錯誤情況:
第一次成功,加入緩存為true,如果今后數據庫連接打不開,應該提示“不可以打開”,也會提示“可以打開”。
第一次不成功,加入緩存為false,如果今后數據庫能夠打開,應該提示“可以打開”,也會提示“不可以打開”。
**結論**
1.在不使用緩存的情況下,每次都通過建立新連接的方式。
?? 優點:可以準確的判斷數據庫配置是否正確,是否真正能夠連接到數據庫,代碼的可讀性和復雜度比較低。
? 缺點:性能比較差。
2.使用緩存。
?? 優點:性能比較高。
?? 缺點:在不正常情況下,準確性沒有保障。代碼的可讀性和復雜度比較高。
**再次改進**
緩存,增加“時間限制”,過一段時間后,就失效。
**觀點**
實現的功能越多,越準確,性能越好,代碼可能會越來越復雜。
代碼的正確性和性能有的時候是“互相排斥”的。
過多的追求完美,也會帶來一些負擔。
**原文參見**:?[http://FansUnion.cn/articles/2961](http://fansunion.cn/articles/2961)