1. 程式人生 > >資料庫連線池應用中資料庫伺服器斷開超時連線的問題

資料庫連線池應用中資料庫伺服器斷開超時連線的問題

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

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

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

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

我們舉一個例子,使用Java開發的Web系統,Tomcat作為HTTP伺服器,MySQL作為資料庫,丟擲異常的資訊如下所示:

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

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

  1. protectedvoid doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {  
  2.     boolean retry = false;  
  3.     String type = request.getParameter(RequestParams.ITEM_TYPE);  
  4.     String top = request.getParameter(RequestParams.TOP_N);  
  5.     Byte itemType = Byte.parseByte(type);  
  6.     Integer topN = super.topN;  
  7.     if(top!=null) {  
  8.         try {  
  9.             topN = Integer.parseInt(top);  
  10.         } catch (Exception e) {}  
  11.     }  
  12.     Target target = itemNames.get(itemType);  
  13.     try {  
  14.         makeStat(request, response, itemType, topN, target);  
  15.     } catch (Exception e) {  
  16.         LOG.warn("", e);  
  17.         // com.ibatis.dao.client.DaoException: Error ending SQL Map transaction.  Cause: java.sql.SQLException: Already closed.
  18.         if(!retry && e instanceof DaoException) {  
  19.             LOG.warn("Try to obtain database connection again.");  
  20.             retry = true;  
  21.             this.makeStat(request, response, itemType, topN, target);  
  22.         } else {  
  23.             response.sendError(500, e.toString());  
  24.             return;  
  25.         }  
  26.     }  
  27.     request.getRequestDispatcher(target.url).forward(request, response);  
  28. }  
  29. privatevoid makeStat(HttpServletRequest request, HttpServletResponse response,   
  30.         Byte itemType, Integer topN, Target target) throws IOException, ServletException {  
  31.     List<StatItems> items = statItemsService.countItems(itemType, new Date(), topN);  
  32.     for (StatItems statK : items) {  
  33.         if(statK.getItemName()!=null && !"null".equalsIgnoreCase(statK.getItemName())) {  
  34.             pieDataset.setValue(statK.getItemName().trim() + " (" + statK.getPercentage() + ")", statK.getItemValue());  
  35.         }  
  36.     }  
  37.     String imageUrl = super.generateImage(pieDataset, target.title, request);  
  38.     request.setAttribute("items", items);  
  39.     request.setAttribute("imageUrl", imageUrl);  
  40.     if(items!=null && !items.isEmpty() && items.size()<topN) {  
  41.         topN = items.size();  
  42.     }  
  43.     request.setAttribute("topN", topN);  
  44. }  

上面程式碼,判斷如果是發生連線失敗,則儲存請求引數,再重新處理該請求。

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

以MySQL為例,檢視檔案/etc/my.cnf,查詢有關超時配置的引數:

  1. mysql> show variables like '%timeout';  
  2. +----------------------------+----------+  
  3. | Variable_name              | Value    |  
  4. +----------------------------+----------+  
  5. | connect_timeout            | 10       |  
  6. | delayed_insert_timeout     | 300      |  
  7. | innodb_lock_wait_timeout   | 50       |  
  8. | innodb_rollback_on_timeout | OFF      |  
  9. | interactive_timeout        | 28800    |  
  10. | lock_wait_timeout          | 31536000 |  
  11. | net_read_timeout           | 30       |  
  12. | net_write_timeout          | 60       |  
  13. | slave_net_timeout          | 3600     |  
  14. | wait_timeout               | 28800    |  
  15. +----------------------------+----------+  

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

  • interactive_timeout
  • wait_timeout

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