1. 程式人生 > 程式設計 >【重溫mysql】1、連線池

【重溫mysql】1、連線池

在我們日常的開發中,會經常與資料庫打交道。對於 java 開發者來說,經常會使用jdbc來與資料庫進行互動。我們可能會看到這樣的程式碼:

try(
    Connection conn = ...;
    PreparedStatement stmt = conn.prepareStatement(sql);
){
    ...
catch(SQLException se){
    se.printStackTrace();
}catch(Exception e){
    e.printStackTrace();
}
複製程式碼

對於生產環境來說,我們常常會使用連線池技術來提高效能。那麼為什麼連線池可以提高效能呢?

為何需要連線池?

首先我們來看看資料庫連線池在一個常見的分散式架構系統中的位置。

從圖上可以看出,連線池位於程式與資料庫之間,起著資料庫溝通的橋樑關鍵作用。
我們通常使用資料庫的流程為:

那麼整個jdbc執行過程中網路連線是怎樣的呢?

  • 第一步:資料庫tcp連線(3次握手)
  • 第二步:mysql許可權認證
  • 第三步:執行sql,獲得結果
  • 第四步:關閉mysql
  • 第五步:資料庫tcp關閉(4次揮手)

可以看到,一個sql操作需要進行這麼多的網路互動操作,如果我們能夠減少其中的一些步驟的話那麼將可以大大提升我們的程式效能。那麼我們使用了連線池後的情況是怎麼樣的呢?

通過連線池,我們只需要進行jdbc過程的第三步:執行sql獲取結果
需要網路請求,而第一步:資料庫tcp連線第二步:資料庫認證第四步:關閉mysql第五步:資料庫tcp關閉的網路過程給轉移到了連線池初始化或建立連線的過程,當連線池中的連線不夠的時候,連線池會自動建立與資料庫的連線,並將建立的連線使用完後放回連線池,從而達到連線的複用。當資料庫訪問請求不是那麼頻繁的時候,連線池將會按照我們配置的規則釋放多餘的資料庫連線,減輕資料庫資源佔用。

因此,我們可以得到結論:資料庫連線池技術本質上為網路資源的複用。

資料庫連線池與執行緒池的比較

在工作中,大家經常也會使用執行緒池技術來提升程式的效能,那麼執行緒池與連線池它們又有何異同呢?
從本質上講,執行緒池與連線池並無太大的區別,它們都是一種資源複用的技術,都需要對使用的資源進行管理,比如最大、最小連線數 ...
它們唯一的區別在於管理的資源物件不同:

  • 執行緒池管理的是執行緒資源,本質上來講是cpu、記憶體等資源的複用
  • 連線池管理的是網路資源,本質上為網路資源的複用

常見連線池

現在的連線池已經有很多種選擇,從開始的c3p0,dbcp,到Druid、HikariCP,連線池技術越來越豐富,我們可能會好奇不就是一個連線池嗎,為什麼還要有這麼多的連線池方案? wenshao(Druid作者,編者注)又是如何看待Druid、HikariCP的?

其實連線池技術與網際網路發展密不可分,連線池的發展就是網際網路發展的一個縮影。從這中的歷程我們可以感受到近15年來蓬勃發展的網際網路。

  • 為何 Druid、HikariCP 會比 c3p0 快?
  • c3p0 在某些條件下為何會存在 Deadlock 問題?
  • HikariCP 為何又號稱最快的連線池,它是如何做到的?
  • Druid又為什麼號稱最好的java執行緒池?

只有深入這些連線池的原始碼,看看它們的具體模型與架構方案,我們才能一一解答上述疑問,從中感受到這些作者的架構思想,並將這些思想與架構融入我們的日常的思維與開發中,提升我們的程式碼質量及內力。在使用遇到問題的時候我們也可以從中發現問題根源、從而進行解決。

求知若渴,虛心若愚。

在網路上已經可以看到很多分析Druid、HikariCP 的原始碼的文章,很多文章寫的很好,如果想要詳細瞭解的話可以參考如下文章:

受限於本文的篇幅,對於執行緒池最核心的部分莫過於獲取連線的過程,在這裡我主要大致分析兩款資料庫連線池 Druid 與 HikariCP 的核心邏輯。

Druid 連線池分析

Druid基於java原生提供的相關內容進行的開發,Druid 在獲取連線的時候使用了ReentrantLock 來對執行緒池擴容過程進行加鎖,Druid 執行緒池使用了兩個執行緒來管理整個過程,分別為建立執行緒和銷燬執行緒,建立執行緒負責連線擴容、銷燬執行緒負責收縮銷燬,具體獲取連線過程如下:

  • 1、檢查池中是否有可用連線
  • 2、如若池中有可用連線,則直接返回連線
  • 3、若池中沒有可用連線,則傳送emty訊號喚醒建立執行緒,等待建立執行緒傳送notEmpty訊號,建立完成後取出執行緒池中最後的連線並返回

如下為初始化過程:

關鍵獲取執行緒池程式碼如下:

 DruidConnectionHolder takeLast() throws InterruptedException,SQLException {
        try {
            while (poolingCount == 0) {
                emptySignal(); // send signal to CreateThread create connection
                notEmptyWaitThreadCount++;
                if (notEmptyWaitThreadCount > notEmptyWaitThreadPeak) {
                    notEmptyWaitThreadPeak = notEmptyWaitThreadCount;
                }
                try {
                    notEmpty.await(); // signal by recycle or creator
                } finally {
                    notEmptyWaitThreadCount--;
                }
                notEmptyWaitCount++;
 
                if (!enable) {
                    connectErrorCount.incrementAndGet();
                    throw new DataSourceDisableException();
                }
            }
        } catch (InterruptedException ie) {
            notEmpty.signal(); // propagate to non-interrupted thread
            notEmptySignalCount++;
            throw ie;
        }
 
        decrementPoolingCount();
        DruidConnectionHolder last = connections[poolingCount];
        connections[poolingCount] = null;
 
        return last;
    }
複製程式碼

HikariCP 分析

HikariCP 也是基於java進行開發,但進行了很多細節方面的優化,比如ConcurrentBag無鎖化來減輕建立鎖開銷,自定義FastList進一步精簡程式碼降低jdk自實現的CopyOnWriteArrayList開銷,使用 Javassist 委託實現動態代理在位元組碼層面進行優化等等。在這裡我們來看一下 ConcurrentBag 是如何做到無鎖化的?

ConcurrentBag 內部同時使用了 ThreadLocal 和 CopyOnWriteArrayList 來儲存元素:

  • 1、嘗試從 ThreadLocal中獲取屬於當前執行緒的元素來避免鎖競爭
  • 2、如果沒有可用元素則掃描公共集合、再次從共享的 CopyOnWriteArrayList 中獲取。ThreadLocal 和 CopyOnWriteArrayList 在 ConcurrentBag 中都是成員變數,執行緒間不共享,避免了偽共享。使用專門的AbstractQueuedLongSynchronizer來管理跨執行緒訊號。

獲取連線的核心過程大致如下:

  • 1、嘗試從當前執行緒的ThreadLocal獲取連線
  • 2、ThreadLocal獲取失敗則嘗試從CopyOnWriteArrayList中獲取
  • 3、嘗試通過CAS自旋方式建立新的連線

通過ThreadLocal快取、CopyOnWriteArrayList 再次快取的方式來實現無鎖化,獲取連線的核心程式碼如下:

public T borrow(long timeout,final TimeUnit timeUnit) throws InterruptedException
{
  // Try the thread-local list first
  final List<Object> list = threadList.get();
  for (int i = list.size() - 1; i >= 0; i--) {
     final Object entry = list.remove(i);
     @SuppressWarnings("unchecked")
     final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
     if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE,STATE_IN_USE)) {
        return bagEntry;
     }
  }

  // Otherwise,scan the shared list ... then poll the handoff queue
  final int waiting = waiters.incrementAndGet();
  try {
     for (T bagEntry : sharedList) {
        if (bagEntry.compareAndSet(STATE_NOT_IN_USE,STATE_IN_USE)) {
           // If we may have stolen another waiter's connection,request another bag add.
           if (waiting > 1) {
              listener.addBagItem(waiting - 1);
           }
           return bagEntry;
        }
     }

     listener.addBagItem(waiting);

     timeout = timeUnit.toNanos(timeout);
     do {
        final long start = currentTime();
        final T bagEntry = handoffQueue.poll(timeout,NANOSECONDS);
        if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE,STATE_IN_USE)) {
           return bagEntry;
        }

        timeout -= elapsedNanos(start);
     } while (timeout > 10_000);

     return null;
  }
  finally {
     waiters.decrementAndGet();
  }
}
複製程式碼

連線池對於分散式應用的一些思考及延伸

從上述原始碼分析來看,為了更好的利用連線池,大家對與執行緒池模型進行了大量的優化。

當前應用規模越來越大,為了更好的解決這些問題湧現的如容器化編排、微服務、service mesh等技術。很遺憾現實沒有銀彈,雖然連線池很好,在當前大規模部署的環境下,動則成千上萬的服務很多微小的問題也浮現了出來。

高效能資料庫連線池的內幕一文中指出了在分散式大規模應用中執行緒池的一些問題:

1、執行緒數過多

在分庫分表的場景下,比如128個分庫:32個伺服器,每個伺服器有4個schema,便會新建128個獨立資料庫連線池。假如使用Druid作為執行緒池,光是執行緒池就將產生256個執行緒。執行緒數過多將會導致記憶體佔用較大: 預設1個執行緒會佔用1M的空間,如果是512個執行緒,則會佔用1M*512=512M上下文切換開銷。

2、連線數過多

資料庫的連線資源比較重,並且隨著連線的增加,資料庫的效能會有明顯的下降。DBA一般會限制每個DB建立連線的個數,比如限制為3K 。假設資料庫單臺限制3K,32臺則容量為3K32=96K。如果應用最大,最小連線數均為10,則每個應用總計需要12810=1.28K個連線。那麼資料庫理論上支援的應用個數為96K/1.28K= 80 臺

3、連線不能複用

同一個物理機下面不同的schema完全獨立,連線不能複用。

因此在分散式大規模的場景下,對於連線池模型還有更進一步的優化的空間。唯品會進行的一系列嘗試對於我們有著很大的啟發與借鑑意義。從網際網路發展趨勢來看,目前服務正在朝著單一化職責的方向發展,微服務、容器化為超大規模服務提供了有力的支撐,service mesh 進一步加強了這一趨勢,也對我們提出了更大的挑戰。

感謝