數據庫連接池原理介紹
什么是連接池
數據庫連接池負責分配、管理和釋放數據庫連接,它允許應用程序重復使用一個現有的數據庫連接,而不是再重新建立一個。
為什么要使用連接池
數據庫連接是一種關鍵的有限的昂貴的資源,這一點在多用戶的網頁應用程序中體現得尤為突出。 一個數據庫連接對象均對應一個物理數據庫連接,每次操作都打開一個物理連接,使用完都關閉連接,這樣造成系統的 性能低下。
數據庫連接池的解決方案是在應用程序啟動時建立足夠的數據庫連接,並講這些連接組成一個連接池(簡單說:在一個“池”里放了好多半成品的數據庫聯接對象),由應用程序動態地對池中的連接進行申請、使用和釋放。對於多於連接池中連接數的並發請求,應該在請求隊列中排隊等待。並且應用程序可以根據池中連接的使用率,動態增加或減少池中的連接數。
連接池技術盡可能多地重用了消耗內存地資源,大大節省了內存,提高了服務器地服務效率,能夠支持更多的客戶服務。通過使用連接池,將大大提高程序運行效率,同時,我們可以通過其自身的管理機制來監視數據庫連接的數量、使用情況等。
傳統的連接機制與數據庫連接池的運行機制區別
不使用連接池流程
下面以訪問MySQL為例,執行一個SQL命令,如果不使用連接池,需要經過哪些流程。

不使用數據庫連接池的步驟:
TCP建立連接的三次握手
MySQL認證的三次握手
真正的SQL執行
MySQL的關閉
TCP的四次握手關閉
可以看到,為了執行一條SQL,卻多了非常多我們不關心的網絡交互。
優點:
實現簡單
缺點:
- 網絡IO較多
- 數據庫的負載較高
- 響應時間較長及QPS較低
- 應用頻繁的創建連接和關閉連接,導致臨時對象較多,GC頻繁
- 在關閉連接后,會出現大量TIME_WAIT 的TCP狀態(在2個MSL之后關閉)
使用連接池流程

使用數據庫連接池的步驟:
第一次訪問的時候,需要建立連接。 但是之后的訪問,均會復用之前創建的連接,直接執行SQL語句。
優點:
- 較少了網絡開銷
- 系統的性能會有一個實質的提升
- 沒了麻煩的TIME_WAIT狀態
數據庫連接池的工作原理
連接池的工作原理主要由三部分組成,分別為
- 連接池的建立
- 連接池中連接的使用管理
- 連接池的關閉
第一、連接池的建立。一般在系統初始化時,連接池會根據系統配置建立,並在池中創建了幾個連接對象,以便使用時能從連接池中獲取。連接池中的連接不能隨意創建和關閉,這樣避免了連接隨意建立和關閉造成的系統開銷。Java中提供了很多容器類可以方便的構建連接池,例如Vector、Stack等。
第二、連接池的管理。連接池管理策略是連接池機制的核心,連接池內連接的分配和釋放對系統的性能有很大的影響。其管理策略是:
當客戶請求數據庫連接時,首先查看連接池中是否有空閑連接,如果存在空閑連接,則將連接分配給客戶使用;如果沒有空閑連接,則查看當前所開的連接數是否已經達到最大連接數,如果沒達到就重新創建一個連接給請求的客戶;如果達到就按設定的最大等待時間進行等待,如果超出最大等待時間,則拋出異常給客戶。
當客戶釋放數據庫連接時,先判斷該連接的引用次數是否超過了規定值,如果超過就從連接池中刪除該連接,否則保留為其他客戶服務。
該策略保證了數據庫連接的有效復用,避免頻繁的建立、釋放連接所帶來的系統資源開銷。
第三、連接池的關閉。當應用程序退出時,關閉連接池中所有的連接,釋放連接池相關的資源,該過程正好與創建相反。
連接池主要參數
使用連接池時,要配置一下參數
- 最小連接數:是連接池一直保持的數據庫連接,所以如果應用程序對數據庫連接的使用量不大,將會有大量的數據庫連接資源被浪費.
- 最大連接數:是連接池能申請的最大連接數,如果數據庫連接請求超過次數,后面的數據庫連接請求將被加入到等待隊列中,這會影響以后的數據庫操作
- 最大空閑時間
- 獲取連接超時時間
- 超時重試連接次數
連接池需要注意的點
1、並發問題
為了使連接管理服務具有最大的通用性,必須考慮多線程環境,即並發問題。這個問題相對比較好解決,因為各個語言自身提供了對並發管理的支持,像java, c#等等,使用synchronized(java) lock(C#)關鍵字即可確保線程是同步的。
2、事務處理
我們知道,事務具有原子性,此時要求對數據庫的操作符合“ALL-OR-NOTHING”原則,即對於一組SQL語句要么全做,要么全不做。
我們知道當2個線程共用一個連接Connection對象,而且各自都有自己的事務要處理時候,對於連接池是一個很頭疼的問題,因為即使Connection類提供了相應的事務支持,可是我們仍然不能確定那個數據庫操作是對應那個事務的,這是由於我們有2個線程都在進行事務操作而引起的。為此我們可以使用每一個事務獨占一個連接來實現,雖然這種方法有點浪費連接池資源但是可以大大降低事務管理的復雜性。
3、連接池的分配與釋放
連接池的分配與釋放,對系統的性能有很大的影響。合理的分配與釋放,可以提高連接的復用度,從而降低建立新連接的開銷,同時還可以加快用戶的訪問速度。
對於連接的管理可使用一個List。即把已經創建的連接都放入List中去統一管理。每當用戶請求一個連接時,系統檢查這個List中有沒有可以分配的連接。如果有就把那個最合適的連接分配給他(如何能找到最合適的連接文章將在關鍵議題中指出);如果沒有就拋出一個異常給用戶,List中連接是否可以被分配由一個線程來專門管理捎后我會介紹這個線程的具體實現。
4、連接池的配置與維護
連接池中到底應該放置多少連接,才能使系統的性能最佳?系統可采取設置最小連接數(minConnection)和最大連接數(maxConnection)等參數來控制連接池中的連接。比方說,最小連接數是系統啟動時連接池所創建的連接數。如果創建過多,則系統啟動就慢,但創建后系統的響應速度會很快;如果創建過少,則系統啟動的很快,響應起來卻慢。這樣,可以在開發時,設置較小的最小連接數,開發起來會快,而在系統實際使用時設置較大的,因為這樣對訪問客戶來說速度會快些。最大連接數是連接池中允許連接的最大數目,具體設置多少,要看系統的訪問量,可通過軟件需求上得到。
如何確保連接池中的最小連接數呢?有動態和靜態兩種策略。動態即每隔一定時間就對連接池進行檢測,如果發現連接數量小於最小連接數,則補充相應數量的新連接,以保證連接池的正常運轉。靜態是發現空閑連接不夠時再去檢查。
Druid數據庫連接池
Druid 相對於其他數據庫連接池的優點
- 強大的監控特性,通過Druid提供的監控功能,可以清楚知道連接池和SQL的工作情況。
a. 監控SQL的執行時間、ResultSet持有時間、返回行數、更新行數、錯誤次數、錯誤堆棧信息;
b. SQL執行的耗時區間分布。什么是耗時區間分布呢?比如說,某個SQL執行了1000次,其中01毫秒區間50次,110毫秒800次,10100毫秒100次,1001000毫秒30次,1~10秒15次,10秒以上5次。通過耗時區間分布,能夠非常清楚知道SQL的執行耗時情況;
c. 監控連接池的物理連接創建和銷毀次數、邏輯連接的申請和關閉次數、非空等待次數、PSCache命中率等。 - 方便擴展。Druid提供了Filter-Chain模式的擴展API,可以自己編寫Filter攔截JDBC中的任何方法,可以在上面做任何事情,比如說性能監控、SQL審計、用戶名密碼加密、日志等等。
- Druid集合了開源和商業數據庫連接池的優秀特性,並結合阿里巴巴大規模苛刻生產環境的使用經驗進行優化。
1.Druid數據源是什么?
Druid是阿里巴巴開源的一個數據源,主要用於java數據庫連接池,相比spring推薦的DBCP和hibernate推薦的C3P0、Proxool數據庫連接池,Druid在市場上占有絕對的優勢;
2.為什么選擇Druid作為數據庫連接池?
這里直接給出一個鏈接:
大話數據庫連接池
文章從市場占有率、性能上比較C3P0、DBCP、HikariCP和Druid,說明了Druid數據源由於有強大的監控特性、可拓展性等特點值得作者推薦。雖說 HikariCP 的性能比 Druid 高,但是因為 Druid 包括很多維度的統計和分析功能,所以大家都選擇使用Druid 的更多;
3.如何結合SpringBoot使用?
要先知道如何使用一個工具,當然首先要從文檔開始,關於文檔可以從官網知道,可以參考https://github.com/alibaba/druid;
(1) 添加Druid依賴
<properties>
<druid-version>1.1.10</druid-version>
</properties>
<dependencies>
......
<!--alibaba druid datasource-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid-version}</version>
</dependency>
......
</dependencies>
(2) 添加配置
springboot支持yml和properties等配置文件,本文采用application.properties配置。
- application.properties
# DataSource settings
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url = jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF8
spring.datasource.username = root
spring.datasource.password = root
spring.datasource.driverClassName = com.mysql.jdbc.Driver
#連接池的配置信息
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
# 配置獲取連接等待超時的時間
spring.datasource.maxWait=60000
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
# 配置一個連接在池中最小生存的時間,單位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
# 打開PSCache,並且指定每個連接上PSCache的大小
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
# 配置監控統計攔截的filters,去掉后監控界面sql無法統計,'wall'用於防火牆
spring.datasource.filters=stat,wall,log4j
# 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000druid-monitor.properties
- druid-monitor.properties
#是否啟用StatFilter默認值true
spring.datasource.druid.web-stat-filter.enabled=true
#多個白名單IP以逗號分隔
druid.monitor.allow=127.0.0.1
#多個黑名單IP以逗號分隔
druid.monitor.deny=0.0.0.0
#druid監控管理界面登錄帳號
druid.monitor.loginUsername=admin
#druid監控管理界面登錄密碼
druid.monitor.loginPassword=password
#是否開啟重置功能
druid.monitor.resetEnable=false
要知道具體配置有啥影響的話,我找了一篇采坑的文章,可以看一下:
使用druid連接池帶來的坑testOnBorrow=false
(3) 添加配置類
為方便以后拓展,這里提供一個數據源配置接口,druid配置也只是這個接口的一個實現類,方便以后切換不同的數據源;
- DbConfig.java
package com.ijustone.service.core.db.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
public interface DbConfig {
/**
* 定義數據源
*
* @return
* @throws Exception
*/
DataSource dataSource() throws Exception;
/**
* 定義session工廠
*
* @param dataSource
* @return
* @throws Exception
*/
SqlSessionFactory sessionFactory(DataSource dataSource) throws Exception;
/**
* 定義失誤管理器
*
* @param dataSource
* @return
*/
DataSourceTransactionManager transactionManager(DataSource dataSource);
}
下面是druid配置實現類
- MyDruirdConfig.java
package com.ijustone.service.core.db.config.impl;
import com.alibaba.druid.pool.DruidDataSource;
import com.ijustone.service.core.db.config.DbConfig;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
/**
* @author JustOne
* @create 2018-08-01 22:47
*/
@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = MyDruirdConfig.PACKAGE, sqlSessionFactoryRef = "sessionFactory")
public class MyDruirdConfig implements DbConfig {
public static final String PACKAGE = "com.ijustone.service.**.mapper";
public static final String MAPPER = "classpath:com/ijustone/service/**/mapper/**/*Mapper.xml";
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driverClassName}")
private String driverClassName;
@Value("${spring.datasource.initialSize}")
private int initialSize;
@Value("${spring.datasource.minIdle}")
private int minIdle;
@Value("${spring.datasource.maxActive}")
private int maxActive;
@Value("${spring.datasource.maxWait}")
private int maxWait;
@Value("${spring.datasource.testWhileIdle:true}")
private boolean testWhileIdle;
@Value("${spring.datasource.timeBetweenEvictionRunsMillis:60000}")
private int timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.validationQuery}")
private String validationQuery;
/**
* 指明是否在從池中取出連接前進行檢驗,如果檢驗失敗,則從池中去除連接並嘗試取出另一個.<br/>
* 注意: 設置為true后如果要生效,validationQuery參數必須設置為非空字符串
*/
@Value("${spring.datasource.testOnBorrow:true}")
private boolean testOnBorrow;
/**
* 指明是否在歸還到池中前進行檢驗<br/>
* 注意: 設置為true后如果要生效,validationQuery參數必須設置為非空字符串
*/
@Value("${spring.datasource.testOnReturn:false}")
private boolean testOnReturn;
@Value("${spring.datasource.minEvictableIdleTimeMillis:300000}")
private int minEvictableIdleTimeMillis;
/**
* 當開啟時, 將為每個連接創建一個statement池,並且被方法創建的PreparedStatements將被緩存起來:
*/
@Value("${spring.datasource.poolPreparedStatements:false}")
private boolean poolPreparedStatements;
/**
* 不限制 statement池能夠同時分配的打開的statements的最大數量,如果設置為0表示不限制
*/
@Value("${spring.datasource.maxOpenPreparedStatements:10}")
private int maxPoolPreparedStatementPerConnectionSize;
@Value("${spring.datasource.defaultAutoCommit:false}")
private boolean defaultAutoCommit;
@Value("${spring.datasource.filters:stat}")
private String filters;
/**
* 當建立新連接時被發送給JDBC驅動的連接參數
*/
@Value("${spring.datasource.connectionProperties:druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000}")
private String connectionProperties;
/**
* 定義數據源
* 注意@Primary注解表示:自動裝配時當出現多個Bean候選者時,被注解為@Primary的Bean將作為首選者,否則將拋出異常
*
* @return
* @throws Exception
*/
@Bean(name = "dataSource")
@Primary
@Override
public DataSource dataSource() throws Exception {
DruidDataSource datasource = new DruidDataSource();
datasource.setUrl(this.dbUrl);
datasource.setUsername(this.username);
datasource.setPassword(this.password);
datasource.setDriverClassName(this.driverClassName);
datasource.setInitialSize(this.initialSize);
datasource.setMinIdle(this.minIdle);
datasource.setMaxActive(this.maxActive);
datasource.setMaxWait(this.maxWait);
datasource.setTimeBetweenEvictionRunsMillis(this.timeBetweenEvictionRunsMillis);
datasource.setMinEvictableIdleTimeMillis(this.minEvictableIdleTimeMillis);
datasource.setValidationQuery(this.validationQuery);
datasource.setTestWhileIdle(this.testWhileIdle);
datasource.setTestOnBorrow(this.testOnBorrow);
datasource.setTestOnReturn(this.testOnReturn);
datasource.setPoolPreparedStatements(this.poolPreparedStatements);
datasource.setMaxPoolPreparedStatementPerConnectionSize(this.maxPoolPreparedStatementPerConnectionSize);
datasource.setDefaultAutoCommit(this.defaultAutoCommit);
datasource.setFilters(this.filters);
datasource.setConnectionProperties(this.connectionProperties);
return datasource;
}
/**
* 定義session工廠
* 注:ualifier的意思是合格者,通過這個標示,表明了哪個實現類才是我們所需要的,
* 我們修改調用代碼,添加@Qualifier注解,需要注意的是@Qualifier的參數名稱必須為我們之前定義@Service注解的名稱之一!
*
* @param dataSource
* @return
* @throws Exception
*/
@Bean(name = "sessionFactory")
@Primary
@Override
public SqlSessionFactory sessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sessionFactory.setMapperLocations(resolver.getResources(MyDruirdConfig.MAPPER));
return sessionFactory.getObject();
}
/**
* 定義事務管理器
*
* @param dataSource
* @return
*/
@Bean(name = "transactionManager")
@Override
public DataSourceTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
我們配置了Druid的監聽器
- DruidMonitorConfiguration.java
package com.ijustone.service.core.druid.monitor;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@PropertySource(value = "classpath:config/druid-monitor.properties")
@ConfigurationProperties
public class DruidMonitorConfiguration {
@Value("${druid.monitor.allow:127.0.0.1}")
private String allow;
@Value("${druid.monitor.deny}")
private String deny;
@Value("${druid.monitor.loginUsername:admin}")
private String loginUsername;
@Value("${druid.monitor.loginPassword:password}")
private String loginPassword;
@Value("${druid.monitor.resetEnable:false}")
private String resetEnable;
@Bean
public ServletRegistrationBean druidStatViewServlet() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
servletRegistrationBean.addInitParameter("allow", this.allow);
servletRegistrationBean.addInitParameter("deny", this.deny);
servletRegistrationBean.addInitParameter("loginUsername", this.loginUsername);
servletRegistrationBean.addInitParameter("loginPassword", this.loginPassword);
servletRegistrationBean.addInitParameter("resetEnable", this.resetEnable);
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean druidStatFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
4.如何查看監控頁面?
訪問http://localhost:8010/druid就可以了,如果工程集成了SpringSecurity等權限工程的話是需要額外配置的,這里給一個踩坑鏈接:(https://www.2cto.com/kf/201803/731528.html).
