數據庫鏈接(2) - 爲何C3P0鏈接池那麼慢

摘要

承接上篇數據庫鏈接(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…

關注【方丈的寺院】,與方丈一塊兒開始技術修行之路

在這裏插入圖片描述

參考

my.oschina.net/javahongxi/…

相關文章
相關標籤/搜索