系統(tǒng)中出現(xiàn)過(guò)幾次connection holder is null問(wèn)題,有的已解決,有的未解決,記錄如下。
首先說(shuō)先druid連接池的實(shí)現(xiàn):
DruidPooledConnection是一個(gè)靜態(tài)代理,持有ConnectionHolder, connection Holder里持有具體的connection對(duì)象, 在執(zhí)行druidPooledConnection的所有和數(shù)據(jù)庫(kù)相關(guān)方法時(shí),都會(huì)先調(diào)用checkState()判斷connection holder是否為null,如果是null就拋connection holder is null的異常。
那connection holder是什么時(shí)候賦值以及什么時(shí)候置成null的?
在Datasource.getConnection()獲取連接的時(shí)候,是從池里取出緩存的connection holder對(duì)象,druid是用一個(gè)數(shù)組緩存connection holder對(duì)象,每次都是從最后一個(gè)取,還的時(shí)候也是放到最后,這樣保證位于數(shù)組最后的連接會(huì)經(jīng)常處于使用狀態(tài),當(dāng)然這中間會(huì)有鎖的使用以及池里沒(méi)線(xiàn)程了通知任務(wù)線(xiàn)程去創(chuàng)建新連接。
Datasource.getConnection()從池里拿出connection holder后,然后new一個(gè)druidPooledConnection去包裝connection holder,所有每次看到都是不同的druidPooledConnection對(duì)象。
第一次:系統(tǒng)中事務(wù)執(zhí)行時(shí)間過(guò)長(zhǎng),超過(guò)60秒,后面導(dǎo)致有的請(qǐng)求會(huì)報(bào)connection holder is null。
拿出來(lái)的connection holder肯定不為null,項(xiàng)目中報(bào)connection holder is null,說(shuō)明是在使用過(guò)程中connection holder被置成null了,很大概率是被別的線(xiàn)程置成null了,因?yàn)楸揪€(xiàn)程只有在事務(wù)提交后還連接的時(shí)候才置null,在github issue上,作者也反復(fù)強(qiáng)調(diào)連接不要跨線(xiàn)程使用。而druid真的就有跨線(xiàn)程操作連接的地方,就是remove abandoned connection功能,這個(gè)功能是為了回收長(zhǎng)時(shí)間還沒(méi)還到池里的連接,多長(zhǎng)時(shí)間看你設(shè)置,而我們項(xiàng)目設(shè)置的60秒沒(méi)還就強(qiáng)制回收,這樣就會(huì)報(bào)上面的錯(cuò)誤了。
建議在生產(chǎn)環(huán)境關(guān)閉remove abandoned功能,如果數(shù)據(jù)庫(kù)負(fù)載不重的話(huà),可以開(kāi)啟testOnBorrow。 testWhileIde不建議開(kāi),因?yàn)椴l(fā)請(qǐng)求多的話(huà),數(shù)組后面的連接都不是idle狀態(tài),開(kāi)沒(méi)開(kāi)testWhileIdle沒(méi)啥區(qū)別。
第二次系統(tǒng)中有的事務(wù)長(zhǎng)時(shí)間未提交,DBA會(huì)把這個(gè)連接kill掉,后面請(qǐng)求會(huì)報(bào)conneciton holder is null
- 為什么有長(zhǎng)時(shí)間未提交的事務(wù),這個(gè)問(wèn)題還沒(méi)找到原因,從Mysql的innodb_trx和lock表里沒(méi)看到有價(jià)值線(xiàn)索,后面想跟蹤事務(wù)和連接來(lái)看看有沒(méi)有收獲。
- 連接被kill了,應(yīng)用端會(huì)報(bào)這樣的異常:
- Can not read response from server. Expected to read 4 bytes, read 0 bytes before connection was unexpectedly lost.
- com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure. The last packet successfully received from the server was 20,840 milliseconds ago. The last packet sent successfully to the server was 20,840 milliseconds ago.
- connection holder is null
前面2個(gè)異常很好理解,tcp連接斷了,應(yīng)用端讀不到數(shù)據(jù)報(bào)錯(cuò),然后druid捕獲到異常,要去判斷這個(gè)異常是可恢復(fù)異常還是不可恢復(fù)異常。因?yàn)檎驹谶B接池的角度來(lái)說(shuō),數(shù)據(jù)庫(kù)拋異常太普遍了,可能是唯一索引重復(fù)也可能是連接斷了,對(duì)于不同的異常處理方式也是不一樣的,唯一索引重復(fù)需要調(diào)用connection.rollback(),然后再把連接還到池里,因?yàn)檫@個(gè)連接還是好的,不影響下次繼續(xù)使用。而連接斷了,則要把這個(gè)連接踢出去,druid用了ExceptionSorter來(lái)判斷這個(gè)異常是不是不可恢復(fù)異常,在轉(zhuǎn)換異常的時(shí)候要用當(dāng)前連接獲取數(shù)據(jù)庫(kù)的metadata,而當(dāng)前連接已經(jīng)斷了,所以報(bào)connection holder is null。
但是這個(gè)connection holder is null只會(huì)報(bào)一次,和項(xiàng)目中大量報(bào)connection holder is null不是一個(gè)東西,目前還沒(méi)找到原因。而這個(gè)問(wèn)題在本地卻重現(xiàn)不了。
PS:數(shù)據(jù)庫(kù)有一個(gè)設(shè)置 rollback_on_timeout,默認(rèn)是off,這個(gè)值是說(shuō)當(dāng)事務(wù)超時(shí)(如超過(guò)50秒還沒(méi)獲取到鎖),默認(rèn)off是回滾最后一條sql語(yǔ)句,on是回滾整個(gè)事務(wù)。這個(gè)值一般不需要設(shè)置成on,交由應(yīng)用去處理,應(yīng)用在獲取不到 can't acquire lock的時(shí)候,一般會(huì)去調(diào)connection.rollback(),當(dāng)然前提是要你的應(yīng)用開(kāi)啟事務(wù)。