1. 程式人生 > >JAVA和C#中資料庫連線池原理與應用

JAVA和C#中資料庫連線池原理與應用

JAVA和C#中資料庫連線池原理

在現在的網際網路發展中,高併發成為了主流,而最關鍵的部分就是對資料庫操作和訪問,在現在的網際網路發展中,ORM框架曾出不窮, 比如:.Net-Core的EFCore、SqlSugar、Dapper。JAVA的Spring-DataJpa(EntityManager),Mybatis,MybatisPlus等等

但是說到ORM其實本質都是操作最底層的資料庫訪問元件:Ado.net,Jdbc

今天我就來聊一聊這兩個資料庫訪問的連線池原理

在說到Ado.net和jdbc的資料連線池之前,首先我們需要了解資料庫連線池是什麼

連線到資料庫伺服器通常由幾個需要很長時間的步驟組成。 必須建立 物理通道(例如套接字或命名管道),必須與伺服器進行初次握手, 必須分析連線字串資訊,必須由伺服器對連線進行身份驗證,必 須執行檢查以便在當前事務中登記,等等。

實際上,大多數應用程式僅使用一個或幾個不同的連線配置。 這意味著在執行應用程式期間,許多相同的連線將反覆地開啟和關閉。這很耗費Cpu的效能。為了將開啟連線的成本降至最低,ADO.NET使用稱為連線池的優化技術。而Java則是jdbc連線池的優化技術。

一般來說,Java應用程式訪問資料庫的過程是:

  1. 裝載資料庫驅動程式;
  2. 通過jdbc建立資料庫連線;
  3. 訪問資料庫,執行sql語句;
  4. 斷開資料庫連線。

這是常用的Tomcat的資料庫連線導圖和Jdbc進行資料庫連線的步驟

而.Net Framwork/.Net Core應用程式訪問資料庫的過程是由 .NET資料提供程式的四個核心物件:

1.Connection:連線資料庫 2.Command:執行資料庫命令 3.DataReader:負責從資料來源中讀取資料 4.DataAdapter:負責資料集和資料庫的聯絡

這是Ado.net資料庫連線的導圖

Ado.net:

Ado.net連線資料庫的步驟:

1.新建一個數據庫連線字串
string conStr = “Data Source=.;Initial Catalog=MySchoolDB;Integrated Security=True”;
2.引入名稱空間:
using System.Data.SqlClient;
3.建立SqlConnection物件
SqlConnection conn = new SqlConnection(conStr);
4.開啟連線:
conn.Open();
5.關閉連線:
conn.Close();
五、使用Command物件的步驟:
1.建立資料庫連線
SqlConnection conn = new SqlConnection(conStr);
2.定義sql語句
string sql = “insert into Admin values(‘值’)”;
3.建立SqlCommand物件
SqlCommand cmd = new SqlCommand(conn,sql);
4.執行命令
cmd.ExecuteScalar();

我們已經知道了在連線時,如果在一瞬間的訪問量突然激增的情況下,那麼執行緒就會開闢越多的資料庫訪問連線,這時候基本的連線已經不足以應對高併發高QPS的訪問了

這個時候Mircosoft創造了由Data Provider提供的一種資料庫連線池 --Ado.net連線池:它使得應用程式使用的連線儲存在連線池裡而避免每次都要完成建立/關閉連線的完整過程。

Data Provider在收到連線請求時建立連線的完整過程是:

  1. 先連線池裡建立新的連線(即“邏輯連線”),然後建立該“邏輯連線”對應的“物理連線”。建立“邏輯連線”一定伴隨著建立“物理連線”。
  2. Data Provider關閉一個連線的完整過程是先關閉“邏輯連線”對應的“物理連線”然後銷燬“邏輯連線”。
  3. 銷燬“邏輯連線”一定伴隨著關閉“物理連線”,SqlConnection.Open()是向Data Provider請求一個連線Data Provider不一定需要完成建立連線的完整過程,可能只需要從連線池裡取出一個可用的連線就可以;
  4. SqlConnection.Close()是請求關閉一個連線,Data Provider不一定需要完成關閉連線的完整過程,可能只需要把連線釋放回連線池就可以。

現在我寫一段測試程式碼測試不使用連線池的資料庫連線效果: 同時我用windows的效能計數器偵測了Cpu的消耗

class Program
{
    static void Main(string[] args)
    {
        SqlConnection con = new SqlConnection("server=.\\sqlexpress;database=zsw;pooling=true;trusted_connection=true;uid=sa;pwd=zsw158991626ZSW;");
        for (int i = 0; i < 10; i++)
        {
            try
            {
                con.Open();
                Console.WriteLine("開始連線資料庫" + System.Threading.Thread.CurrentThread.Name);
                System.Threading.Thread.Sleep(1000);
            }
            catch (Exception e) { Console.WriteLine(e.Message); }
            finally
            {
                con.Close();
                System.Threading.Thread.Sleep(1000);
            }
        }
        Console.Read();
    }
}

這個時候我的程式碼是開啟了資料庫池連線,而我的連線數只有1,但是當我們去掉Console.Readkey的時候設定pooling=false的時候此時我的資料連線佔用了10個,由於我的電腦sqlserver效能檢測打不開,但是大家可以去網上百度後試試檢視連線數

但是! .Net Core連線了資料庫好像是預設開啟資料連線池,這個我找了半天的文件也沒有結果。

那麼這個pooling是什麼呢?

每當程式需要讀寫資料庫的時候。Connection.Open()會使用ConnectionString連線到資料庫,資料庫會為程式建立 一個連線,並且保持開啟狀態,此後程式就可以使用T-SQL語句來查詢/更新資料庫。當執行到Connection.Close()後,資料庫就會關閉當 前的連線。很好,一切看上去都是如此有條不紊。

但是如果我的程式需要不定時的開啟和關閉連線,(比如說 ASP.Net 或是 Web Service ),例如當Http Request傳送到伺服器的時候、,我們需要開啟Connection 然後使用Select* from Table 返回一個DataTable/DataSet給客戶端/瀏覽器,然後關閉當前的Connection。那每次都Open/Close Connection 如此的頻繁操作對於整個系統無疑就成了一種浪費。

ADO.Net Team就給出了一個比較好地解決方法。將先前的Connection儲存起來,當下一次需要開啟連線的時候就將先前的Connection 交給下一個連線。這就是Connection Pool。

那麼這個pooling是如何工作的呢?

首先當一個程式執行Connection.open()時候,ADO.net就需要判斷,此連線是否支援Connection Pool (Pooling 預設為True),如果指定為False, ADO.net就與資料庫之間建立一個連線(為了避免混淆,所有資料庫中的連線,都使用”連線”描述),然後返回給程式。 如果指定為 True,ADO.net就會根據ConnectString建立一個Connection Pool,然後向Connection Pool中填充Connection(所有.net程式中的連線,都使用”Connection”描述)。填充多少個Connection由Min Pool Size (預設為0)屬性來決定。例如如果指定為5,則ADO.net會一次與SQL資料庫之間開啟5個連線,然後將4個Connection,儲存在 Connection Pool中,1個Connection返回給程式。

當程式執行到Connection.close() 的時候。如果Pooling 為True,ADO.net 就把當前的Connection放到Connection Pool並且保持與資料庫之間的連線。 同時還會判斷Connection Lifetime(預設為0)屬性,0代表無限大,如果Connection存在的時間超過了Connection LifeTime,ADO.net就會關閉的Connection同時斷開與資料庫的連線,而不是重新儲存到Connection Pool中。

(這個設定主要用於群集的SQL 資料庫中,達到負載平衡的目的)。如果Pooling指定為False,則直接斷開與資料庫之間的連線。

然後當下一次Connection.Open() 執行的時候,ADO.Net就會判斷新的ConnectionString與之前儲存在Connection Pool中的Connection的connectionString是否一致。 (ADO.Net會將ConnectionString轉成二進位制流,所 以也就是說,新的ConnectionString與儲存在Connection Pool中的Connection的ConnectionString必須完全一致,即使多加了一個空格,或是修改了Connection String中某些屬性的次序都會讓ADO.Net認為這是一個新的連線,而從新建立一個新的連線。所以如果您使用的UserID,Password的認 證方式,修改了Password也會導致一個Connection,如果使用的是SQL的整合認證,就需要儲存兩個連線使用的是同一個)。

然後 ADO.net需要判斷當前的Connection Pool中是否有可以使用的Connection(沒有被其他程式所佔用),如果沒有的話,ADO.net就需要判斷ConnectionString設 置的Max Pool Size (預設為100),如果Connection Pool中的所有Connection沒有達到Max Pool Size,ADO.net則會再次連線資料庫,建立一個連線,然後將Connection返回給程式。

如果已經達到了 MaxPoolSize,ADO.net就不會再次建立任何新的連線,而是等待Connection Pool中被其他程式所佔用的Connection釋放,這個等待時間受SqlConnection.ConnectionTimeout(預設是15 秒)限制,也就是說如果時間超過了15秒,SqlConnection就會丟擲超時錯誤(所以有時候如果SqlConnection.open()方法拋 出超時錯誤,一個可能的原因就是沒有及時將之前的Connnection關閉,同時Connection Pool數量達到了MaxPoolSize。)

如果有可用的Connection,從Connection Pool 取出的Connection也不是直接就返回給程式,ADO.net還需要檢查ConnectionString的ConnectionReset屬性 (預設為True)是否需要對Connection 最一次reset。這是由於,之前從程式中返回的Connection可能已經被修改過,比如說使用 SqlConnection.ChangeDatabase method 修改當前的連線,此時返回的Connection可能就已經不是連線當前的Connection String指定的Initial Catalog資料庫了。所以需要reset一次當前的連線。但是由於所有的額外檢查都會增大ADO.net Connection Pool 對系統的開銷。

連線池是為每個唯一的連線字串建立的。 當建立一個池後,將建立多個連線物件並將其新增到該池中,以滿足最小池大小的需求。 連線根據需要新增到池中,但是不能超過指定的最大池大小(預設值為 100)。 連線在關閉或斷開時釋放回池中。

總結

在請求 SqlConnection 物件時,如果存在可用的連線,將從池中獲取該物件。 連線要可用,必須未使用,具有匹配的事務上下文或未與任何事務上下文關聯,並且具有與伺服器的有效連結。

連線池程序通過在連線釋放回池中時重新分配連線,來滿足這些連線請求。 如果已達到最大池大小且不存在可用的連線,則該請求將會排隊。 然後,池程序嘗試重新建立任何連線,直至到達超時時間(預設值為 15 秒)。 如果池程序在連線超時之前無法滿足請求,將引發異常。

用好連線池將會大大提高應用程式的效能。相反,如果使用不當的話,則百害而無一益。一般來說,應當遵循以下原則:

  1. 在最晚的時刻申請連線,在最早的時候釋放連線。
  2. 關閉連線時先關閉相關使用者定義的事務。
  3. 確保並維持連線池中至少有一個開啟的連線。
  4. 盡力避免池碎片的產生。主要包括整合安全性產生的池碎片以及使用許多資料庫產生的池碎片。

Jdbc:

說完了Ado.net再來聊聊Jdbc,因為我最近也在學Java所以自然而然地將兩個語言進行了對比

JDBC預設的資料庫連線池

JDBC的API中沒有提供連線池的方法。一些大型的WEB應用伺服器如BEA的WebLogic和IBM的WebSphere等提供了連線池的機制,但是必須有其第三方的專用類方法支援連線池的用法。

JDBC 的資料庫連線池使用 javax.sql.DataSource 來表示,DataSource 只是一個介面,該介面通常由伺服器(Weblogic, WebSphere, Tomcat)提供實現,也有一些開源組織提供實現:

  ①DBCP 資料庫連線池

  ②C3P0 資料庫連線池

  DataSource 通常被稱為資料來源,它包含連線池和連線池管理兩個部分,習慣上也經常把 DataSource稱為連線池

  資料來源和資料庫連線不同,資料來源無需建立多個,它是產生資料庫連線的工廠,因此整個應用只需要一個數據源即可。

  當資料庫訪問結束後,程式還是像以前一樣關閉資料庫連線:conn.close(); 但上面的程式碼並沒有關閉資料庫的物理連線,它僅僅把資料庫連線釋放,歸還給了資料庫連線池。

JDBC的資料庫連線池的工作機制:

資料庫連線池負責分配、管理和釋放資料庫連線的。資料庫連線池在初始化時,會建立一定數量的連線放入連線池中,這些資料庫連線的數量是由最小資料庫連線數量來設定的。無論這些資料庫連線有沒有被使用,連線池一直都將保持有至少有這麼多數量的連線。連線池的最大資料庫連線數量限制了這個連線池佔有的最大連線數,當應用程式向連線池請求的連線數大於這個限制時,這些請求將會被加入到等待佇列中。 資料庫的最小連線數和最大連線數的設定要考慮一下幾個因素:

1) 最小連線數是資料庫連線池會一直保持的資料庫連線,如果當應用程式對資料庫連線的使用不是特別大時,將會有大量的資料庫連線資源被浪費;

2) 最大連線數是指資料庫能申請的最大連線數,如果資料庫連線請求超過這個數時,後面的資料庫連線請求就會被加入到等待佇列,這樣會影響後面的資料庫操作;

3) 如果最小連線數和最大連線數相差太大的話,那麼最先的連線請求會獲利,之後超過最小連線數量的連線就等價於重新建立了一個新的資料庫連線.不過,這些大於最小連線數的資料庫連線在使用完不會馬上被釋放,它將被放到連線池中等待重複使用或是空閒超時後被釋放。

現在我們試試用DBCP的方式連線資料庫

1、首先建立一個maven專案,然後在resources檔案下新建一個db.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mysql?&useSSL=false&serverTimezone=UTC
jdbc.username=root //使用者名稱
jdbc.password=123456 //密碼
initSize=10 //初始化連線數
maxTotal=200 //最大連線數
maxIdle=60 //最大空閒數,資料庫連線的最大空閒時間。超過空閒時間,資料庫連線將被標記為不可用,然後被釋放。設為0表示無限制。

2、接著匯入maven的包依賴

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.19</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-dbcp2</artifactId>
        <version>2.7.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <version>2.7.0</version>
    </dependency>

    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.2</version>
    </dependency>
</dependencies>

直接複製貼上即可,但是請注意你的jdk預設版本必須再8以上!

3、再新建一個JdbcUtil類

package com.jdbc.util;    
import org.apache.commons.dbcp2.BasicDataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;

/**
 * DBCP的方式連結資料庫
 */
public class JdbcUtil {
    private static String driver;
    private static String url;
    private static String username;
    private static String password;
    private static int initSize;
    private static int maxTotal;
    private static  int maxIdle;
    private static BasicDataSource ds;
    static {
        ds = new BasicDataSource();
        Properties cfg=new Properties();
        try { //讀取db.properties檔案
            InputStream in = JdbcUtil.class
                    .getClassLoader()
                    .getResourceAsStream("db.properties");
            cfg.load(in);
            //初始化引數
            driver=cfg.getProperty("jdbc.driver");
            url=cfg.getProperty("jdbc.url");
            username=cfg.getProperty("jdbc.username");
            password=cfg.getProperty("jdbc.password");
            initSize=Integer.parseInt(cfg.getProperty("initSize"));
            maxTotal=Integer.parseInt(cfg.getProperty("maxTotal"));
            maxIdle=Integer.parseInt(cfg.getProperty("maxIdle"));
            in.close();
            //初始化連線池
            ds.setDriverClassName(driver);
            ds.setUrl(url);
            ds.setUsername(username);
            ds.setPassword(password);
            ds.setInitialSize(initSize);
            ds.setMaxTotal(maxTotal);
            ds.setMaxIdle(maxIdle);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
    public static Connection getConnection() {//連線資料庫封裝類
        try {
            /*
             * getConnection()從連線池中獲取的重用
             * 連線,如果連線池滿了,則等待。
             * 如果有歸還的連線線,則獲取重用的連線
             */
            Connection conn = ds.getConnection();
            return conn;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
    public static void close(Connection conn) {//關閉資料庫的連線方法,封裝複雜的關閉過程;
        if(conn!=null) {
            try {
                //將用過的連線歸還到連線池
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

4、我們編寫一個測試類進行驗證

package com.jdbc.service;
import com.jdbc.util.JdbcUtil;

import java.sql.Connection;
import java.sql.SQLException;

public class JdbcTest {
    public static void main(String[] args) {
        try {
            for (int i=0;i<1000;i++){
                Thread a= new Thread(new TestThread(),"執行緒:"+(i+1));
                a.start();
                System.out.println(a.getName()+"已啟動");
            }
        }
        catch (Exception ex){
            ex.printStackTrace();
        }
    }
    private static class TestThread implements Runnable{

        private Connection con= JdbcUtil.getConnection();
        @Override
        public void run() {
            try {
                if (con.isClosed()){
                    System.out.println("連線已經關閉");
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
            finally {
                //JdbcUtil.close(con);
                //System.out.println("\t"+Thread.currentThread().getName()+"已關閉");
            }
        }
    }
}

現在執行測試,發現輸出

端口占據了200個,執行緒池開啟了工作,只不過我沒有釋放連線埠,但是我修改一下db.properties的最大連線數為300,現在我們來看看效果

可以看到我們的資料庫連線已經報錯了,這是為什麼呢?因為我本地的MySQL連線只有200埠,當超過200個埠連線時就會崩潰。這也是常見的資料庫連線效能瓶頸

現在我們關閉連線的程式碼取消註釋,可以看到即使有1000個連線也會快速執行,而且不會佔用多餘的埠

DBCP的方式也是伺服器Tomcat的所使用的方式,所以在tomcat使用資料連線池還是很有必要的,至少能扛得住一般的併發!該資料庫連線池既可以與應用伺服器整合使用,也可由應用程式獨立使用。

總結

JAVA的JDBC和微軟Ado.net其實本質上的差別並不大,因為都是對於資料庫的操作,其根本資料庫的效能最大瓶頸真的就是連結問題嗎?

那麼資料庫的索引,現在的資料庫分庫分表,讀寫分離技術的存在是因為什麼呢?所以,資料庫連線池也是效能優化之一的,未來還有更多的資料庫優化操作等待著人們去探索

比如現在的阿里巴巴的Druid,就是最求精益求精的結果,微軟的ORM也紛紛早都開啟了資料庫連線池的優化,這標誌著未來的網際網路效能瓶頸已經不在局勢與傳統的關係型資料庫了

未來Nosql的流行介入讓高併發更能承擔起網際網路大專案的重任!

其實對於Ado.net和jdbc我並沒有花時間去進行效能比較,我喜歡C#也喜歡Java,優秀的語言本就是互相借鑑,就和我們寫程式碼、學演算法一樣,如果你開始就懂得了如何寫出優秀的程式碼我相信,你也不會在乎語言的效能優勢了。

本文引用:

https://blog.csdn.net/huwei2003/article/details/71459198

https://blog.csdn.net/hliq5399/article/details/73292023

https://blog.csdn.net/weixin_40751299/article/details/81609332

https://www.cnblogs.com/justdoitba/p/8087984.html

https://www.cnblogs.com/albertrui/p/8421791.html

https://blog.csdn.net/L_it123/article/details/88205528

感謝以上的大佬們的文章,讓我得以節約時間寫出這篇文