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


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

通過以上的配置分析得知,一個數(shù)據(jù)庫連接從連接池中借出后經(jīng)過21600s即6小時后會被強制回收,不會超過Mysql的默認8小時,而且也不存在這么長時間的事務(wù),所以不太可能是因為數(shù)據(jù)庫連接借出超時導(dǎo)致上面的錯誤,那么就是從數(shù)據(jù)庫連接池中申請的連接已經(jīng)超時了?似乎也不太可能,因為有檢查機制,即每隔30s就會檢查一次連接池中的連接是否超時,并且連接池中允許存在的空閑連接最大時間為540s。這就奇怪了,到底是什么原因?qū)е律厦娴腻e誤呢?這時注意到上述錯誤堆棧中的com.atomikos.datasource.pool.ConnectionPool.findOrWaitForAnAvailableConnection。是否問題的原因在于使用了Atomikos呢,帶著這樣的疑惑去閱讀了Druid和Atomikos相關(guān)的源碼。
由于Atomikos連接池是基于Druid連接池之上的,所以Atomikos新建和銷毀數(shù)據(jù)庫連接都是從Druid連接池中借出和歸還數(shù)據(jù)庫連接,而不是直接與數(shù)據(jù)庫交互,那么我們就來看看Druid是如何維持數(shù)據(jù)庫連接的。
public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
//初始化檢查配置和后臺線程
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()方法進行初始化工作,然后調(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ù)組中的最后一個元素,獲取到Holder后還需要將其封裝為DruidPooledConnection,這時該連接的connectedTimeMillis會被賦值為當前時間,這個時間在后續(xù)的分析中會非常重要。
因為配置了testWhileIdle為true,所以需要進行下面的有效性檢查,獲取該連接的上次活躍時間,得到空閑時間,如果超過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);
}同時,由于配置了removeAbandoned為true,所以需要檢查活躍連接是否超時,如果超時就斷開物理連接。下面看一下連接池的回收方法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();
}在對數(shù)據(jù)庫連接進行回收時,如果連接時間超過了數(shù)據(jù)庫的物理連接時間(默認8小時)則需要斷開物理連接,否則就調(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;
}注意上述標紅的地方,回收的這個連接的lastActiveTimeMillis被刷新為當前時間,這個時間也是非常重要的,在后續(xù)分析中會用到。
三、Atomikos框架
項目關(guān)于Atomikos的配置信息,如下所示

從上面的配置可以看出,atomikos連接池的最大連接數(shù)是25個,最小連接數(shù)是10個,連接最大的存活時間是500s,下面來看一下atomikos的源碼。
private void init() throws ConnectionPoolException
{
if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": initializing..." ); //如果連接池最小連接數(shù)沒有達到就新增數(shù)據(jù)庫連接
addConnectionsIfMinPoolSizeNotReached(); //開啟維持連接池平衡的線程
launchMaintenanceTimer();
}以上是Atomikos初始化的部分,先補充數(shù)據(jù)庫連接池達到最小連接數(shù),然后開啟后臺線程維持連接池的平衡。
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(); //如果達到了最大的存活時間就移除該連接
removeConnectionsThatExceededMaxLifetime(); //如果沒有滿足最小連接數(shù)就新增連接
addConnectionsIfMinPoolSizeNotReached(); //移除超過最小連接數(shù)以外的連接
removeIdleConnectionsIfMinPoolSizeExceeded();
}
});
TaskManager.SINGLETON.executeTask ( maintenanceTimer );
}在配置中,maintenanceInterval的值為30,即每個30秒執(zhí)行一次上述的四個方法,主要看一下removeConnectionsThatExceededMaxLifetime()這個方法。
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 ); //如果超過最大的存活時間就銷毀該連接
destroyPooledConnection(xpc);
it.remove();
}
}
logCurrentPoolSize();
}上述方法遍歷數(shù)據(jù)庫連接池中的所有連接,如果存活時間超過maxLifetime即500s就銷毀該連接,這時由于連接池中的連接數(shù)就小于minPoolSize,所以會立即補充新的連接到連接池中。那么,系統(tǒng)在夜間沒有用戶使用時,Atomikos連接池的運行狀態(tài)為:維持最小的連接數(shù)10個數(shù)據(jù)庫連接,當這10個連接超過500s時就會銷毀,再重新創(chuàng)建10個新的數(shù)據(jù)庫連接,不斷重復(fù)這樣的操作。
四、分析與總結(jié)
下面我們開始分析產(chǎn)生錯誤日志的原因,當沒有用戶使用系統(tǒng)時,Druid連接池應(yīng)該有10個空閑的連接,Atomikos連接池也有10個空閑的連接,這時Atomikos的10個連接達到了最大的生存時間500s,就需要銷毀這些連接,對于Druid來說就是回收連接,調(diào)用recycle方法。由于這10個連接應(yīng)該是500s之前從Druid連接池借出的,所以它們的connectTimeMillis也是500s之前的時間,即物理連接時間肯定小于8小時,可以成功回收到Druid連接池中,同時lastActiveTimeMillis也更新為當前時間,放在connections數(shù)組的末尾。
與此同時,Atomikos還需要重新生成10個新的連接,即從Druid連接池獲取10個連接,調(diào)用getConnection方法,這時會進行有效性的檢查,又因為lastActiveTimeMillis基本上為當前時間,所以idleMillis肯定比30s小,不需要進行select 1的連接數(shù)據(jù)庫操作,這樣即使該連接已經(jīng)失效了還是會借出給Atomikos。每隔500s不斷循環(huán)上述操作,并且期間沒有用戶的操作,一旦超過8個小時的Mysql連接時間,Atomikos在使用數(shù)據(jù)庫連接時就會產(chǎn)生上述日志中的錯誤了。

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

