承接上篇數據庫鏈接(1)從jdbc到mybatis,介紹下數據庫鏈接池技術java
在上一篇中咱們介紹說客戶端創建一次鏈接耗時太長(創建鏈接,設置字符集,autocommit等),若是在每一個sql操做都須要經歷創建鏈接,關閉鏈接。不只應用程序響應慢,並且會產生不少臨時對象,應用服務器GC壓力大。另外數據庫server端對鏈接也有限制,好比MySQL默認151個鏈接(實際環境中通常會調大這個值,尤爲是多個服務時)node
如今面臨的問題就是如何提升對稀缺性的資源高效管理。由於客戶端與數據庫的鏈接本質就是tcp請求,加上基於tcp協議封裝的mysql請求。那麼一般解決這類問題,咱們有兩種方式,一種是池話技術,即便用一個容器,提早建立好鏈接,請求時直接從池子裏面拿,另一種就是利用IO多路複用技術。利於在spring5中,mongo ,cassandra等數據庫的訪問就能夠利用reactive來實現了,可是關係型數據庫不行,緣由在於關型數據庫的訪問目前都是基於JDBC,JDBC操做數據庫的流程,創建connection,構建Statement,執行這一套是串行的,阻塞型。一個事務中的多個操做只能在同一個鏈接中完成。因此不能使用IO多路複用技術,是受限於JDBC的阻塞。對於其餘語言,是能夠的,好比nodejsmysql
因此咱們使用池話技術來提供數據庫訪問react
一般,程序員在業務開發中常用的是線程池,利用CPU多核,來併發處理任務,提升效率。數據庫鏈接池與線程池同屬於池化技術,沒有太大區別,都是須要管理池的大小,資源控制。不一樣的數據庫鏈接池中放的是connection,同時還須要管理事務,因此一般數據庫鏈接池中會對這個進行優化git
從鏈接池中取鏈接執行sql操做,多了兩步設置connection autocommit屬性操做 程序員
經過將connection分紅兩組,來提供效率 github
一個基本的數據庫鏈接池包括幾大部分spring
取出鏈接sql
放回鏈接數據庫
異步/同步處理線程
進行建立鏈接和銷燬鏈接 對於一個數據庫鏈接池的根本就在於併發容器的實現,也是決定鏈接池的效率高低,常見的鏈接池配置以下
initialSize:初始鏈接數
maxActive: 最大鏈接數量
minIdle: 最小鏈接數量
maxWait: 獲取鏈接最大等待時間ms
minEvictableIdleTimeMillis:鏈接保持空閒而不被驅逐的最小時間
timeBetweenEvictionRunsMillis:銷燬線程的時間檢測
testOnBorrow:申請鏈接時執行,比較影響性能
validationQuery:testOnBorrow爲true檢測是不是有效鏈接sql
testWhileIdle:申請鏈接的時候檢測
複製代碼
目前的開源數據庫鏈接池主要有如下,
C3P0,和DBCP是出現的比較早的數據庫鏈接,主要用於hibernate,和tomcat6.0如下,比較穩定,在低併發的狀況下,工做還能夠,可是高併發下,性能比較差,因此在tomcat6,又重寫了一個jdbc-pool,來替代DBCP。
Druid是阿里巴巴開源的高性能數據庫鏈接池,目前基本是各大互聯網公司的標配了,加上又是國內的,文檔比較易讀,因此流行度比較高,另一個是hikariCP,性能比較高,目前普及度還不是特別高。
那爲何C3P0和DBCP的性能比較低呢? 前面提到數據庫鏈接池本質上就是一個併發容器的實現。一般咱們能夠利用List+鎖機制實現。或者使用jdk原生的,好比CopyOnWriteList這樣的結構 而鎖經過有兩種,一種JVM級別的synchronized,一種是JDK提供的ReentrantLock,二者在語義上並無多大區別,互斥,內存可見,可重入。JDK5中引入ReentrantLock時,性能比synchronzied要好不少,而在JDK6中,通過優化後的,二者並沒有太大性能上區別。因此ReentrantLock更多優點在於
能夠中斷等待的線程 一直拿不到鎖的等待線程,能夠中斷掉,避免出現死鎖
能夠結合Condition,更加靈活控制線程
看下com.mchange.v2.c3p0.DriverManagerDataSource 的實現
// should NOT be sync'ed -- driver() is sync'ed and that's enough // sync'ing the method creates the danger that one freeze on connect
// blocks access to the entire DataSource
public Connection getConnection() throws SQLException
{
ensureDriverLoaded();
// 經過此方法來獲取鏈接
Connection out = driver().connect( jdbcUrl, properties );
if (out == null)
throw new SQLException("Apparently, jdbc URL '" + jdbcUrl + "' is not valid for the underlying " +
"driver [" + driver() + "].");
return out;
}
複製代碼
在獲取鏈接的時候首先在一個synchonized中去獲取java.sql.Driver,
private synchronized Driver driver() throws SQLException
{
//To simulate an unreliable DataSource...
//double d = Math.random() * 10;
//if ( d > 1 )
// throw new SQLException(this.getClass().getName() + " TEST of unreliable Connection. If you're not testing, you shouldn't be seeing this!");
//System.err.println( "driver() <-- " + this );
if (driver == null)
{
if (driverClass != null && forceUseNamedDriverClass)
{
if ( Debug.DEBUG && logger.isLoggable( MLevel.FINER ) )
logger.finer( "Circumventing DriverManager and instantiating driver class '" + driverClass +
"' directly. (forceUseNamedDriverClass = " + forceUseNamedDriverClass + ")" );
try
{
driver = (Driver) Class.forName( driverClass ).newInstance();
this.setDriverClassLoaded( true );
}
catch (Exception e)
{ SqlUtils.toSQLException("Cannot instantiate specified JDBC driver. Exception while initializing named, forced-to-use driver class'" + driverClass +"'", e); }
}
else
driver = DriverManager.getDriver( jdbcUrl );
}
return driver;
}
複製代碼
具體的鏈接池管理是BasicResourcePool,能夠看下代碼,裏面全都是synchronized方法。併發性能怎麼能好。
再來看下Druid的實現,DruidDataSource
private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {
DruidConnectionHolder holder;
for (boolean createDirect = false;;) {
// 帶有超時的鏈接獲取
if (maxWait > 0) {
holder = pollLast(nanos);
} else {
holder = takeLast();
}
}
複製代碼
併發環境下去拿鏈接時,並無在讀操做上加鎖,比互斥鎖的性能要高 互斥鎖是一種比較保守的策略,像synchronized,它避免了寫寫衝突,寫讀衝突,和讀讀衝突,對於數據庫鏈接池,應用程序來拿,是一個讀操做比較多的,容許多個讀同時操做,可以提升系統的併發性。
private DruidConnectionHolder pollLast(long nanos) throws InterruptedException, SQLException {
long estimate = nanos;
// 隊列阻塞,當取鏈接時,沒有鏈接,線程空轉,等待另外建立線程去建立鏈接
for (;;) {
if (poolingCount == 0) {
// 通知建立線程去建立鏈接
emptySignal();
}
}
decrementPoolingCount();
// 從數組中獲取鏈接
DruidConnectionHolder last = connections[poolingCount];
connections[poolingCount] = null;
long waitNanos = nanos - estimate;
last.setLastNotEmptyWaitNanos(waitNanos);
return last;
}
複製代碼
在建立鏈接線程,銷燬鏈接線程中增長寫鎖
private boolean put(DruidConnectionHolder holder) {
// 加鎖
lock.lock();
try {
if (poolingCount >= maxActive) {
return false;
}
connections[poolingCount] = holder;
incrementPoolingCount();
if (poolingCount > poolingPeak) {
poolingPeak = poolingCount;
poolingPeakTime = System.currentTimeMillis();
}
//發出鏈接池非空信號,等待的線程開始處理
notEmpty.signal();
notEmptySignalCount++;
if (createScheduler != null) {
createTaskCount--;
if (poolingCount + createTaskCount < notEmptyWaitThreadCount //
&& activeCount + poolingCount + createTaskCount < maxActive) {
emptySignal();
}
}
} finally {
lock.unlock();
}
return true;
}
複製代碼
HikariCP在讀寫鎖的基礎上進行了進一步的優化 github.com/brettwooldr…
關注【方丈的寺院】,與方丈一塊兒開始技術修行之路