詳解springboot+atomikos+druid?數(shù)據(jù)庫(kù)連接失效分析
一、起因
最近查看系統(tǒng)的后臺(tái)日志,經(jīng)常發(fā)現(xiàn)這樣的報(bào)錯(cuò)信息:The last package successfully received from the server was 40802382 milliseconds ago,截圖如下所示。


由于我們的系統(tǒng)都是在白天使用,夜里基本上沒(méi)有用戶(hù)使用,再加上以上的報(bào)錯(cuò)信息都是出現(xiàn)在早晨,結(jié)合錯(cuò)誤日志初步分析,應(yīng)該是數(shù)據(jù)庫(kù)連接超時(shí)自動(dòng)斷開(kāi)了。百度一番后,得知Mysql的默認(rèn)連接時(shí)間是8小時(shí),超過(guò)8小時(shí)沒(méi)有操作后就會(huì)自動(dòng)斷開(kāi)連接,但是已經(jīng)使用了druid數(shù)據(jù)庫(kù)連接池,按理說(shuō)已經(jīng)對(duì)數(shù)據(jù)庫(kù)連接做了保護(hù)和檢查,不應(yīng)該出現(xiàn)這樣的問(wèn)題。要想徹底弄明白這個(gè)問(wèn)題,就只能去研究druid數(shù)據(jù)庫(kù)連接池框架了。
二、Druid數(shù)據(jù)庫(kù)連接池
項(xiàng)目的數(shù)據(jù)庫(kù)連接池基本配置信息如下所示

通過(guò)以上的配置分析得知,一個(gè)數(shù)據(jù)庫(kù)連接從連接池中借出后經(jīng)過(guò)21600s即6小時(shí)后會(huì)被強(qiáng)制回收,不會(huì)超過(guò)Mysql的默認(rèn)8小時(shí),而且也不存在這么長(zhǎng)時(shí)間的事務(wù),所以不太可能是因?yàn)閿?shù)據(jù)庫(kù)連接借出超時(shí)導(dǎo)致上面的錯(cuò)誤,那么就是從數(shù)據(jù)庫(kù)連接池中申請(qǐng)的連接已經(jīng)超時(shí)了?似乎也不太可能,因?yàn)橛袡z查機(jī)制,即每隔30s就會(huì)檢查一次連接池中的連接是否超時(shí),并且連接池中允許存在的空閑連接最大時(shí)間為540s。這就奇怪了,到底是什么原因?qū)е律厦娴腻e(cuò)誤呢?這時(shí)注意到上述錯(cuò)誤堆棧中的com.atomikos.datasource.pool.ConnectionPool.findOrWaitForAnAvailableConnection。是否問(wèn)題的原因在于使用了Atomikos呢,帶著這樣的疑惑去閱讀了Druid和Atomikos相關(guān)的源碼。
由于Atomikos連接池是基于Druid連接池之上的,所以Atomikos新建和銷(xiāo)毀數(shù)據(jù)庫(kù)連接都是從Druid連接池中借出和歸還數(shù)據(jù)庫(kù)連接,而不是直接與數(shù)據(jù)庫(kù)交互,那么我們就來(lái)看看Druid是如何維持?jǐn)?shù)據(jù)庫(kù)連接的。
public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
//初始化檢查配置和后臺(tái)線程
init();
if (filters.size() > 0) {
FilterChainImpl filterChain = new FilterChainImpl(this);
return filterChain.dataSource_connect(this, maxWaitMillis);
} else {
return getConnectionDirect(maxWaitMillis);
}
}從Druid連接池中獲取數(shù)據(jù)庫(kù)連接,先調(diào)用init()方法進(jìn)行初始化工作,然后調(diào)用getConnectionDirect()獲取連接。
decrementPoolingCount(); DruidConnectionHolder last = connections[poolingCount]; connections[poolingCount] = null;
DruidPooledConnection poolalbeConnection = new DruidPooledConnection(holder);
public DruidPooledConnection(DruidConnectionHolder holder){
super(holder.getConnection());
this.conn = holder.getConnection();
this.holder = holder;
this.lock = holder.lock;
dupCloseLogEnable = holder.getDataSource().isDupCloseLogEnable();
ownerThread = Thread.currentThread();
connectedTimeMillis = System.currentTimeMillis();
}上述是獲取連接池中連接的關(guān)鍵代碼,即獲取connections數(shù)組中的最后一個(gè)元素,獲取到Holder后還需要將其封裝為DruidPooledConnection,這時(shí)該連接的connectedTimeMillis會(huì)被賦值為當(dāng)前時(shí)間,這個(gè)時(shí)間在后續(xù)的分析中會(huì)非常重要。
因?yàn)榕渲昧藅estWhileIdle為true,所以需要進(jìn)行下面的有效性檢查,獲取該連接的上次活躍時(shí)間,得到空閑時(shí)間,如果超過(guò)30s則做有效性檢查。
long idleMillis = currentTimeMillis - lastActiveTimeMillis;
long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
if (timeBetweenEvictionRunsMillis <= 0) {
timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
}
if (idleMillis >= timeBetweenEvictionRunsMillis
|| idleMillis < 0 // unexcepted branch
) {
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
discardConnection(poolableConnection.holder);
continue;
}
}long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000);
if (timeMillis >= removeAbandonedTimeoutMillis) {
iter.remove();
pooledConnection.setTraceEnable(false);
abandonedList.add(pooledConnection);
}同時(shí),由于配置了removeAbandoned為true,所以需要檢查活躍連接是否超時(shí),如果超時(shí)就斷開(kāi)物理連接。下面看一下連接池的回收方法recycle的關(guān)鍵代碼
if (phyTimeoutMillis > 0) {
long phyConnectTimeMillis = currentTimeMillis - holder.connectTimeMillis;
if (phyConnectTimeMillis > phyTimeoutMillis) {
discardConnection(holder);
return;
}
}
lock.lock();
try {
if (holder.active) {
activeCount--;
holder.active = false;
}
closeCount++;
result = putLast(holder, currentTimeMillis);
recycleCount++;
} finally {
lock.unlock();
}在對(duì)數(shù)據(jù)庫(kù)連接進(jìn)行回收時(shí),如果連接時(shí)間超過(guò)了數(shù)據(jù)庫(kù)的物理連接時(shí)間(默認(rèn)8小時(shí))則需要斷開(kāi)物理連接,否則就調(diào)用putLast方法將該連接回收到連接池。
boolean putLast(DruidConnectionHolder e, long lastActiveTimeMillis) {
if (poolingCount >= maxActive || e.discard) {
return false;
}
e.lastActiveTimeMillis = lastActiveTimeMillis;
connections[poolingCount] = e;
incrementPoolingCount();
if (poolingCount > poolingPeak) {
poolingPeak = poolingCount;
poolingPeakTime = lastActiveTimeMillis;
}
notEmpty.signal();
notEmptySignalCount++;
return true;
}注意上述標(biāo)紅的地方,回收的這個(gè)連接的lastActiveTimeMillis被刷新為當(dāng)前時(shí)間,這個(gè)時(shí)間也是非常重要的,在后續(xù)分析中會(huì)用到。
三、Atomikos框架
項(xiàng)目關(guān)于Atomikos的配置信息,如下所示

從上面的配置可以看出,atomikos連接池的最大連接數(shù)是25個(gè),最小連接數(shù)是10個(gè),連接最大的存活時(shí)間是500s,下面來(lái)看一下atomikos的源碼。
private void init() throws ConnectionPoolException
{
if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": initializing..." ); //如果連接池最小連接數(shù)沒(méi)有達(dá)到就新增數(shù)據(jù)庫(kù)連接
addConnectionsIfMinPoolSizeNotReached(); //開(kāi)啟維持連接池平衡的線程
launchMaintenanceTimer();
}以上是Atomikos初始化的部分,先補(bǔ)充數(shù)據(jù)庫(kù)連接池達(dá)到最小連接數(shù),然后開(kāi)啟后臺(tái)線程維持連接池的平衡。
private void launchMaintenanceTimer() {
int maintenanceInterval = properties.getMaintenanceInterval();
if ( maintenanceInterval <= 0 ) {
if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": using default maintenance interval..." );
maintenanceInterval = DEFAULT_MAINTENANCE_INTERVAL;
}
maintenanceTimer = new PooledAlarmTimer ( maintenanceInterval * 1000 );
maintenanceTimer.addAlarmTimerListener(new AlarmTimerListener() {
public void alarm(AlarmTimer timer) {
reapPool(); //如果達(dá)到了最大的存活時(shí)間就移除該連接
removeConnectionsThatExceededMaxLifetime(); //如果沒(méi)有滿(mǎn)足最小連接數(shù)就新增連接
addConnectionsIfMinPoolSizeNotReached(); //移除超過(guò)最小連接數(shù)以外的連接
removeIdleConnectionsIfMinPoolSizeExceeded();
}
});
TaskManager.SINGLETON.executeTask ( maintenanceTimer );
}在配置中,maintenanceInterval的值為30,即每個(gè)30秒執(zhí)行一次上述的四個(gè)方法,主要看一下removeConnectionsThatExceededMaxLifetime()這個(gè)方法。
private synchronized void removeConnectionsThatExceededMaxLifetime()
{
long maxLifetime = properties.getMaxLifetime();
if ( connections == null || maxLifetime <= 0 ) return;
if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": closing connections that exceeded maxLifetime" );
Iterator<XPooledConnection> it = connections.iterator();
while ( it.hasNext() ) {
XPooledConnection xpc = it.next();
long creationTime = xpc.getCreationTime();
long now = System.currentTimeMillis();
if ( xpc.isAvailable() && ( (now - creationTime) >= (maxLifetime * 1000L) ) ) {
if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": connection in use for more than " + maxLifetime + "s, destroying it: " + xpc ); //如果超過(guò)最大的存活時(shí)間就銷(xiāo)毀該連接
destroyPooledConnection(xpc);
it.remove();
}
}
logCurrentPoolSize();
}上述方法遍歷數(shù)據(jù)庫(kù)連接池中的所有連接,如果存活時(shí)間超過(guò)maxLifetime即500s就銷(xiāo)毀該連接,這時(shí)由于連接池中的連接數(shù)就小于minPoolSize,所以會(huì)立即補(bǔ)充新的連接到連接池中。那么,系統(tǒng)在夜間沒(méi)有用戶(hù)使用時(shí),Atomikos連接池的運(yùn)行狀態(tài)為:維持最小的連接數(shù)10個(gè)數(shù)據(jù)庫(kù)連接,當(dāng)這10個(gè)連接超過(guò)500s時(shí)就會(huì)銷(xiāo)毀,再重新創(chuàng)建10個(gè)新的數(shù)據(jù)庫(kù)連接,不斷重復(fù)這樣的操作。
四、分析與總結(jié)
下面我們開(kāi)始分析產(chǎn)生錯(cuò)誤日志的原因,當(dāng)沒(méi)有用戶(hù)使用系統(tǒng)時(shí),Druid連接池應(yīng)該有10個(gè)空閑的連接,Atomikos連接池也有10個(gè)空閑的連接,這時(shí)Atomikos的10個(gè)連接達(dá)到了最大的生存時(shí)間500s,就需要銷(xiāo)毀這些連接,對(duì)于Druid來(lái)說(shuō)就是回收連接,調(diào)用recycle方法。由于這10個(gè)連接應(yīng)該是500s之前從Druid連接池借出的,所以它們的connectTimeMillis也是500s之前的時(shí)間,即物理連接時(shí)間肯定小于8小時(shí),可以成功回收到Druid連接池中,同時(shí)lastActiveTimeMillis也更新為當(dāng)前時(shí)間,放在connections數(shù)組的末尾。
與此同時(shí),Atomikos還需要重新生成10個(gè)新的連接,即從Druid連接池獲取10個(gè)連接,調(diào)用getConnection方法,這時(shí)會(huì)進(jìn)行有效性的檢查,又因?yàn)閘astActiveTimeMillis基本上為當(dāng)前時(shí)間,所以idleMillis肯定比30s小,不需要進(jìn)行select 1的連接數(shù)據(jù)庫(kù)操作,這樣即使該連接已經(jīng)失效了還是會(huì)借出給Atomikos。每隔500s不斷循環(huán)上述操作,并且期間沒(méi)有用戶(hù)的操作,一旦超過(guò)8個(gè)小時(shí)的Mysql連接時(shí)間,Atomikos在使用數(shù)據(jù)庫(kù)連接時(shí)就會(huì)產(chǎn)生上述日志中的錯(cuò)誤了。

綜上所述,導(dǎo)致報(bào)錯(cuò)的原因其實(shí)是使用了兩層數(shù)據(jù)庫(kù)連接池,這樣Druid連接池借出的數(shù)據(jù)庫(kù)連接并沒(méi)有被實(shí)際使用,這才導(dǎo)致這些數(shù)據(jù)庫(kù)連接成功躲避了Druid本身的檢查機(jī)制。
到此這篇關(guān)于springboot+atomikos+druid 數(shù)據(jù)庫(kù)連接失效分析的文章就介紹到這了,更多相關(guān)springboot+atomikos+druid 數(shù)據(jù)庫(kù)連接失效分析內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springboot 中 druid+jpa+MYSQL數(shù)據(jù)庫(kù)配置過(guò)程
- Springboot2 集成 druid 加密數(shù)據(jù)庫(kù)密碼的配置方法
- springboot項(xiàng)目整合druid數(shù)據(jù)庫(kù)連接池的實(shí)現(xiàn)
- 如何在SpringBoot 中使用 Druid 數(shù)據(jù)庫(kù)連接池
- springboot 整合druid數(shù)據(jù)庫(kù)密碼加密功能的實(shí)現(xiàn)代碼
- Springboot Druid 自定義加密數(shù)據(jù)庫(kù)密碼的幾種方案
- SpringBoot整合Druid數(shù)據(jù)庫(kù)連接池的方法
- SpringBoot開(kāi)發(fā)案例之配置Druid數(shù)據(jù)庫(kù)連接池的示例
相關(guān)文章
SpringSecurity+JWT實(shí)現(xiàn)登錄流程分析
Spring Security 是一個(gè)功能強(qiáng)大且高度可定制的身份驗(yàn)證和訪問(wèn)控制框架,它是為Java應(yīng)用程序設(shè)計(jì)的,特別是那些基于Spring的應(yīng)用程序,下面給大家介紹SpringSecurity+JWT實(shí)現(xiàn)登錄流程,感興趣的朋友一起看看吧2024-12-12
selenium-java實(shí)現(xiàn)自動(dòng)登錄跳轉(zhuǎn)頁(yè)面方式
利用Selenium和Java語(yǔ)言可以編寫(xiě)一個(gè)腳本自動(dòng)刷新網(wǎng)頁(yè),首先,需要確保Google瀏覽器和Chrome-Driver驅(qū)動(dòng)的版本一致,通過(guò)指定網(wǎng)站下載對(duì)應(yīng)版本的瀏覽器和驅(qū)動(dòng),在Maven項(xiàng)目中添加依賴(lài),編寫(xiě)腳本實(shí)現(xiàn)網(wǎng)頁(yè)的自動(dòng)刷新,此方法適用于需要頻繁刷新網(wǎng)頁(yè)的場(chǎng)景,簡(jiǎn)化了操作,提高了效率2024-11-11
最流行的java后臺(tái)框架spring quartz定時(shí)任務(wù)
近日項(xiàng)目開(kāi)發(fā)中需要執(zhí)行一些定時(shí)任務(wù),比如需要在每天凌晨時(shí)候,分析一次前一天的日志信息,借此機(jī)會(huì)整理了一下定時(shí)任務(wù)的幾種實(shí)現(xiàn)方式,由于項(xiàng)目采用spring框架,所以我都將結(jié)合spring框架來(lái)介紹2015-12-12
springboot rabbitmq整合rabbitmq之消息持久化存儲(chǔ)問(wèn)題
這篇文章主要介紹了springboot rabbitmq整合rabbitmq之消息持久化存儲(chǔ)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09
Java通過(guò)JsApi方式實(shí)現(xiàn)微信支付
本文講解了Java如何實(shí)現(xiàn)JsApi方式的微信支付,代碼內(nèi)容詳細(xì),文章思路清晰,需要的朋友可以參考下2015-07-07
springboot mybatis druid配置多數(shù)據(jù)源教程
這篇文章主要介紹了springboot mybatis druid配置多數(shù)據(jù)源教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
SpringBoot中實(shí)現(xiàn)分布式的Session共享的詳細(xì)教程
這篇文章主要介紹了SpringBoot中實(shí)現(xiàn)分布式的Session共享,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06
SpringBoot常用數(shù)據(jù)庫(kù)開(kāi)發(fā)技術(shù)匯總介紹
Spring Boot常用的數(shù)據(jù)庫(kù)開(kāi)發(fā)技術(shù)有JDBCTemplate、JPA和Mybatis,它們分別具有不同的特點(diǎn)和適用場(chǎng)景,可以根據(jù)具體的需求選擇合適的技術(shù)來(lái)進(jìn)行開(kāi)發(fā)2023-04-04
eclipse下搭建hibernate5.0環(huán)境的步驟(圖文)
這篇文章主要介紹了eclipse下搭建hibernate5.0環(huán)境的步驟(圖文),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05

