資料庫連線(2) - 為什麼C3P0連線池那麼慢

方丈的寺院發表於2018-04-15

摘要

承接上篇資料庫連線(1)從jdbc到mybatis,介紹下資料庫連線池技術

為什麼需要連線池

在上一篇中我們介紹說客戶端建立一次連線耗時太長(建立連線,設定字符集,autocommit等),如果在每個sql操作都需要經歷建立連線,關閉連線。不僅應用程式響應慢,而且會產生很多臨時物件,應用伺服器GC壓力大。另外資料庫server端對連線也有限制,比如MySQL預設151個連線(實際環境中一般會調大這個值,尤其是多個服務時)

現在面臨的問題就是如何提高對稀缺性的資源高效管理。因為客戶端與資料庫的連線本質就是tcp請求,加上基於tcp協議封裝的mysql請求。那麼通常解決這類問題,我們有兩種方式,一種是池話技術,即使用一個容器,提前建立好連線,請求時直接從池子裡面拿,另外一種就是利用IO多路複用技術。利於在spring5中,mongo ,cassandra等資料庫的訪問就可以利用reactive來實現了,但是關係型資料庫不行,原因在於關型資料庫的訪問目前都是基於JDBC,JDBC運算元據庫的流程,建立connection,構建Statement,執行這一套是序列的,阻塞型。一個事務中的多個操作只能在同一個連線中完成。所以不能使用IO多路複用技術,是受限於JDBC的阻塞。對於其他語言,是可以的,比如nodejs

所以我們使用池話技術來提供資料庫訪問

資料庫連線池與執行緒池的區別

通常,程式設計師在業務開發中經常使用的是執行緒池,利用CPU多核,來併發處理任務,提高效率。資料庫連線池與執行緒池同屬於池化技術,沒有太大區別,都是需要管理池的大小,資源控制。不同的資料庫連線池中放的是connection,同時還需要管理事務,所以通常資料庫連線池中會對這個進行優化

從連線池中取連線執行sql操作,多了兩步設定connection autocommit屬性操作
這裡寫圖片描述

通過將connection分成兩組,來提供效率
這裡寫圖片描述

開源連線池技術介紹

這裡寫圖片描述

一個基本的資料庫連線池包括幾大部分

  • 取出連線

  • 放回連線

  • 非同步/同步處理執行緒

    進行建立連線和銷燬連線
    對於一個資料庫連線池的根本就在於併發容器的實現,也是決定連線池的效率高低,常見的連線池配置如下

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在讀寫鎖的基礎上進行了進一步的優化
https://github.com/brettwooldridge/HikariCP/wiki/Down-the-Rabbit-Hole

參考

https://my.oschina.net/javahongxi/blog/1523745

相關文章