今天我們說說數據源和數據庫連接池,熟悉java開發的同仁應該都了解C3PO,在這里不做過多的贅述了,今天我們說的是阿里DRUID,druid是后起之秀,因為它的優秀很快占領了使用市場,下邊我們一起來看看druid數據源的配置以及druid監控的配置和監控的實現邏輯。
1、druid數據源配置
下面是druid的數據源配置項,這些配置項都是com.alibaba.druid.pool.DruidDataSource類和其基類com.alibaba.druid.pool.DruidAbstractDataSource的public final屬性,這些配置型和C3P0的數據源配置項基本一樣,有個別的是明白發生了變化但是參數所表示的意思不變,還有一些參數是druid自己擴展的,其中filters屬性就是傑出代表,次屬性是DruidAbstractDataSource類的,是一個List<Filter>的集合,此屬性提供了三個可選值:監控統計用的stat、日志用的log4j、 防御sql注入的wall,這三個值可以單獨使用也可以兩兩組合或者一起使用,組合使用的時候不同值之間用逗號隔開。有人可能會有疑問了,不是一個List集合嗎,為什么這里卻是用逗號分隔的,那是因為druid在賦值的時候有特殊處理,至於是如何處理的在下邊我們會說到。
配置 | 缺省值 | 說明 |
name | 配置這個屬性的意義在於,如果存在多個數據源,監控的時候 可以通過名字來區分開來。如果沒有配置,將會生成一個名字, 格式是:"DataSource-" + System.identityHashCode(this) |
|
jdbcUrl | 連接數據庫的url,不同數據庫不一樣。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto |
|
username | 連接數據庫的用戶名 | |
password | 連接數據庫的密碼。如果你不希望密碼直接寫在配置文件中, 可以使用ConfigFilter。詳細看這里: https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter |
|
driverClassName | 根據url自動識別 | 這一項可配可不配,如果不配置druid會根據url自動識別dbType, 然后選擇相應的driverClassName |
initialSize | 0 | 初始化時建立物理連接的個數。初始化發生在顯示調用init方法, 或者第一次getConnection時 |
maxActive | 8 | 最大連接池數量 |
maxIdle | 8 | 已經不再使用,配置了也沒效果 |
minIdle | 最小連接池數量 | |
maxWait | 獲取連接時最大等待時間,單位毫秒。配置了maxWait之后, 缺省啟用公平鎖,並發效率會有所下降, 如果需要可以通過配置useUnfairLock屬性為true使用非公平鎖。 |
|
poolPreparedStatements | false | 是否緩存preparedStatement,也就是PSCache。 PSCache對支持游標的數據庫性能提升巨大,比如說oracle。 在mysql5.5以下的版本中沒有PSCache功能,建議關閉掉。 5.5及以上版本有PSCache,建議開啟。 |
maxOpenPreparedStatements | -1 | 要啟用PSCache,必須配置大於0,當大於0時, poolPreparedStatements自動觸發修改為true。 在Druid中,不會存在Oracle下PSCache占用內存過多的問題, 可以把這個數值配置大一些,比如說100 |
validationQuery | 用來檢測連接是否有效的sql,要求是一個查詢語句。 如果validationQuery為null,testOnBorrow、testOnReturn、 testWhileIdle都不會其作用。 |
|
testOnBorrow | true | 申請連接時執行validationQuery檢測連接是否有效, 做了這個配置會降低性能。 |
testOnReturn | false | 歸還連接時執行validationQuery檢測連接是否有效, 做了這個配置會降低性能 |
testWhileIdle | false | 建議配置為true,不影響性能,並且保證安全性。 申請連接的時候檢測,如果空閑時間大於 timeBetweenEvictionRunsMillis, 執行validationQuery檢測連接是否有效。 |
timeBetweenEvictionRunsMillis | 有兩個含義: 1) Destroy線程會檢測連接的間隔時間 2) testWhileIdle的判斷依據,詳細看testWhileIdle屬性的說明 |
|
numTestsPerEvictionRun | 不再使用,一個DruidDataSource只支持一個EvictionRun | |
minEvictableIdleTimeMillis | Destory線程中如果檢測到當前連接的最后活躍時間和當前時間的差值大於 minEvictableIdleTimeMillis,則關閉當前連接。 |
|
connectionInitSqls | 物理連接初始化的時候執行的sql | |
exceptionSorter | 根據dbType自動識別 | 當數據庫拋出一些不可恢復的異常時,拋棄連接 |
filters | 屬性類型是字符串,通過別名的方式配置擴展插件, 常用的插件有: 監控統計用的filter:stat 日志用的filter:log4j 防御sql注入的filter:wall |
|
proxyFilters | 類型是List<com.alibaba.druid.filter.Filter>, 如果同時配置了filters和proxyFilters, 是組合關系,並非替換關系 |
|
removeAbandoned | 對於建立時間超過removeAbandonedTimeout的連接強制關閉 | |
removeAbandonedTimeout | 指定連接建立多長時間就需要被強制關閉 | |
logAbandoned | 指定發生removeabandoned的時候,是否記錄當前線程的堆棧信息到日志中 |
2、druid監控的配置與監控訪問
要使用druid監控需要做好兩個配置:
1)、在配置數據源時需要配置filters並且賦值你需要使用的監控項(stat、log4j、wall);
2)、需要在項目的web.xml中配置druid的自定義servlet(com.alibaba.druid.support.http.StatViewServlet),配置樣例如下代碼所示:
<servlet>
<servlet-name>DruidStatView</servlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>DruidStatView</servlet-name> <url-pattern>/druid/*</url-pattern> </servlet-mapping>
3)、監控系統的訪問:http://IP:PORT/projectName/druid/(http://localhost:8088/cd_management/druid/sql.html),監控效果如下圖所示:
3、filters屬性的賦值邏輯
上面有提到filters的配置項有三個,可以隨機組合也可以一起使用,也有提到filters是一個List集合,那我們來看看druid是如何賦值的,如下源碼所示,賦值時調用的setFilters(String filters)方法,最終是通過逗號分隔為數組然后遍歷調用FilterManager.loadFilter(this.filters, item.trim()),然后用反射機制生成相應的對象並添加到Filter集合。
public void setFilters(String filters) throws SQLException { if (filters != null && filters.startsWith("!")) { filters = filters.substring(1); this.clearFilters(); } this.addFilters(filters); } public void addFilters(String filters) throws SQLException { if (filters == null || filters.length() == 0) { return; } String[] filterArray = filters.split("\\,"); for (String item : filterArray) { FilterManager.loadFilter(this.filters, item.trim()); } }
public static void loadFilter(List<Filter> filters, String filterName) throws SQLException { if (filterName.length() == 0) { return; } String filterClassNames = getFilter(filterName); if (filterClassNames != null) { for (String filterClassName : filterClassNames.split(",")) { if (existsFilter(filters, filterClassName)) { continue; } Class<?> filterClass = Utils.loadClass(filterClassName); if (filterClass == null) { LOG.error("load filter error, filter not found : " + filterClassName); continue; } Filter filter; try { filter = (Filter) filterClass.newInstance(); } catch (ClassCastException e) { LOG.error("load filter error.", e); continue; } catch (InstantiationException e) { throw new SQLException("load managed jdbc driver event listener error. " + filterName, e); } catch (IllegalAccessException e) { throw new SQLException("load managed jdbc driver event listener error. " + filterName, e); } filters.add(filter); } return; } if (existsFilter(filters, filterName)) { return; } Class<?> filterClass = Utils.loadClass(filterName); if (filterClass == null) { LOG.error("load filter error, filter not found : " + filterName); return; } try { Filter filter = (Filter) filterClass.newInstance(); filters.add(filter); } catch (Exception e) { throw new SQLException("load managed jdbc driver event listener error. " + filterName, e); } }
4、druid監控實現邏輯
要說明druid監控邏輯從如下三個方面切入分析:
1)、監控的數據什么時候生成
在這里我們拿Spring和druid整合案例來分析說明druid監控數據的生成,上面我們有提到要使用druid的監控空能需要配置filters,並且filters可以配置多個,這里druid關於這些filter的處理其實借鑒了過濾器鏈的原理,druid關於監控數據的收集處理邏輯是這樣的,我們從Spring的JdbcTemplate類開始看,如下源碼一,是一個查詢的處理,rs = ps.executeQuery()是PreparedStatement開始執行sql從數據庫查詢數據的開始,這里我們給ps對象賦予的是DruidPooledPreparedStatement類對象,所以進入DruidPooledPreparedStatement類我們來看它的具體實現。
源碼二是DruidPooledPreparedStatement類對executeQuery方法的實現,這個方法里面最關鍵的是ResultSet rs = stmt.executeQuery()這句,stmt是PreparedStatementProxyImpl類的類對象。
源碼三是PreparedStatementProxyImpl類對executeQuery方法的實現,這個方法實現中調用了父類的createChain()方法,源碼四為父類方法實現,這個方法的返回值是一個過濾器鏈類FilterChainImpl類對象,FilterChainImpl類的
preparedStatement_executeQuery(PreparedStatementProxy statement)方法實現如源碼五。
public <T> T query( PreparedStatementCreator psc, final PreparedStatementSetter pss, final ResultSetExtractor<T> rse) throws DataAccessException { Assert.notNull(rse, "ResultSetExtractor must not be null"); logger.debug("Executing prepared SQL query"); return execute(psc, new PreparedStatementCallback<T>() { @Override public T doInPreparedStatement(PreparedStatement ps) throws SQLException { ResultSet rs = null; try { if (pss != null) { pss.setValues(ps); } rs = ps.executeQuery(); ResultSet rsToUse = rs; if (nativeJdbcExtractor != null) { rsToUse = nativeJdbcExtractor.getNativeResultSet(rs); } return rse.extractData(rsToUse); } finally { JdbcUtils.closeResultSet(rs); if (pss instanceof ParameterDisposer) { ((ParameterDisposer) pss).cleanupParameters(); } } } }); }
public ResultSet executeQuery() throws SQLException { checkOpen(); incrementExecuteCount(); transactionRecord(sql); oracleSetRowPrefetch(); conn.beforeExecute(); try { ResultSet rs = stmt.executeQuery(); if (rs == null) { return null; } DruidPooledResultSet poolableResultSet = new DruidPooledResultSet(this, rs); addResultSetTrace(poolableResultSet); return poolableResultSet; } catch (Throwable t) { errorCheck(t); throw checkException(t); } finally { conn.afterExecute(); } }
public ResultSet executeQuery() throws SQLException { firstResultSet = true; updateCount = null; lastExecuteSql = sql; lastExecuteType = StatementExecuteType.ExecuteQuery; lastExecuteStartNano = -1L; lastExecuteTimeNano = -1L; return createChain().preparedStatement_executeQuery(this); }
public FilterChainImpl createChain() { FilterChainImpl chain = this.filterChain; if (chain == null) { chain = new FilterChainImpl(this.getConnectionProxy().getDirectDataSource()); } else { this.filterChain = null; } return chain; }
@Override
public ResultSetProxy preparedStatement_executeQuery(PreparedStatementProxy statement) throws SQLException { if (this.pos < filterSize) { return nextFilter().preparedStatement_executeQuery(this, statement); } ResultSet resultSet = statement.getRawObject().executeQuery(); if (resultSet == null) { return null; } return new ResultSetProxyImpl(statement, resultSet, dataSource.createResultSetId(), statement.getLastExecuteSql()); }
看了如上源碼我們會發現FilterChainImpl類的preparedStatement_executeQuery方法執行的時候會先執行過濾器類的此方法,所以我們看看過濾器類做了什么,這里我們拿SQL監控的過濾器類(FilterEventAdapter)來分析,如下源碼是此類的方法實現。我看可以看到此方法先調用了statementExecuteQueryBefore(statement, statement.getSql())方法,然后調用了下一個過濾器類的查詢方法,在方法正常執行以后又調用了statementExecuteQueryAfter(statement, statement.getSql(), resultSet)方法,在方法執行異常的時候調用了statement_executeErrorAfter(statement, statement.getSql(), error),這些方法的作用就是保存SQL執行中的監控數據。說到這里從流程上就說明了druid監控數據的來源。
public ResultSetProxy preparedStatement_executeQuery(FilterChain chain, PreparedStatementProxy statement) throws SQLException { try { statementExecuteQueryBefore(statement, statement.getSql()); ResultSetProxy resultSet = chain.preparedStatement_executeQuery(statement); if (resultSet != null) { statementExecuteQueryAfter(statement, statement.getSql(), resultSet); resultSetOpenAfter(resultSet); } return resultSet; } catch (SQLException error) { statement_executeErrorAfter(statement, statement.getSql(), error); throw error; } catch (RuntimeException error) { statement_executeErrorAfter(statement, statement.getSql(), error); throw error; } catch (Error error) { statement_executeErrorAfter(statement, statement.getSql(), error); throw error; } }
2)、監控的數據保存到哪里
這里我們簡單說明,數據是保存到DruidDataSource類的dataSourceStat對象中。
3)、監控的請求如何處理的
要使用druid的監控功能需要配置com.alibaba.druid.support.http.StatViewServlet,這是一個繼承自HttpServlet的servlet,用來處理訪問druid監控的請求,具體處理流程如下:
5、如何去除監控頁面的廣告
1) 使用過druid的同仁應該都了解,druid的監控頁面加載以后,footer頁是有阿里的廣告的如下圖所示,如果是一個商業項目這個是很不雅也是不允許的,那么我們來看看如何去除廣告。
2)要去除這個廣告需要修改druid.jar的源碼文件,具體方法是,用winRAR打開jar包,在druid-1.1.6.jar\support\http\resources\js\common.js路徑下找到文件,修改common.js中如下圖所示的代碼,刪除buildFooter函數中的代碼即可:
buildFooter : function() { var html ='<footer class="footer">'+ ' <div class="container">'+ '<a href="https://render.alipay.com/p/s/taobaonpm_click/druid_banner_click" target="new"><img src="https://render.alipay.com/p/s/taobaonpm_click/druid_banner"></a><br/>' + ' powered by <a href="https://github.com/alibaba/" target="_blank">AlibabaTech</a> & <a href="http://www.sandzhang.com/" target="_blank">sandzhang</a> & <a href="http://melin.iteye.com/" target="_blank">melin</a> & <a href="https://github.com/shrekwang" target="_blank">shrek.wang</a>'+ ' </div>'+ ' </footer>'; $(document.body).append(html); },