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應用程序訪問數據庫的過程是:
- 裝載數據庫驅動程序;
- 通過jdbc建立數據庫連接;
- 訪問數據庫,執行sql語句;
- 斷開數據庫連接。


這是常用的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在收到連接請求時建立連接的完整過程是:
- 先連接池里建立新的連接(即“邏輯連接”),然后建立該“邏輯連接”對應的“物理連接”。建立“邏輯連接”一定伴隨着建立“物理連接”。
- Data Provider關閉一個連接的完整過程是先關閉“邏輯連接”對應的“物理連接”然后銷毀“邏輯連接”。
- 銷毀“邏輯連接”一定伴隨着關閉“物理連接”,SqlConnection.Open()是向Data Provider請求一個連接Data Provider不一定需要完成建立連接的完整過程,可能只需要從連接池里取出一個可用的連接就可以;
- 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 秒)。 如果池進程在連接超時之前無法滿足請求,將引發異常。
用好連接池將會大大提高應用程序的性能。相反,如果使用不當的話,則百害而無一益。一般來說,應當遵循以下原則:
- 在最晚的時刻申請連接,在最早的時候釋放連接。
- 關閉連接時先關閉相關用戶定義的事務。
- 確保並維持連接池中至少有一個打開的連接。
- 盡力避免池碎片的產生。主要包括集成安全性產生的池碎片以及使用許多數據庫產生的池碎片。
JDBC:
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
感謝以上的大佬們的文章,讓我得以節約時間寫出這篇文章。
