DruidDataSource#getConnection
@Override
public DruidPooledConnection getConnection() throws SQLException {
return getConnection(maxWait);
}
public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
//初始化 所以init方法是第一次獲取連接的時候才會初始化
init();
if (filters.size() > 0) {
// 如果有攔截器 攔截器鏈 責(zé)任鏈模式執(zhí)行所有的攔截
FilterChainImpl filterChain = new FilterChainImpl(this);
// 攔截器獲取數(shù)據(jù)庫連接代碼詳解 , 應(yīng)該也是執(zhí)行完攔截后獲取連接
return filterChain.dataSource_connect(this, maxWaitMillis);
} else {
// 直接獲取連接
return getConnectionDirect(maxWaitMillis);
}
}
//直接獲取連接
public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
//超時重試的次數(shù)
int notFullTimeoutRetryCnt = 0;
for (;;) {
// handle notFullTimeoutRetry
DruidPooledConnection poolableConnection;
try {
//獲取連接的內(nèi)部方法
poolableConnection = getConnectionInternal(maxWaitMillis);
} catch (GetConnectionTimeoutException ex) {
if (notFullTimeoutRetryCnt <= this.notFullTimeoutRetryCount && !isFull()) {
//重試次數(shù)超過日志打印
notFullTimeoutRetryCnt++;
if (LOG.isWarnEnabled()) {
LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
}
continue;
}
throw ex;
}
if (testOnBorrow) {
// 測試連接
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
//驗(yàn)證異常
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
//關(guān)閉連接
discardConnection(poolableConnection.holder);
continue;
}
} else {
if (poolableConnection.conn.isClosed()) {
discardConnection(poolableConnection.holder); // 傳入null,避免重復(fù)關(guān)閉
continue;
}
// todo 不太能理解 既然已經(jīng)在init的時候已經(jīng)啟動了守護(hù)線程createAndStartDestroyThread去進(jìn)行空閑線程和超時線程的定時掃描和回收,為什么在獲取連接的時候還要重新處理一次呢???
if (testWhileIdle) {
//空閑檢測有效性
final DruidConnectionHolder holder = poolableConnection.holder;
long currentTimeMillis = System.currentTimeMillis();
long lastActiveTimeMillis = holder.lastActiveTimeMillis;
long lastExecTimeMillis = holder.lastExecTimeMillis;
long lastKeepTimeMillis = holder.lastKeepTimeMillis;
//更新最近活躍時間
if (checkExecuteTime
&& lastExecTimeMillis != lastActiveTimeMillis) {
lastActiveTimeMillis = lastExecTimeMillis;
}
if (lastKeepTimeMillis > lastActiveTimeMillis) {
lastActiveTimeMillis = lastKeepTimeMillis;
}
//存活時間
long idleMillis = currentTimeMillis - lastActiveTimeMillis;
long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
if (timeBetweenEvictionRunsMillis <= 0) {
//運(yùn)行時間間隔設(shè)置為默認(rèn)時間
timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
}
if (idleMillis >= timeBetweenEvictionRunsMillis
|| idleMillis < 0 // unexcepted branch
) {
//校驗(yàn)是否有效
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
//關(guān)閉連接
discardConnection(poolableConnection.holder);
continue;
}
}
}
}
if (removeAbandoned) {
//當(dāng)前連接
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
poolableConnection.connectStackTrace = stackTrace;
poolableConnection.setConnectedTimeNano();
poolableConnection.traceEnable = true;
activeConnectionLock.lock();
try {
//活躍的連接
activeConnections.put(poolableConnection, PRESENT);
} finally {
activeConnectionLock.unlock();
}
}
if (!this.defaultAutoCommit) {
//自動提交
poolableConnection.setAutoCommit(false);
}
return poolableConnection;
}
druid中找到相關(guān)代碼,查看報錯相關(guān)的變量的邏輯
if (validConnectionChecker != null) {
//校驗(yàn)連接狀態(tài),其中的validationQuery就是我們要執(zhí)行的sql語句,validationQueryTimeOut表示執(zhí)行sql的超時時間,valid表示是否檢驗(yàn)成功或者正常響應(yīng)
boolean valid = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);
//當(dāng)前系統(tǒng)時間
long currentTimeMillis = System.currentTimeMillis();
if (holder != null) {
holder.lastValidTimeMillis = currentTimeMillis;
holder.lastExecTimeMillis = currentTimeMillis;
}
//如果連接校驗(yàn)成功,并且是mysql數(shù)據(jù)庫的話
if (valid && isMySql) { // unexcepted branch
//獲取最近一次查詢數(shù)據(jù)庫接受數(shù)據(jù)包的時間
long lastPacketReceivedTimeMs = MySqlUtils.getLastPacketReceivedTimeMs(conn);
if (lastPacketReceivedTimeMs > 0) {
//當(dāng)前時間與最后一次接受數(shù)據(jù)的時間差
long mysqlIdleMillis = currentTimeMillis - lastPacketReceivedTimeMs;
//如果時間差大于druid的配置timeBetweenEvictionRunsMislis就開始報錯。
//timeBetweenEvictionRunsMislis的默認(rèn)時間是60秒
if (lastPacketReceivedTimeMs > 0
&& mysqlIdleMillis >= timeBetweenEvictionRunsMillis) {
discardConnection(holder);
String errorMsg = "discard long time none received connection. "
+ ", jdbcUrl : " + jdbcUrl
+ ", jdbcUrl : " + jdbcUrl
+ ", lastPacketReceivedIdleMillis : " + mysqlIdleMillis;
LOG.error(errorMsg);
return false;
}
}
}
通過上述分析,我們要立馬就知道,我們要想不報錯就需要將timeBetweenEvictionRunsMislis的時間變大。于是我將這個值改大了再試試,結(jié)果還是在報錯。
主要是lastPacketReceivedTimeMs 代表的是最后一次接受數(shù)據(jù)包的時間。顯然這里的時間可以是昨天或者很久以前。那么為了控制讓程序不要執(zhí)行這里的報錯和返回false,只能通過修改這里的valid了。我們看一下這里的檢驗(yàn)連接可用性的代碼:
public boolean isValidConnection(Connection conn, String validateQuery, int
validationQueryTimeout) throws Exception {
if (conn.isClosed()) {
return false;
}
//判斷usePingMethod是否為true
if (usePingMethod) {
if (conn instanceof DruidPooledConnection) {
conn = ((DruidPooledConnection) conn).getConnection();
}
if (conn instanceof ConnectionProxy) {
conn = ((ConnectionProxy) conn).getRawObject();
}
if (clazz.isAssignableFrom(conn.getClass())) {
if (validationQueryTimeout <= 0) {
validationQueryTimeout = DEFAULT_VALIDATION_QUERY_TIMEOUT;
}
//使用該類中的ping類進(jìn)行執(zhí)行。這里的ping并不是執(zhí)行sql,而是ping命令
try {
ping.invoke(conn, true, validationQueryTimeout * 1000);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof SQLException) {
throw (SQLException) cause;
}
throw e;
}
return true;
}
}
//如果usePingMethod為false,那么就執(zhí)行sql
String query = validateQuery;
//這里的validateQuery,就是spring.datasource.druid.validation-query的配置項(xiàng),這里如果為空的話,就采用select 1
if (validateQuery == null || validateQuery.isEmpty()) {
query = DEFAULT_VALIDATION_QUERY;
}
Statement stmt = null;
ResultSet rs = null;
try {
stmt = conn.createStatement();
if (validationQueryTimeout > 0) {
stmt.setQueryTimeout(validationQueryTimeout);
}
rs = stmt.executeQuery(query);
return true;
} finally {
JdbcUtils.close(rs);
JdbcUtils.close(stmt);
}
}
}
那么這里的usePingMethod又是如何判斷的,按理說這里的值應(yīng)該是true,然后導(dǎo)致后邊的檢索庫的sql沒有進(jìn)行。也就導(dǎo)致獲取最近的接受mysql數(shù)據(jù)包的時間沒有更新。那么問題的核心又變成了usePingMethod的賦值問題。發(fā)現(xiàn)在初始化的時候就就行了usePingMethod的初始化
public MySqlValidConnectionChecker(){
try {
clazz = Utils.loadClass("com.mysql.jdbc.MySQLConnection");
if (clazz == null) {
clazz = Utils.loadClass("com.mysql.cj.jdbc.ConnectionImpl");
}
if (clazz != null) {
ping = clazz.getMethod("pingInternal", boolean.class, int.class);
}
if (ping != null) {
usePingMethod = true;
}
} catch (Exception e) {
LOG.warn("Cannot resolve com.mysql.jdbc.Connection.ping method. Will use 'SELECT 1' instead.", e);
}
configFromProperties(System.getProperties());
}
@Override
public void configFromProperties(Properties properties) {
String property = properties.getProperty("druid.mysql.usePingMethod");
if ("true".equals(property)) {
setUsePingMethod(true);
} else if ("false".equals(property)) {
setUsePingMethod(false);
}
}
通過上述分析,我們大概明白了錯誤的原因,那么我們需要明白這個錯誤導(dǎo)致返回false,最后是否會對業(yè)務(wù)有什么影響。我們需要知道是誰在調(diào)這塊的代碼
public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
int notFullTimeoutRetryCnt = 0;
for (;;) {
// handle notFullTimeoutRetry
DruidPooledConnection poolableConnection;
try {
poolableConnection = getConnectionInternal(maxWaitMillis);
} catch (GetConnectionTimeoutException ex) {
if (notFullTimeoutRetryCnt <= this.notFullTimeoutRetryCount && !isFull()) {
notFullTimeoutRetryCnt++;
if (LOG.isWarnEnabled()) {
LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
}
continue;
}
throw ex;
}
if (testOnBorrow) {
//測試數(shù)據(jù)庫可用性
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
discardConnection(poolableConnection.holder);
continue;
}
}
當(dāng)testConnectionInternal返回為false的時候,就會執(zhí)行這里的discardConnection方法,這里要釋放這個連接了,
public void discardConnection(DruidConnectionHolder holder) {
if (holder != null) {
Connection conn = holder.getConnection();
if (conn != null) {
//這塊應(yīng)該是釋放的主要動作了。
JdbcUtils.close(conn);
}
//多線程加鎖
this.lock.lock();
try {
if (holder.discard) {
return;
}
if (holder.active) {
//讓活動的連接數(shù)減少一個
--this.activeCount;
holder.active = false;
}
//銷毀的個數(shù)+1
++this.discardCount;
holder.discard = true;
//如果活動的線程小于最小的連接數(shù)
if (this.activeCount <= this.minIdle) {
//這里釋放信號量,負(fù)責(zé)創(chuàng)建的線程會新建連接
this.emptySignal();
}
} finally {
this.lock.unlock();
}
}
JdbcUtils.close(conn);主要設(shè)置了一下判斷標(biāo)志
public static void close(Connection x) {
if (x != null) {
try {
if (x.isClosed()) {
return;
}
x.close();
} catch (Exception var2) {
LOG.debug("close connection error", var2);
}
}
}
通過上述代碼,我們并沒有發(fā)現(xiàn)銷毀數(shù)組中連接的操作,那么我們看創(chuàng)建新鏈接的的時候有沒有相關(guān)的邏輯,方法如下:
private void emptySignal() {
if (this.createScheduler == null) {
this.empty.signal();
} else if (this.createTaskCount < this.maxCreateTaskCount) {
//如果創(chuàng)建的連接小于最大的連接
if (this.activeCount + this.poolingCount + this.createTaskCount < this.maxActive) {
//正在活動的連接+連接池中剩下的+現(xiàn)在要創(chuàng)建的之<允許的最大的連接;
// 就創(chuàng)建一個
this.submitCreateTask(false);
}
}
}
druid connections[]數(shù)組
DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
try {
while(this.poolingCount == 0) {
//如果連接池中連接數(shù)為0,就發(fā)信號,讓其創(chuàng)建的線程來創(chuàng)建
this.emptySignal();
if (this.failFast && this.isFailContinuous()) {
throw new DataSourceNotAvailableException(this.createError);
}
++this.notEmptyWaitThreadCount;
if (this.notEmptyWaitThreadCount > this.notEmptyWaitThreadPeak) {
this.notEmptyWaitThreadPeak = this.notEmptyWaitThreadCount;
}
try {
//如果池子中為空,就阻塞。等待創(chuàng)建線程創(chuàng)建好了之后進(jìn)行喚醒
this.notEmpty.await();
} finally {
--this.notEmptyWaitThreadCount;
}
++this.notEmptyWaitCount;
if (!this.enable) {
connectErrorCountUpdater.incrementAndGet(this);
if (this.disableException != null) {
throw this.disableException;
}
throw new DataSourceDisableException();
}
}
} catch (InterruptedException var5) {
this.notEmpty.signal();
++this.notEmptySignalCount;
throw var5;
}
//將連接池中的連接減少一個,因?yàn)檫@個要出去干活了。
this.decrementPoolingCount();
//拿到這個連接
DruidConnectionHolder last = this.connections[this.poolingCount];
//設(shè)置為空,gc的時候進(jìn)行空間釋放
this.connections[this.poolingCount] = null;
//返回
return last;
通過上述分析,我們知道獲取連接的時候是通過connects數(shù)組獲取的,獲取之后就交給業(yè)務(wù)。所以說上邊的釋放對連接池沒有任何影響,所以對業(yè)務(wù)沒有影響。通過上述分析,我們對druid數(shù)據(jù)庫連接池的工作過程有了很近一步的理解。至于上述的連接置為null的操作在線程池中也是相同的做法
對于druid連接池來說。報錯信息:
([com.alibaba.druid.pool.DruidAbstractDataSource:]) discard long time none received connection.
不會影響業(yè)務(wù)。避免報錯的方法是在項(xiàng)目啟動的時候通過腳本添加是否使用ping命令來檢測連接的可用性。druid讀取該配置的時候直接讀取的系統(tǒng)變量。所以在項(xiàng)目中添加配置是沒有作用的。當(dāng)上述報錯產(chǎn)生之后,druid會將連接銷毀,并嘗試從連接池中獲取新鏈接。如果沒有的話就會創(chuàng)建。其中的界限條件就是最小連接數(shù),最大連接數(shù)等。除此之外,druid有兩個線程,分別為連接創(chuàng)建線程和心跳檢測線程。他們相互配合保證連接的可用性和連接異步創(chuàng)建,所以對業(yè)務(wù)來說,總是有連接可用的