數據庫連接池應用中數據庫服務器斷開超時連接的問題

數據庫應用開發過程中,我們可能會遇到一個問題:應用使用了數據庫連接池,每經過指定時間後,發出到數據庫服務器的任何請求都會失敗,而且有且僅有一次失敗,之後的正常訪問都沒有問題。尤其是在Web應用中,如果晚上時段沒有訪問,而第二天第一個訪客的經歷就是碰到一個數據庫訪問錯誤,如果開發系統的程序員沒有注意這個問題的話,可能終端用戶訪問會看到拋出的一堆數據庫異常信息。

其實,這個問題的主要原因是,應用中數據庫連接池中會保存指定數量的數據庫連接實例,而這些連接實例並沒有定時地檢測其到數據庫服務器連接是否正常;數據庫服務器可以配置一個數據庫連接實例的超時時間,超過時間後它會自動斷開連接。也就是,被斷開的那個連接此時仍然保存在應用的數據庫連接池內,下次被使用的時候就會發生數據庫連接斷開而導致一次訪問失敗。

解決上述連接關閉的方案有兩種值得推薦:

  • 如果能夠提供這樣一種檢測機制,在應用的連接池管理中定時地檢測連接池中連接的有效性,就完全可以避免上面描述的問題。
  • 在應用代碼中通過異常處理機制,來實現該次業務的重新處理,也可以很好地避免。

我們舉一個例子,使用Java開發的Web系統,Tomcat作爲HTTP服務器,MySQL作爲數據庫,拋出異常的信息如下所示:

[http-bio-8080-exec-10] 2012-11-28 00:55:43 [org.shirdrn.wm.de.action.StatAction]-[WARN] 
com.ibatis.dao.client.DaoException: Error ending SQL Map transaction.  Cause: java.sql.SQLException: Already closed.
	at com.ibatis.dao.engine.transaction.sqlmap.SqlMapDaoTransaction.rollback(SqlMapDaoTransaction.java:51)
	at com.ibatis.dao.engine.transaction.sqlmap.SqlMapDaoTransactionManager.rollbackTransaction(SqlMapDaoTransactionManager.java:85)
	at com.ibatis.dao.engine.impl.DaoContext.endTransaction(DaoContext.java:112)
	at com.ibatis.dao.engine.impl.DaoProxy.invoke(DaoProxy.java:77)
	at $Proxy8.selectByExample(Unknown Source)
	at org.shirdrn.wm.de.service.impl.StatItemsServiceImpl.countItems(Unknown Source)
	at org.shirdrn.wm.de.action.StatAction.makeStat(Unknown Source)
	at org.shirdrn.wm.de.action.StatAction.doGet(Unknown Source)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:621)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:224)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:169)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:168)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:98)
	at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:927)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407)
	at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:987)
	at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:579)
	at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:307)
	at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
	at java.lang.Thread.run(Thread.java:619)
Caused by: java.sql.SQLException: Already closed.
	at org.apache.commons.dbcp.PoolableConnection.close(PoolableConnection.java:84)
	at org.apache.commons.dbcp.PoolingDataSource$PoolGuardConnectionWrapper.close(PoolingDataSource.java:181)
	at com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransaction.close(JdbcTransaction.java:81)
	at com.ibatis.sqlmap.engine.transaction.TransactionManager.end(TransactionManager.java:93)
	at com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate.endTransaction(SqlMapExecutorDelegate.java:734)
	at com.ibatis.sqlmap.engine.impl.SqlMapSessionImpl.endTransaction(SqlMapSessionImpl.java:176)
	at com.ibatis.sqlmap.engine.impl.SqlMapClientImpl.endTransaction(SqlMapClientImpl.java:153)
	at com.ibatis.dao.engine.transaction.sqlmap.SqlMapDaoTransaction.rollback(SqlMapDaoTransaction.java:49)
	... 25 more

我們通過上面給出的第二種方案來解決,對應異常中實現的代碼,進行異常處理的邏輯如下所示:

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		boolean retry = false;
		String type = request.getParameter(RequestParams.ITEM_TYPE);
		String top = request.getParameter(RequestParams.TOP_N);
		Byte itemType = Byte.parseByte(type);
		Integer topN = super.topN;
		if(top!=null) {
			try {
				topN = Integer.parseInt(top);
			} catch (Exception e) {}
		}
		Target target = itemNames.get(itemType);
		try {
			makeStat(request, response, itemType, topN, target);
		} catch (Exception e) {
			LOG.warn("", e);
			// com.ibatis.dao.client.DaoException: Error ending SQL Map transaction.  Cause: java.sql.SQLException: Already closed.
			if(!retry && e instanceof DaoException) {
				LOG.warn("Try to obtain database connection again.");
				retry = true;
				this.makeStat(request, response, itemType, topN, target);
			} else {
				response.sendError(500, e.toString());
				return;
			}
		}
		request.getRequestDispatcher(target.url).forward(request, response);
	}

	private void makeStat(HttpServletRequest request, HttpServletResponse response, 
			Byte itemType, Integer topN, Target target) throws IOException, ServletException {
		List<StatItems> items = statItemsService.countItems(itemType, new Date(), topN);
		for (StatItems statK : items) {
			if(statK.getItemName()!=null && !"null".equalsIgnoreCase(statK.getItemName())) {
				pieDataset.setValue(statK.getItemName().trim() + " (" + statK.getPercentage() + ")", statK.getItemValue());
			}
		}
		String imageUrl = super.generateImage(pieDataset, target.title, request);
		request.setAttribute("items", items);
		request.setAttribute("imageUrl", imageUrl);
		if(items!=null && !items.isEmpty() && items.size()<topN) {
			topN = items.size();
		}
		request.setAttribute("topN", topN);
	}

上面代碼,判斷如果是發生連接失敗,則保存請求參數,再重新處理該請求。

 

另一種不推薦的方案,就是修改數據庫服務器的連接超時配置。因爲在實際項目中,通常應用上線的相關人員未必是DBA,對於修改數據庫服務器的配置可能會給其它上線業務帶來風險。解決方法如下:

以MySQL爲例,查看文件/etc/my.cnf,查詢有關超時配置的參數:

mysql> show variables like '%timeout';
+----------------------------+----------+
| Variable_name              | Value    |
+----------------------------+----------+
| connect_timeout            | 10       |
| delayed_insert_timeout     | 300      |
| innodb_lock_wait_timeout   | 50       |
| innodb_rollback_on_timeout | OFF      |
| interactive_timeout        | 28800    |
| lock_wait_timeout          | 31536000 |
| net_read_timeout           | 30       |
| net_write_timeout          | 60       |
| slave_net_timeout          | 3600     |
| wait_timeout               | 28800    |
+----------------------------+----------+

我們可以在屬性組mysqld下面修改如下兩個參數:

  • interactive_timeout
  • wait_timeout

MySQL數據庫服務器配置的連接超時時間默認是8小時,如果修改的超時時間足夠長的話,就不會出現前面發生的連接斷開的問題。但是,如果有很多應用都在使用數據庫連接池,大量的數據庫連接資源一直被佔用,嚴重的話可能使數據庫服務器宕機,而且,也會使一些攻擊者僞造大量請求,使數據庫服務器負荷過載而宕機,從而影響應用處理業務。

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