恰當的JDBC超時設置可以有效地減小服務失效的時間。本文將對數據庫的各類超時設置及其設置方法作介紹。 java
真實案例:應用服務器在遭到DDos攻擊後沒法響應 mysql
在遭到DDos攻擊後,整個服務都垮掉了。因爲第四層交換機不堪重負,網絡變得沒法鏈接,從而致使業務系統也沒法正常運轉。安全組很快屏蔽了全部的DDos攻擊,並恢復了網絡,但業務系統卻仍是沒法工做。 經過分析系統的thread dump發現,業務系統停在了JDBC API的調用上。20分鐘後,系統仍處於WAITING狀態,沒法響應。30分鐘後,系統拋出異常,服務恢復正常。 sql
爲何咱們明明將query timeout設置成了3秒,系統卻持續了30分鐘的WAITING狀態?爲何30分鐘後系統又恢復正常了? 當你對理解了JDBC的超時設置後,就能找到問題的答案。 數據庫
爲何咱們要了解JDBC?
當遇到性能問題或系統出錯時,業務系統和數據庫一般是咱們最關心的兩個部分。在公司裏,這兩個部分是交由兩個不一樣的部門來負責的,所以各個部門都會集中精力地在自身領域內尋找問題,這樣的話,在業務系統和數據庫之間的部分就會成爲一個盲區。對於Java應用而言,這個盲區就是DBCP數據庫鏈接池和JDBC,本文將集中介紹JDBC。 安全
什麼是JDBC? 服務器
JDBC是Java應用中用來鏈接關係型數據庫的標準API。Sun公司一共定義了4種類型的JDBC,咱們主要使用的是第4種,該類型的Driver徹底由Java代碼實現,經過使用socket與數據庫進行通訊。 網絡
第4種類型的JDBC經過socket對字節流進行處理,所以也會有一些基本網絡操做,相似於HttpClient這種用於網絡操做的代碼庫。當在網絡操做中遇到問題的時候,將會消耗大量的cpu資源,而且失去響應超時。若是你以前用過HttpClient,那麼你必定遇到過未設置timeout形成的錯誤。一樣,第4種類型的JDBC,若沒有合理地設置socket timeout,也會有相同的錯誤——鏈接被阻塞。
接下來,就讓咱們來學習一下如何正確地設置socket timeout,以及須要考慮的問題。 oracle
應用與數據庫間的timeout層級 框架
上圖展現了簡化後應用與數據庫間的timeout層級。(譯者注:WAS/BLOC是做者公司的具體應用名稱,無需深究)
高級別的timeout依賴於低級別的timeout,只有當低級別的timeout無誤時,高級別的timeout才能確保正常。例如,當socket timeout出現問題時,高級別的statement timeout和transaction timeout都將失效。
咱們收到的不少評論中提到: socket
statement timeout沒法處理網絡鏈接失敗時的超時,它能作的僅僅是限制statement的操做時間。網絡鏈接失敗時的timeout必須交由JDBC來處理。
JDBC的socket timeout會受到操做系統socket timeout設置的影響,這就解釋了爲何在以前的案例中,JDBC鏈接會在網絡出錯後阻塞30分鐘,而後又奇蹟般恢復,即便咱們並無對JDBC的socket timeout進行設置。
DBCP鏈接池位於圖2的左側,你會發現timeout層級與DBCP是相互獨立的。DBCP負責的是數據庫鏈接的建立和管理,並不干涉timeout的處理。當鏈接在DBCP中建立,或是DBCP發送校驗query檢查鏈接有效性的時候,socket timeout將會影響這些過程,但並不直接對應用形成影響。
當在應用中調用DBCP的getConnection()方法時,你能夠設置獲取數據庫鏈接的超時時間,可是這和JDBC的timeout絕不相關。
什麼是Transaction Timeout?
transaction timeout通常存在於框架(Spring, EJB)或應用級。transaction timeout或許是個相對陌生的概念,簡單地說,transaction timeout就是「statement Timeout * N(須要執行的statement數量) + @(垃圾回收等其餘時間)」。transaction timeout用來限制執行statement的總時長。
例如,假設執行一個statement須要0.1秒,那麼執行少許statement不會有什麼問題,但如果要執行100,000個statement則須要10,000秒(約7個小時)。這時,transaction timeout就派上用場了。EJB CMT (Container Managed Transaction)就是一種典型的實現,它提供了多種方法供開發者選擇。但咱們並不使用EJB,Spring的transaction timeout設置會更經常使用一些。在Spring中,你可使用下面展現的XML或是在源碼中使用@Transactional註解來進行設置。
Spring提供的transaction timeout配置很是簡單,它會記錄每一個事務的開始時間和消耗時間,當特定的事件發生時就會對消耗時間作校驗,當超出timeout值時將拋出異常。
Spring中,數據庫鏈接被保存在ThreadLocal裏,這被稱爲事務同步(Transaction Synchronization),與此同時,事務的開始時間和消耗時間也被保存下來。當使用這種代理鏈接建立statement時,就會校驗事務的消耗時間。EJB CMT的實現方式與之相似,其結構自己也十分簡單。
當你選用的容器或框架並不支持transaction timeout這一特性,你能夠考慮本身來實現。transaction timeout並無標準的API。Lucy框架的1.5和1.6版本都不支持transaction timeout,可是你能夠經過使用Spring的Transaction Manager來達到與之一樣的效果。
假設某個事務中包含5個statement,每一個statement的執行時間是200ms,其餘業務邏輯的執行時間是100ms,那麼transaction timeout至少應該設置爲1,100ms(200 * 5 + 100)。
什麼是Statement Timeout?
statement timeout用來限制statement的執行時長,timeout的值經過調用JDBC的java.sql.Statement.setQueryTimeout(int timeout) API進行設置。不過如今開發者已經不多直接在代碼中設置,而可能是經過框架來進行設置。
以iBatis爲例,statement timeout的默認值能夠經過sql-map-config.xml中的defaultStatementTimeout 屬性進行設置。同時,你還能夠設置sqlmap中select,insert,update標籤的timeout屬性,從而對不一樣sql語句的超時時間進行獨立的配置。
若是你使用的是Lucy1.5或1.6版本,經過設置queryTimeout屬性能夠在datasource層面對statement timeout進行設置。
statement timeout的具體值須要依據應用自己的特性而定,並無可供推薦的配置。
JDBC的statement timeout處理過程
不一樣的關係型數據庫,以及不一樣的JDBC驅動,其statement timeout處理過程會有所不一樣。其中,Oracle和MS SQLServer的處理相相似,MySQL和CUBRID相似。
Oracle JDBC Statement的QueryTimeout處理過程
1. 經過調用Connection的createStatement()方法建立statement
2. 調用Statement的executeQuery()方法
3. statement經過自身connection將query發送給Oracle數據庫
4. statement在OracleTimeoutPollingThread(每一個classloader一個)上進行註冊
5. 達到超時時間
6. OracleTimeoutPollingThread調用OracleStatement的cancel()方法
7. 經過connection向正在執行的query發送cancel消息
JTDS (MS SQLServer) Statement的QueryTimeout處理過程
1. 經過調用Connection的createStatement()方法建立statement
2. 調用Statement的executeQuery()方法
3. statement經過自身connection將query發送給MS SqlServer數據庫
4. statement在TimerThread上進行註冊
5. 達到超時時間
6. TimerThread調用JtdsStatement實例中的TsdCore.cancel()方法
7. 經過ConnectionJDBC向正在執行的query發送cancel消息
MySQL JDBC Statement的QueryTimeout處理過程
1. 經過調用Connection的createStatement()方法建立statement
2. 調用Statement的executeQuery()方法
3. statement經過自身connection將query發送給MySQL數據庫
4. statement建立一個新的timeout-execution線程用於超時處理
5. 5.1版本後改成每一個connection分配一個timeout-execution線程
6. 向timeout-execution線程進行註冊
7. 達到超時時間
6. TimerThread調用JtdsStatement實例中的TsdCore.cancel()方法
7. timeout-execution線程建立一個和statement配置相同的connection
8. 使用新建立的connection向超時query發送cancel query(KILL QUERY 「connectionId」)
CUBRID JDBC Statement的QueryTimeout處理過程
1. 經過調用Connection的createStatement()方法建立statement
2. 調用Statement的executeQuery()方法
3. statement經過自身connection將query發送給CUBRID數據庫
4. statement建立一個新的timeout-execution線程用於超時處理
5. 5.1版本後改成每一個connection分配一個timeout-execution線程
6. 向timeout-execution線程進行註冊
7. 達到超時時間
6. TimerThread調用JtdsStatement實例中的TsdCore.cancel()方法
7. timeout-execution線程建立一個和statement配置相同的connection
8. 使用新建立的connection向超時query發送cancel消息
什麼是JDBC的socket timeout?
第4種類型的JDBC使用socket與數據庫鏈接,數據庫並不對應用與數據庫間的鏈接超時進行處理。
JDBC的socket timeout在數據庫被忽然停掉或是發生網絡錯誤(因爲設備故障等緣由)時十分重要。因爲TCP/IP的結構緣由,socket沒有辦法探測到網絡錯誤,所以應用也沒法主動發現數據庫鏈接斷開。若是沒有設置socket timeout的話,應用在數據庫返回結果前會無期限地等下去,這種鏈接被稱爲dead connection。
爲了不dead connections,socket必需要有超時配置。socket timeout能夠經過JDBC設置,socket timeout可以避免應用在發生網絡錯誤時產生無休止等待的狀況,縮短服務失效的時間。
不推薦使用socket timeout來限制statement的執行時長,所以socket timeout的值必需要高於statement timeout,不然,socket timeout將會先生效,這樣statement timeout就變得毫無心義,也沒法生效。
下面展現了socket timeout的兩個設置項,不一樣的JDBC驅動其配置方式會有所不一樣。
經過查看CUBRID,MySQL,MS SQL Server (JTDS)和Oracle的JDBC驅動源碼,咱們發現全部的驅動內部都是使用上面的2個API來設置socket timeout的。
下面是不一樣驅動的socket timeout配置方式。
JDBC Driver | connectTimeout配置項 | socketTimeout配置項 | url格式 | 示例 |
MySQL Driver | connectTimeout(默認值:0,單位:ms) | socketTimeout(默認值:0,單位:ms) | jdbc:mysql://[host:port],[host:port]…/[database][?propertyName1][=propertyValue1][&propertyName2][=propertyValue2]… | jdbc:mysql://xxx.xx.xxx.xxx:3306/database?connectTimeout=60000&socketTimeout=60000 |
MS-SQL DriverjTDS Driver | loginTimeout(默認值:0,單位:s) | socketTimeout(默認值:0,單位:s) | jdbc:jtds:<server_type>://<server>[:<port>][/<database>][;<property>=<value>[;...]] | jdbc:jtds:sqlserver://server:port/database;loginTimeout=60;socketTimeout=60 |
Oracle Thin Driver | oracle.net.CONNECT_TIMEOUT (默認值:0,單位:ms) | oracle.jdbc.ReadTimeout(默認值:0,單位:ms) | 不支持經過url配置,只能經過OracleDatasource.setConnectionProperties() API設置,使用DBCP時能夠調用BasicDatasource.setConnectionProperties()或BasicDatasource.addConnectionProperties()進行設置 | |
CUBRID Thin Driver | 無獨立配置項(默認值:5,000,單位:ms) | 無獨立配置項(默認值:5,000,單位:ms) |
經過properties屬性進行配置時,須要傳入key爲「connectionProperties」的鍵值對,value的格式爲「[propertyName=property;]*」。下面是iBatis中的properties配置。
操做系統的socket timeout配置
若是不設置socket timeout或connect timeout,應用多數狀況下是沒法發現網絡錯誤的。所以,當網絡錯誤發生後,在鏈接從新鏈接成功或成功接收到數據以前,應用會無限制地等下去。可是,經過本文開篇處的實際案例咱們發現,30分鐘後應用的鏈接問題奇蹟般的解決了,這是由於操做系統一樣可以對socket timeout進行配置。公司的Linux服務器將socket timeout設置爲了30分鐘,從而會在操做系統的層面對網絡鏈接作校驗,所以即便JDBC的socket timeout設置爲0,由網絡錯誤形成的數據庫鏈接問題的持續時間也不會超過30分鐘。
一般,應用會在調用Socket.read()時因爲網絡問題被阻塞住,而不多在調用Socket.write()時進入waiting狀態,這取決於網絡構成和錯誤類型。當Socket.write()被調用時,數據被寫入到操做系統內核的緩衝區,控制權當即回到應用手上。所以,一旦數據被寫入內核緩衝區,Socket.write()調用就必然會成功。可是,若是系統內核緩衝區因爲某種網絡錯誤而滿了的話,Socket.write()也會進入waiting狀態。這種狀況下,操做系統會嘗試從新發包,當達到重試的時間限制時,將產生系統錯誤。在咱們公司,從新發包的超時時間被設置爲15分鐘。
至此,我已經對JDBC的內部操做作了講解,但願可以讓你們學會如何正確的配置超時時間,從而減小錯誤的發生。
最後,我將列出一些常見的問題。
FAQ
Q1. 我已經使用Statement.setQueryTimeout()方法設置了查詢超時,但在網絡出錯時並無產生做用。
➔ 查詢超時僅在socket timeout生效的前提下才有效,它並不能用來解決外部的網絡錯誤,要解決這種問題,必須設置JDBC的socket timeout。
Q2. transaction timeout,statement timeout和socket timeout和DBCP的配置有什麼關係?
➔ 當經過DBCP獲取數據庫鏈接時,除了DBCP獲取鏈接時的waitTimeout配置之外,其餘配置對JDBC沒有什麼影響。
Q3. 若是設置了JDBC的socket timeout,那DBCP鏈接池中處於IDLE狀態的鏈接是否也會在達到超時時間後被關閉?
➔ 不會。socket的設置只會在產生數據讀寫時生效,而不會對DBCP中的IDLE鏈接產生影響。當DBCP中發生新鏈接建立,老的IDLE鏈接被移除,或是鏈接有效性校驗的時候,socket設置會對其產生必定的影響,但除非發生網絡問題,不然影響很小。
Q4. socket timeout應該設置爲多少?
➔ 就像我在正文中提的那樣,socket timeout必須高於statement timeout,但並無什麼推薦值。在發生網絡錯誤的時候,socket timeout將會生效,可是再當心的配置也沒法避免網絡錯誤的發生,只是在網絡錯誤發生後縮短服務失效的時間(若是網絡恢復正常的話)。