詳解springboot+atomikos+druid?數(shù)據(jù)庫連接失效分析
一、起因
最近查看系統(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)都是在白天使用,夜里基本上沒有用戶使用,再加上以上的報(bào)錯(cuò)信息都是出現(xiàn)在早晨,結(jié)合錯(cuò)誤日志初步分析,應(yīng)該是數(shù)據(jù)庫連接超時(shí)自動(dòng)斷開了。百度一番后,得知Mysql的默認(rèn)連接時(shí)間是8小時(shí),超過8小時(shí)沒有操作后就會(huì)自動(dòng)斷開連接,但是已經(jīng)使用了druid數(shù)據(jù)庫連接池,按理說已經(jīng)對(duì)數(shù)據(jù)庫連接做了保護(hù)和檢查,不應(yīng)該出現(xiàn)這樣的問題。要想徹底弄明白這個(gè)問題,就只能去研究druid數(shù)據(jù)庫連接池框架了。
二、Druid數(shù)據(jù)庫連接池
項(xiàng)目的數(shù)據(jù)庫連接池基本配置信息如下所示
通過以上的配置分析得知,一個(gè)數(shù)據(jù)庫連接從連接池中借出后經(jīng)過21600s即6小時(shí)后會(huì)被強(qiáng)制回收,不會(huì)超過Mysql的默認(rèn)8小時(shí),而且也不存在這么長時(shí)間的事務(wù),所以不太可能是因?yàn)閿?shù)據(jù)庫連接借出超時(shí)導(dǎo)致上面的錯(cuò)誤,那么就是從數(shù)據(jù)庫連接池中申請(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。是否問題的原因在于使用了Atomikos呢,帶著這樣的疑惑去閱讀了Druid和Atomikos相關(guān)的源碼。
由于Atomikos連接池是基于Druid連接池之上的,所以Atomikos新建和銷毀數(shù)據(jù)庫連接都是從Druid連接池中借出和歸還數(shù)據(jù)庫連接,而不是直接與數(shù)據(jù)庫交互,那么我們就來看看Druid是如何維持?jǐn)?shù)據(jù)庫連接的。
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ù)庫連接,先調(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í)間,如果超過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í)就斷開物理連接。下面看一下連接池的回收方法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ù)庫連接進(jìn)行回收時(shí),如果連接時(shí)間超過了數(shù)據(jù)庫的物理連接時(shí)間(默認(rèn)8小時(shí))則需要斷開物理連接,否則就調(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,下面來看一下atomikos的源碼。
private void init() throws ConnectionPoolException { if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": initializing..." ); //如果連接池最小連接數(shù)沒有達(dá)到就新增數(shù)據(jù)庫連接 addConnectionsIfMinPoolSizeNotReached(); //開啟維持連接池平衡的線程 launchMaintenanceTimer(); }
以上是Atomikos初始化的部分,先補(bǔ)充數(shù)據(jù)庫連接池達(dá)到最小連接數(shù),然后開啟后臺(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(); //如果沒有滿足最小連接數(shù)就新增連接 addConnectionsIfMinPoolSizeNotReached(); //移除超過最小連接數(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 ); //如果超過最大的存活時(shí)間就銷毀該連接 destroyPooledConnection(xpc); it.remove(); } } logCurrentPoolSize(); }
上述方法遍歷數(shù)據(jù)庫連接池中的所有連接,如果存活時(shí)間超過maxLifetime即500s就銷毀該連接,這時(shí)由于連接池中的連接數(shù)就小于minPoolSize,所以會(huì)立即補(bǔ)充新的連接到連接池中。那么,系統(tǒng)在夜間沒有用戶使用時(shí),Atomikos連接池的運(yùn)行狀態(tài)為:維持最小的連接數(shù)10個(gè)數(shù)據(jù)庫連接,當(dāng)這10個(gè)連接超過500s時(shí)就會(huì)銷毀,再重新創(chuàng)建10個(gè)新的數(shù)據(jù)庫連接,不斷重復(fù)這樣的操作。
四、分析與總結(jié)
下面我們開始分析產(chǎn)生錯(cuò)誤日志的原因,當(dāng)沒有用戶使用系統(tǒng)時(shí),Druid連接池應(yīng)該有10個(gè)空閑的連接,Atomikos連接池也有10個(gè)空閑的連接,這時(shí)Atomikos的10個(gè)連接達(dá)到了最大的生存時(shí)間500s,就需要銷毀這些連接,對(duì)于Druid來說就是回收連接,調(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ù)庫操作,這樣即使該連接已經(jīng)失效了還是會(huì)借出給Atomikos。每隔500s不斷循環(huán)上述操作,并且期間沒有用戶的操作,一旦超過8個(gè)小時(shí)的Mysql連接時(shí)間,Atomikos在使用數(shù)據(jù)庫連接時(shí)就會(huì)產(chǎn)生上述日志中的錯(cuò)誤了。
綜上所述,導(dǎo)致報(bào)錯(cuò)的原因其實(shí)是使用了兩層數(shù)據(jù)庫連接池,這樣Druid連接池借出的數(shù)據(jù)庫連接并沒有被實(shí)際使用,這才導(dǎo)致這些數(shù)據(jù)庫連接成功躲避了Druid本身的檢查機(jī)制。
到此這篇關(guān)于springboot+atomikos+druid 數(shù)據(jù)庫連接失效分析的文章就介紹到這了,更多相關(guān)springboot+atomikos+druid 數(shù)據(jù)庫連接失效分析內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springboot 中 druid+jpa+MYSQL數(shù)據(jù)庫配置過程
- Springboot2 集成 druid 加密數(shù)據(jù)庫密碼的配置方法
- springboot項(xiàng)目整合druid數(shù)據(jù)庫連接池的實(shí)現(xiàn)
- 如何在SpringBoot 中使用 Druid 數(shù)據(jù)庫連接池
- springboot 整合druid數(shù)據(jù)庫密碼加密功能的實(shí)現(xiàn)代碼
- Springboot Druid 自定義加密數(shù)據(jù)庫密碼的幾種方案
- SpringBoot整合Druid數(shù)據(jù)庫連接池的方法
- SpringBoot開發(fā)案例之配置Druid數(shù)據(jù)庫連接池的示例
相關(guān)文章
關(guān)于重寫equals()方法和hashCode()方法及其簡單的應(yīng)用
這篇文章主要介紹了關(guān)于重寫equals()方法和hashCode()方法及其簡單的應(yīng)用,網(wǎng)上的知識(shí)有些可能是錯(cuò)誤的,關(guān)于?equals()?方法的理解,大家討論不一樣,需要的朋友可以參考下2023-04-04Spring Boot實(shí)現(xiàn)簡單的定時(shí)任務(wù)
這篇文章主要給大家介紹了關(guān)于利用Spring Boot實(shí)現(xiàn)簡單的定時(shí)任務(wù)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07Java Swing GridBagLayout網(wǎng)格袋布局的實(shí)現(xiàn)
這篇文章主要介紹了Java Swing GridBagLayout網(wǎng)格袋布局的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12spring自定義注解實(shí)現(xiàn)攔截器的實(shí)現(xiàn)方法
本篇文章主要介紹了spring自定義注解實(shí)現(xiàn)攔截器的實(shí)現(xiàn)方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08