最近讓人頭疼的一個(gè)問題,就是服務(wù)器在不確定的時(shí)間點(diǎn)會(huì)出現(xiàn)關(guān)于數(shù)據(jù)庫連接的exception,大致的exception如下:
org.hibernate.util.JDBCExceptionReporter?-?SQL?Error:0,?SQLState:?08S01 org.hibernate.util.JDBCExceptionReporter?-?The?last?packet?successfully?received?from?the?server?was43200?milliseconds?ago. The?last?packet?sent?successfully?to?the?server?was?43200?milliseconds?ago,?which?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?'autoReconnect=true'?to?avoid?this?problem. org.hibernate.event.def.AbstractFlushingEventListener?-?Could?not?synchronize?database?state?with?session org.hibernate.exception.JDBCConnectionException:?Could?not?execute?JDBC?batch?update com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException:?Connection.close()?has?already?been?called.?Invalid?operation?in?this?state. org.hibernate.util.JDBCExceptionReporter?-?SQL?Error:0,?SQLState:?08003 org.hibernate.util.JDBCExceptionReporter?-?No?operations?allowed?after?connection?closed.?Connection?was?implicitly?closed?due?to?underlying?exception/error: **?BEGIN?NESTED?EXCEPTION?** com.mysql.jdbc.exceptions.jdbc4.CommunicationsException
先說一下發(fā)生這個(gè)Exception的大致原因:
MySQL的配置中,有一個(gè)叫做“wait_timeout”的參數(shù),這個(gè)參數(shù)大致的意思是這樣:當(dāng)一個(gè)客戶端連接到exception后,如果客戶端不自己斷開,也不做任何操作,MySQL數(shù)據(jù)庫會(huì)將這個(gè)連接保留”wait_timeout”這么長時(shí)間(單位是s,默認(rèn)是28800s,也就是8小時(shí)),超過這個(gè)時(shí)間之后,MySQL數(shù)據(jù)庫為了節(jié)省資源,就會(huì)在數(shù)據(jù)庫端斷開這個(gè)連接;當(dāng)然,在此過程中,如果客戶端在這個(gè)連接上有任意的操作,MySQL數(shù)據(jù)庫都會(huì)重新開始計(jì)算這個(gè)時(shí)間。
這么看來,發(fā)生上面Exception的原因就是因?yàn)槲业姆?wù)器和MySQL數(shù)據(jù)庫的連接超過了”wait_timeout”時(shí)間,MySQL服務(wù)器端將其斷開了,但是我的程序再次使用這個(gè)連接時(shí)沒有做任何判斷,所以就掛了。
那這個(gè)問題怎么解決呢?
在想解決方案的過程中,我發(fā)現(xiàn)了幾個(gè)讓我不著頭緒的問題:
第一個(gè)問題:我們的服務(wù)器曾經(jīng)在設(shè)計(jì)的過程中考慮過這個(gè)事情,所以服務(wù)器的主線程有一個(gè)定時(shí)的check機(jī)制,每隔半小時(shí)會(huì)發(fā)送一個(gè)”select 1″到數(shù)據(jù)庫來保證連接是活動(dòng)的,為什么這個(gè)check機(jī)制不起作用了呢?
第二個(gè)問題:從上面的Exception中可以得到這么一個(gè)信息:
The?last?packet?sent?successfully?to?the?server?was?43200?milliseconds?ago,? which?is?longer?than?the?server?configured?value?of?'wait_timeout'.
這個(gè)信息說的很明白,最后一個(gè)成功發(fā)到Server的包是43200毫秒之前。但是43200毫秒才43.2秒,也就是說我們的服務(wù)器43.2秒之前才和MySQL服務(wù)器通過信,怎么會(huì)發(fā)生超過”wait_timeout“的問題呢?而且MySQL數(shù)據(jù)庫的配置也確實(shí)是28800秒(8小時(shí)),這又是神馬情況呢?
在網(wǎng)上google了n長時(shí)間,倒是有不少關(guān)于這個(gè)問題的討論,但是一直沒有找到讓我覺得有效的方法,只能自己結(jié)合google到結(jié)果來慢慢琢磨了。
首先,MySQL數(shù)據(jù)庫那邊的解決方案很單一,就是延長”wait_timeout“的數(shù)值。我看有的人直接就延長到一年了,也有人說這個(gè)值最大也就是21天,即使值設(shè)的再大,MySQL也就只識(shí)別21天(這個(gè)我沒有具體去MySQL的文檔中去查)。但是這是一個(gè)治標(biāo)不治本的方法,即使可以一年,也還是會(huì)有斷的時(shí)候,服務(wù)器可是要7×24小時(shí)在線的呀。
既然MySQL數(shù)據(jù)庫那邊沒什么好方法,接下來就只能從程序這邊來搞了。
先說說程序這邊的大致結(jié)構(gòu)吧:兩個(gè)線程,一個(gè)線程負(fù)責(zé)查詢和上面說到的check機(jī)制,另一個(gè)線程負(fù)責(zé)定時(shí)更新數(shù)據(jù)庫,使用hibernate,配置很簡單,都是最基本的,沒有任何關(guān)于連接池和緩存的配置,就像下面這樣:
<session-factory> ????<property>org.hibernate.dialect.MySQL5InnoDBDialect</property> ????<property>com.mysql.jdbc.Driver</property> ????<property>true</property> ????<property>UTF-8</property> ????<property>true</property> ????<!-- 以下就全是mapping了,省略 --> </session-factory>
程序中更新的過程大致是這樣:
session?=?org.hibernate.SessionFactory.openSession(); transaction?=?session.beginTransaction(); session.update(something); transaction.commit(); session.close();
在這里,所有關(guān)于數(shù)據(jù)庫Connection的連接和關(guān)閉都在Hibernate中,因此,不去挖掘Hibernate的源碼是不可能了。
在挖掘Hibernate源碼之前,必須明確目標(biāo):挖掘什么?
其實(shí)我的目標(biāo)很明確,既然斷開連接是MySQL數(shù)據(jù)庫做的,那么相對(duì)于我們程序這邊的問題就是我們?cè)谑褂猛赀B接之后沒有調(diào)用Connection.close(),才會(huì)保留一個(gè)長連接在那里。那么,Hibernate是什么時(shí)候開啟這個(gè)連接,又什么時(shí)候調(diào)用Connection.close()的呢?
接下來就是Hibernate的源碼挖掘中。。。
枯燥的過程就不說了,說說挖掘出的東西:
Hibernate(忘了說了,我們用的Hibernate版本是3.3.2)在上面的那種配置之下,會(huì)有一個(gè)默認(rèn)的連接池,名字叫:DriverManagerConnectionProvider;這是一個(gè)極其簡單的連接池,默認(rèn)會(huì)在池中保留20個(gè)連接,這些連接不是一開始Hibernate初始化時(shí)就創(chuàng)建好的,而是在你需要使用連接時(shí)創(chuàng)建出來,使用完之后才加入到池中的。這里有一個(gè)叫closeConnection(Connection conn)的方法,這個(gè)方法很NB,它直接將傳入的連接不做任何處理,放到池中。而這個(gè)類內(nèi)部的連接池實(shí)際是一個(gè)ArrayList,每次取得時(shí)候remove掉ArrayList的第一個(gè)連接,用完后直接用add方法加入到ArrayList的最后。
我們的程序更新時(shí),Hibernate會(huì)通過DriverManagerConnectionProvider得到一個(gè)連接Connection,在使用完之后,調(diào)用session.close()時(shí),Hibernate會(huì)調(diào)用DriverManagerConnectionProvider的closeConnection方法(就是上面說的那個(gè)NB方法),這個(gè)時(shí)候,該連接會(huì)直接放到DriverManagerConnectionProvider的ArrayList中,從始至終也沒有地方去調(diào)用Connection的close方法。
說到這里,問題就很明顯了。
第一,我們的那個(gè)”select 1“的check機(jī)制和我們服務(wù)器程序中更新的邏輯是兩個(gè)線程,check機(jī)制工作時(shí),它會(huì)向DriverManagerConnectionProvider獲取一個(gè)連接,而此時(shí)更新邏輯工作時(shí),它會(huì)向DriverManagerConnectionProvider獲取另外一個(gè)連接,兩個(gè)邏輯工作完之后都會(huì)將自己獲得的連接放回DriverManagerConnectionProvider的池中,而且是放到那個(gè)池的末尾。這樣,check機(jī)制再想check這兩個(gè)連接就需要運(yùn)氣了,因?yàn)楦逻壿嫺峦曛缶桶堰B接放回池中了,而更新邏輯是定時(shí)的,check機(jī)制也是定時(shí)的,兩個(gè)定時(shí)機(jī)制如果總是能錯(cuò)開,那么check機(jī)制check的永遠(yuǎn)都是兩個(gè)中的一個(gè)連接,另外一個(gè)就麻煩了。這也就是為什么check機(jī)制不好使的原因。
第二,關(guān)于Exception信息中那個(gè)43200毫秒的問題也就能說明白了,check機(jī)制check的總是一個(gè)連接,而另外一個(gè)過期的連接被更新線程拿跑了,并且在check機(jī)制之后沒多久就有更新發(fā)生,43200毫秒恐怕就是它們之間的間隔吧。
到這里問題分析清楚了,怎么解決呢?
最容易想到的方案,也是網(wǎng)上說的最多的方案,就是延長MySQL端”wait_timeout“的時(shí)間。我說了,治標(biāo)不治本,我覺得不爽,不用。
第二個(gè)看到最多的就是用”autoReconnect = true”這個(gè)方案,郁悶的是MySQL 5之后的數(shù)據(jù)庫把這個(gè)功能給去了,說會(huì)有副作用(也沒具體說有啥副作用,我也懶得查),我們用的Hibernate 3.3.2這個(gè)版本也沒有autoReconnect這個(gè)功能了。
第三個(gè)說的最多的就是使用c3p0池了,況且Hibernate官網(wǎng)的文檔中也提到,默認(rèn)的那個(gè)連接池非常的屎,僅供測試使用,推薦使用c3p0(讓我郁悶的是我連c3p0的官網(wǎng)都沒找到,只在sourceForge上有個(gè)項(xiàng)目主頁)。好吧,我就決定用c3p0來搞定這個(gè)問題。
用c3p0解決這個(gè)Exception問題
首先很明了,只要是池它就肯定有這個(gè)問題,除非在放入池之前就把連接關(guān)閉,那池還頂個(gè)屁用。所以我參考的博客里說到,最好的方式就是在獲取連接時(shí)check一下,看看該連接是否還有效,即該Connection是否已經(jīng)被MySQL數(shù)據(jù)庫那邊給關(guān)了,如果關(guān)了就重連一個(gè)。因此,按照這個(gè)思路,我修正了Hibernate的exception,問題得到了解決:
<session-factory> ????<property>org.hibernate.dialect.MySQL5InnoDBDialect</property> ????<property>com.mysql.jdbc.Driver</property> ????<property>true</property> ????<property>UTF-8</property> ????<property>true</property> ????<!-- c3p0在我們使用的Hibernate版本中自帶,不用下載,直接使用 --> ????<property>org.hibernate.connection.C3P0ConnectionProvider</property> ????<property>5</property> ????<property>20</property> ????<property>1800</property> ????<property>50</property> ????<!-- 下面這句很重要,后面有解釋 --> ????<property>true</property> ????<!-- 以下就全是mapping了,省略 --> </session-factory>
上面配置中最重要的就是hibernate.c3p0.testConnectionOnCheckout這個(gè)屬性,它保證了我們前面說的每次取出連接時(shí)會(huì)檢查該連接是否被關(guān)閉了。不過這個(gè)屬性會(huì)對(duì)性能有一些損耗,引用我參考的博客上得話:程序能用是第一,之后才是它的性能(又不是不能容忍)。
當(dāng)然,c3p0自帶類似于select 1這樣的check機(jī)制,但是就像我說的,除非你將check機(jī)制的間隔時(shí)間把握的非常好,否則,問題是沒有解決的。
好了,至此,困擾我的問題解決完了。希望上面的這些整理可以為我以后碰到類似的問題留個(gè)思路,也可以為正在被此問題困擾的人提供一絲幫助