先交待下項目背景,項目中有個功能是從mysql中獲取數據庫信息來建立數據庫鏈接,用的鏈接池是druid,jar包版本是1.0.9。java
最近項目中出現了一個神奇的異常:mysql
讀取MySQL數據錯誤: jdbc:mysql://61.160.xxx.xxx:3306/xxx?characterEncoding=utf-8&autoReconnect=false&connectTimeout=15000&socketTimeout=60000, taskId=xxxxx, message: nullsql
這個發生在數據庫操做時,神奇之處在於:數據庫
拋出異常的代碼以下:apache
@Override protected Result<Date> doImportData() { String url = getJdbcUrl(); String host = getHost(); int port = getPort(); String username = getUsername(); String password = getPassword(); Connection con = null; DataSource dataSource = null; try { long t1 = System.currentTimeMillis(); dataSource = DataSourceProvider.getDataSource(host, port, username, password, url, MAX_POOL_SIZE, MIN_POOL_SIZE, INITIAL_POOL_SZIE); con = dataSource.getConnection(); long getConnectionCostTime = System.currentTimeMillis() - t1; logger.info("taskId=" + task.getTaskId() + ",getConnection()=" + getConnectionCostTime + "ms"); ··· } catch (SQLException e) { logger.error("讀取MySQL數據錯誤: " + url + ", taskId=" + task.getTaskId() + ", message: " + e.getMessage()); ··· } finally { if (con != null) { try { con.close(); } catch (SQLException e) { // ignore } } } return result; }
logger.info("taskId=" + task.getTaskId() + ",getConnection()=" + getConnectionCostTime + "ms");
long t1 = System.currentTimeMillis(); dataSource = DataSourceProvider.getDataSource(host, port, username, password, url, MAX_POOL_SIZE, MIN_POOL_SIZE, INITIAL_POOL_SZIE); con = dataSource.getConnection();
con = dataSource.getConnection(); // 項目中用的是druid鏈接池 // com.alibaba.druid.pool.DruidDataSource public DruidPooledConnection getConnection() throws SQLException
在方法裏往下找下去,沒找到什麼有用的信息,終於仍是放棄了...緩存
項目中的異常還在報,好想僞裝異常沒發生...嗯,仍是繼續排查吧。數據結構
接下來,恰好碰上項目發佈,果斷修改了打印異常信息的方式,此次直接打出異常堆棧而不單單是message了:多線程
logger.error("讀取MySQL數據錯誤: " + url + ", taskId=" + task.getTaskId() + ", message: " + ExceptionUtils.getStackTrace(e));
ExceptionUtils
是apache commmons包提供的類,封裝了異常相關的操做方法。socket
等待着,等待着,異常信息終於來了:ide
[遊戲數據採集異常]讀取MySQL數據錯誤: jdbc:mysql://61.160.xxx.xxx:3306/xxx?characterEncoding=utf-8&autoReconnect=false&connectTimeout=15000&socketTimeout=60000, taskId=xxxxxx, message: com.alibaba.druid.pool.DataSourceDisableException at com.alibaba.druid.pool.DruidDataSource.pollLast(DruidDataSource.java:1482) at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1074) at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:941) at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:921) at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:911) at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:98) ···
從異常堆棧信息來看,報錯的異常是DataSourceDisableException
,代碼是DruidDataSource.java
的1482行。
DataSourceDisableException
異常:首先來認識下DataSourceDisableException
異常:
public class DataSourceDisableException extends SQLException
這個類繼承了SQLException
,因此代碼中能catch到。
再看看異常拋出的的位置:
if (!enable) { connectErrorCount.incrementAndGet(); throw new DataSourceDisableException(); }
發現兩個問題:
new DataSourceDisableException('出現異常了')
,這就致使了異常信息是null。狀況已經很明顯了,接下來只要找到致使enable爲false的位置就好了。
經過搜索得知,enable初始化時爲true,對外暴露了setEnable(boolean)
方法:
public void setEnable(boolean enable) { ··· this.enable = enable; ··· }
同時在close()
方法中明確地將enable設置成了false:
public void close() { ··· enable = false; ··· }
進一步排查,發現setEnable()
方法沒有地方調用,而close()
方法有地訪問在調用。對照代碼邏輯,得出的結論以下:
代碼裏有這樣一個操做:數據庫鏈接時,以host、port爲key,從緩存中獲取dataSource,若username、password與鏈接裏的不一致時,就會關閉獲取到的dataSource,並以新的username、password建立新的dataSource.可是,當同一個host、port有多個帳號跟密碼時,這種操做會有問題,緣由是多線程狀況下,關閉的dataSource能夠會被其餘線程獲取到。
知道了問題的所在,解決辦法就很簡單了:保證dataSource只能被一個線程獲取,這裏用的緩存數據結構是ConcurrentHashMap
,獲取數據源時,用的是操做是get()
方法,這裏只要將操做改爲remove()
,而且使用完以後,再將dataSource
用put()
方法返還就能夠了。
這個異常困擾了我兩天,緣由與解決方法並不複雜,難點在於異常信息。對於異常信息爲null的狀況,實在無人下午,只能對着代碼乾瞪眼。本次排查的起色出如今異常堆棧信息的打印,詳細的異常信息有助於問題的快速解決。
寫了這麼多,總結以下: