關於MySQL數據庫連接超時問題的分析與解決

操作系統 Windows 10 Enterprise,數據庫 MySQL-5.5.16,c3p0-0.9.5.2

關於針對數據庫的連接,之前沒有特別注意過,直到遇到如下問題:

Could not open JDBC Connection for transaction; nested exception is com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The last packet successfully received from the server was 54,812,410 milliseconds ago. The last packet sent successfully to the server was 54,812,411 milliseconds ago. is longer than the server configured value of ‘wait_timeout’. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property ‘autoReconnect=true’ to avoid this problem

從上面的信息得知,應用程序與數據庫服務器的連接中斷了,那麼爲什麼會出現上面的問題呢?不是有連接池嗎?裏面不是保存有大量的連接嗎?比如使用c3p0數據源配置如下:

ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setInitialPoolSize(5);
dataSource.setMaxPoolSize(20);
dataSource.setAcquireIncrement(1);

錯誤信息中已經說得很明白了,上一次客戶端和服務的通信已經是54812411ms前了,但是MySQL默認配置的wait_timeout屬性確是28800s,也就是8小時,該參數的意義是MySQL在沒有進行通信的連接上等待的最長時間直到服務器關閉該連接,上文信息中指出不通信的間隔已經有54812.411s了,所以客戶端與數據庫服務器的連接已經被服務器強制關閉了,所以纔會出現如上的錯誤。

但是信息中也給出了可以解決的辦法,如下:

  • 使連接過期
  • 對連接的有效性進行測試
  • 增加數據庫服務器的超時時間
  • 使用autoReconnect屬性避免
  • 通過捕捉異常來重試建立連接

上述幾種方法可以從範圍上分成數據庫層次、應用層次。

一、數據庫層次上解決

1.1 通過增大wait_timeout的值

使用這種方式是最直接、最暴力的,通過增加該屬性的值可以增大MySQL服務器在不活躍的連接上等待的時間,但是這種方式影響的範圍很大,因爲數據庫的使用面向的應用肯定不止一個,所以如果不在萬不得已的情況之下,不要使用這種方式。

1.2 配置autoReconnect屬性

表明數據庫驅動是否會重新建立不活躍的連接,默認情況下是false。一般是在JDBC的連接URL中使用。如果設置爲true,對於在不活躍的連接上進行的查詢會拋出異常,這些查詢屬於當前的事務,事務中進行下一次查詢時,驅動將會重新建立連接。這個屬性不推薦使用,因爲它對於會話狀態和數據一致性會造成影響,尤其是當應用不能正確的處理SQLExceptions時。在考慮強一致性的時候,尤其要避免使用該屬性。

二、應用層次上解決

2.1 使連接過期

此處使用的是c3p0數據源,在該數據源下配置連接的過期時間是通過maxIdleTime屬性實現的,該屬性的意義是:一個不使用的連接在數據庫連接池中存在的最長時間,直到它被連接池拋棄。單位是秒,默認值是0,表示連接池中的連接永遠不會過期。

ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setMaxIdleTime(1800);

配置的屬性值要大於wait_timeout的值,以免在MySQL服務器強制斷開連接之後,連接還沒有過期。

2.2 對連接有效性進行測試

對與數據庫服務器的連接進行有效性的測試是一個可行的方式,可以隨時確定數據庫服務器是否可用,而不僅僅是連接是否可用。這裏使用c3p0數據源作爲示例進行,其他的數據源,比如DBCPdruid其中也有相對應的方式完成這種操作。

c3p0提供了很多的方式來對連接池中的連接進行測試,通過這種測試來避免在應用程序的層次上看到上文中提到的錯誤。通過以下幾個屬性來完成這種測試:

下面三個屬性是控制什麼時候進行測試連接。

  • idleConnectionTestPeriod:如果該屬性的值大於0,每隔固定的時間段,c3p0會測試所有閒置、保持在連接池中的連接。默認情況下是不進行測試的;
  • testConnectionOnCheckout:是否在取連接進行測試;
  • testConnectionOnCheckin:是否在放入連接時進行測試;

下面三個屬性是決定如何進行測試連接。

  • automaticTestTablec3p0會自動創建一個空表以備查詢進行連接測試;
  • connectionTesterClassNameConnectionTester接口的全限定名實現, 默認值是com.mchange.v2.c3p0.impl.DefaultConnectionTester,用來定義c3p0數據源如何測試連接,過於靈活,避免過度使用;
  • preferredTestQuery:定義的被用來執行連接測試的查詢;

2.2.1 進行測試的時候要考慮的問題

在進行連接測試的時候,需要考慮很多其他的問題。然後根據這些需要考慮的額外的問題來確定最佳的測試方法。

2.2.1.1 性能問題

性能問題是一個非常重要的話題,尤其是在大型的複雜應用中,在測試有效性時,也不要給連接帶來過大的負擔。

首先要考慮連接測試的時機

  • 從連接池中取出連接時:這種測試是最簡單,也是最可靠的,但是會給客戶端的性能帶來極大的影響,可以通過配置屬性testConnectionOnCheckout=true來開啓在此時進行測試;
  • 連接放入連接池時:此時進行測試時,需要同時結合idleConnectionTestPeriod,可以滿足較高的可靠性,可以通過設置testConnectionOnCheckin=true,在這種情況下,在閒時和放入連接時進行異步測試,性能依然不會下降;

其次要考慮測試的語句

  • 如果使用的數據庫驅動支持JDBC4,並且使用的c3p0是0.9.5或以上。可以直接調用JDBC的API來處理,其中有一個isValid()方法可以用來實現作爲快速且可靠的連接測試。默認情況下,c3p0使用這個方法進行測試。如果想要設置一個超時時間,可以通過繼承IsValidOnlyConnectionTester來實現,如下:

    public class Tester extends IsValidOnlyConnectionTester{
      protected int getIsValidTimeout() { return 30; }
    }
  • 如果驅動不支持最新的API,默認情況下c3p0通過在連接上調用DatabaseMetaDatagetTables()方法。這樣可以與任何類型的數據庫有效兼容,但是由於返回很大的數據量會對數據庫連接池的性能造成顯著影響;在JDBC3驅動以下(或者0.9.5版本之前)可以配置preferredTestQuery加速測試,但它優先於數據源進行加載,此時如果數據庫中沒有對應的表,則會造成錯誤,如下:

    這裏寫圖片描述

    對於這種情況,則可以通過設置automaticTestTable屬性來代替,c3p0會自動的創建一個空表來作爲測試查詢的對象;

2.2.1.2 兼容性問題

另外一個要考慮的是數據庫兼容問題,比如需要兼容多種數據庫時,比如同時要測試的數據庫有MySQLSQL ServerOracle,則需要考慮測試語句的通用性。在這種情況下可以通過可以設定與數據庫和表無關的語句,比如select 1來測試這個連接。但是針對不同數據庫類型,支持的這種輕量級的測試語句並不相同,如下對不同的常見的數據庫的支持:

語句 數據庫類型
select 1 H2、MySQL、SQL Server、PostgreSQL、SQLite
select 1 from DUAL Oracle

以上的信息來自於這篇文章

2.2.1.3 靈活性的問題

如果對於數據源提供的測試方法或者需要自定義一些行爲時,靈活性的問題就很重要了。

c3p0也提供了這樣的支持,通過配置connectionTesterClassName屬性,提供給實現類的完全限定名即可。這種情況不需要preferredTestQuery或者automaticTestTable,默認情況下該屬性的值是com.mchange.v2.c3p0.impl.DefaultConnectionTester。可以通過繼承DefaultConnectionTester的父類AbstractConnectionTester實現。簡單實現如下:

public class MyConnectionTester extends AbstractConnectionTester {

    public int activeCheckConnection(Connection c, String preferredTestQuery, Throwable[] rootCauseOutParamHolder) {
        try {
            ResultSet resultset = c.createStatement().executeQuery("SELECT 1");
            while(resultset.next()){
                int result = resultset.getInt(1);
                if(result == 1){
                    System.out.println("test successfully");
                } else{
                    System.out.println("test failed");
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return 0;
    }

    public int statusOnException(Connection c, Throwable t, String preferredTestQuery, Throwable[] rootCauseOutParamHolder) {
        return 0;
    }
}

配置如下:

ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setIdleConnectionTestPeriod(3);
dataSource.setConnectionTesterClassName("com.lmy86263.MyConnectionTester");

2.3 針對連接斷開進行重試

由於目前所有的應用都是直接使用數據源來管理與數據庫的連接,而在程序內部只是提供一些必須的配置項,所以如果要重試則需要通過編程手段來完成,如下在建立數據源的時候如果連接失敗則會重試指定次數:

ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClass("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
dataSource.setUser("root");
dataSource.setPassword("abc");

int retry = 3;
while(retry > 0){
    try {
        dataSource.getConnection();
        break;
    } catch (SQLException e) {
        retry--;
    }
}

如果對JDBC的超時的詳細機制比較感興趣可以參考最後一篇文章。


相關文章:

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