【Java學習筆記番外篇】——JDBC資料庫連線池
宣告:本文章內容主要摘選自尚矽谷宋紅康JDBC教程、《Java核心卷二》,示例程式碼部分出自本人,更多詳細內容推薦直接觀看以上教程及書籍,若有錯誤之處請指出,歡迎交流。
注意:請先學習前導知識——Mysql基礎知識,JDBC基礎。
上一篇筆記中介紹了JDBC的基礎入門知識,但仍有一部分知識(如操作BLOB型別欄位、批量插入資料)還沒講到,所以待會在連線池的內容講完會補加這部分的內容。
一、資料庫連線池
1.JDBC資料庫連線池的必要性
在使用開發基於資料庫的web程式時,傳統的模式基本是按以下步驟:
- 在主程式(如servlet、beans)中建立資料庫連線
- 進行sql操作
- 斷開資料庫連線
這種模式開發,存在的問題:
- 普通的JDBC資料庫連線使用 DriverManager 來獲取,每次向資料庫建立連線的時候都要將 Connection 載入到記憶體中,再驗證使用者名稱和密碼(得花費0.05s~1s的時間)。需要資料庫連線的時候,就向資料庫要求一個,執行完成後再斷開連線。這樣的方式將會消耗大量的資源和時間。資料庫的連線資源並沒有得到很好的重複利用。若同時有幾百人甚至幾千人線上,頻繁的進行資料庫連線操作將佔用很多的系統資源,嚴重的甚至會造成伺服器的崩潰。
- 對於每一次資料庫連線,使用完後都得斷開。否則,如果程式出現異常而未能關閉,將會導致資料庫系統中的記憶體洩漏,最終將導致重啟資料庫。
- 這種開發不能控制被建立的連線物件數,系統資源會被毫無顧及的分配出去,如連線過多,也可能導致記憶體洩漏,伺服器崩潰。
2.資料庫連線池技術
為解決傳統開發中的資料庫連線問題,可以採用資料庫連線池技術。
資料庫連線池的基本思想:就是為資料庫連線建立一個“緩衝池”。預先在緩衝池中放入一定數量的連線,當需要建立資料庫連線時,只需從“緩衝池”中取出一個,使用完畢之後再放回去。
資料庫連線池負責分配、管理和釋放資料庫連線,它允許應用程式重複使用一個現有的資料庫連線,而不是重新建立一個。
資料庫連線池在初始化時將建立一定數量的資料庫連線放到連線池中,這些資料庫連線的數量是由最小資料庫連線數來設定的。無論這些資料庫連線是否被使用,連線池都將一直保證至少擁有這麼多的連線數量。連線池的最大資料庫連線數量
資料庫連線池技術的優點
1. 資源重用
由於資料庫連線得以重用,避免了頻繁建立,釋放連線引起的大量效能開銷。在減少系統消耗的基礎上,另一方面也增加了系統執行環境的平穩性。
2. 更快的系統反應速度
資料庫連線池在初始化過程中,往往已經建立了若干資料庫連線置於連線池中備用。此時連線的初始化工作均已完成。對於業務請求處理而言,直接利用現有可用連線,避免了資料庫連線初始化和釋放過程的時間開銷,從而減少了系統的響應時間。
3. 新的資源分配手段
對於多應用共享同一資料庫的系統而言,可在應用層通過資料庫連線池的配置,實現某一應用最大可用資料庫連線數的限制,避免某一應用獨佔所有的資料庫資源
4. 統一的連線管理,避免資料庫連線洩漏
在較為完善的資料庫連線池實現中,可根據預先的佔用超時設定,強制回收被佔用連線,從而避免了常規資料庫連線操作中可能出現的資源洩露
3.多種開源的資料庫連線池
請自行下載對應的連線池Jar包以保證可以連線!!!下載後需要將jar包匯入專案中的路徑,Eclipse的匯入方式與先前匯入JDBC的驅動一樣,而IDEA可以在專案中新建一個lib資料夾,將Jar包複製到lib資料夾內,然後選中該Jar包,右鍵選擇"Add as Library",這樣子就搞定了
- JDBC 的資料庫連線池使用 javax.sql.DataSource 來表示,DataSource 只是一個介面,該介面通常由伺服器(Weblogic, WebSphere, Tomcat)提供實現,也有一些開源組織提供實現:
- DBCP 是Apache提供的資料庫連線池。tomcat 伺服器自帶dbcp資料庫連線池。速度相對c3p0較快,但因自身存在BUG,Hibernate3已不再提供支援。
- C3P0 是一個開源組織提供的一個數據庫連線池,速度相對較慢,穩定性還可以。hibernate官方推薦使用
- Proxool 是sourceforge下的一個開源專案資料庫連線池,有監控連線池狀態的功能,穩定性較c3p0差一點
- BoneCP 是一個開源組織提供的資料庫連線池,速度快
- Druid 是阿里提供的資料庫連線池,據說是集DBCP 、C3P0 、Proxool 優點於一身的資料庫連線池,但是速度不確定是否有BoneCP快
- DataSource 通常被稱為資料來源,它包含連線池和連線池管理兩個部分,習慣上也經常把 DataSource 稱為連線池
- DataSource用來取代DriverManager來獲取Connection,獲取速度快,同時可以大幅度提高資料庫訪問速度。
- 特別注意:
- 資料來源和資料庫連線不同,資料來源無需建立多個,它是產生資料庫連線的工廠,因此整個應用只需要一個數據源即可。
- 當資料庫訪問結束後,程式還是像以前一樣關閉資料庫連線:conn.close(); 但conn.close()並沒有關閉資料庫的物理連線,它僅僅把資料庫連線釋放,歸還給了資料庫連線池。
(1)C3P0資料庫連線池
- 獲取連線方式一
//使用C3P0資料庫連線池的方式,獲取資料庫的連線:不推薦
public static Connection getConnection1() throws Exception{
ComboPooledDataSource cpds = new ComboPooledDataSource();
cpds.setDriverClass("com.mysql.jdbc.Driver");
cpds.setJdbcUrl("jdbc:mysql://localhost:3306/test");
cpds.setUser("root");
cpds.setPassword("abc123");
// cpds.setMaxPoolSize(100);
Connection conn = cpds.getConnection();
return conn;
}
- 獲取連線方式二
//使用C3P0資料庫連線池的配置檔案方式,獲取資料庫的連線:推薦
private static DataSource cpds = new ComboPooledDataSource("helloc3p0");
public static Connection getConnection2() throws SQLException{
Connection conn = cpds.getConnection();
return conn;
}
其中,src下的配置檔案為:【c3p0-config.xml】
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<named-config name="helloc3p0">
<!-- 獲取連線的4個基本資訊 -->
<property name="user">root</property>
<property name="password">abc123</property>
<property name="jdbcUrl">jdbc:mysql:///test</property>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<!-- 涉及到資料庫連線池的管理的相關屬性的設定 -->
<!-- 若資料庫中連線數不足時, 一次向資料庫伺服器申請多少個連線 -->
<property name="acquireIncrement">5</property>
<!-- 初始化資料庫連線池時連線的數量 -->
<property name="initialPoolSize">5</property>
<!-- 資料庫連線池中的最小的資料庫連線數 -->
<property name="minPoolSize">5</property>
<!-- 資料庫連線池中的最大的資料庫連線數 -->
<property name="maxPoolSize">10</property>
<!-- C3P0 資料庫連線池可以維護的 Statement 的個數 -->
<property name="maxStatements">20</property>
<!-- 每個連線同時可以使用的 Statement 物件的個數 -->
<property name="maxStatementsPerConnection">5</property>
</named-config>
</c3p0-config>
(2)DBCP資料庫連線池
- DBCP 是 Apache 軟體基金組織下的開源連線池實現,該連線池依賴該組織下的另一個開源系統:Common-pool。如需使用該連線池實現,應在系統中增加如下兩個 jar 檔案:
- Commons-dbcp.jar:連線池的實現
- Commons-pool.jar:連線池實現的依賴庫
- Tomcat 的連線池正是採用該連線池來實現的。該資料庫連線池既可以與應用伺服器整合使用,也可由應用程式獨立使用。
- 資料來源和資料庫連線不同,資料來源無需建立多個,它是產生資料庫連線的工廠,因此整個應用只需要一個數據源即可。
- 當資料庫訪問結束後,程式還是像以前一樣關閉資料庫連線:conn.close(); 但上面的程式碼並沒有關閉資料庫的物理連線,它僅僅把資料庫連線釋放,歸還給了資料庫連線池。
- 配置屬性說明
屬性 | 預設值 | 說明 |
---|---|---|
initialSize | 0 | 連線池啟動時建立的初始化連線數量 |
maxActive | 8 | 連線池中可同時連線的最大的連線數 |
maxIdle | 8 | 連線池中最大的空閒的連線數,超過的空閒連線將被釋放,如果設定為負數表示不限制 |
minIdle | 0 | 連線池中最小的空閒的連線數,低於這個數量會被建立新的連線。該引數越接近maxIdle,效能越好,因為連線的建立和銷燬,都是需要消耗資源的;但是不能太大。 |
maxWait | 無限制 | 最大等待時間,當沒有可用連線時,連線池等待連線釋放的最大時間,超過該時間限制會丟擲異常,如果設定-1表示無限等待 |
poolPreparedStatements | false | 開啟池的Statement是否prepared |
maxOpenPreparedStatements | 無限制 | 開啟池的prepared 後的同時最大連線數 |
minEvictableIdleTimeMillis | 連線池中連線,在時間段內一直空閒, 被逐出連線池的時間 | |
removeAbandonedTimeout | 300 | 超過時間限制,回收沒有用(廢棄)的連線 |
removeAbandoned | false | 超過removeAbandonedTimeout時間後,是否進 行沒用連線(廢棄)的回收 |
- 獲取連線方式一:
public static Connection getConnection3() throws Exception {
BasicDataSource source = new BasicDataSource();
source.setDriverClassName("com.mysql.jdbc.Driver");
source.setUrl("jdbc:mysql:///test");
source.setUsername("root");
source.setPassword("abc123");
//
source.setInitialSize(10);
Connection conn = source.getConnection();
return conn;
}
- 獲取連線方式二:
//使用dbcp資料庫連線池的配置檔案方式,獲取資料庫的連線:推薦
private static DataSource source = null;
static{
try {
Properties pros = new Properties();
InputStream is = DBCPTest.class.getClassLoader().getResourceAsStream("dbcp.properties");
pros.load(is);
//根據提供的BasicDataSourceFactory建立對應的DataSource物件
source = BasicDataSourceFactory.createDataSource(pros);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection4() throws Exception {
Connection conn = source.getConnection();
return conn;
}
其中,src下的配置檔案為:【dbcp.properties】
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true&useServerPrepStmts=false
username=root
password=abc123
initialSize=10
#...
(3)Druid(德魯伊)資料庫連線池
Druid是阿里巴巴開源平臺上一個資料庫連線池實現,它結合了C3P0、DBCP、Proxool等DB池的優點,同時加入了日誌監控,可以很好的監控DB池連線和SQL的執行情況,可以說是針對監控而生的DB連線池,可以說是目前最好的連線池之一。
package com.atguigu.druid;
import java.sql.Connection;
import java.util.Properties;
import javax.sql.DataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
public class TestDruid {
public static void main(String[] args) throws Exception {
Properties pro = new Properties(); pro.load(TestDruid.class.getClassLoader().getResourceAsStream("druid.properties"));
DataSource ds = DruidDataSourceFactory.createDataSource(pro);
Connection conn = ds.getConnection();
System.out.println(conn);
}
}
其中,src下的配置檔案為:【druid.properties】
url=jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
username=root
password=123456
driverClassName=com.mysql.jdbc.Driver
initialSize=10
maxActive=20
maxWait=1000
filters=wall
- 詳細配置引數:
配置 | 預設 | 說明 |
---|---|---|
name | 配置這個屬性的意義在於,如果存在多個數據源,監控的時候可以通過名字來區分開來。 如果沒有配置,將會生成一個名字,格式是:”DataSource-” + System.identityHashCode(this) | |
url | 連線資料庫的url,不同資料庫不一樣。例如:mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto | |
username | 連線資料庫的使用者名稱 | |
password | 連線資料庫的密碼。如果你不希望密碼直接寫在配置檔案中,可以使用ConfigFilter。詳細看這裡:https://github.com/alibaba/druid/wiki/使用ConfigFilter | |
driverClassName | 根據url自動識別 這一項可配可不配,如果不配置druid會根據url自動識別dbType,然後選擇相應的driverClassName(建議配置下) | |
initialSize | 0 | 初始化時建立物理連線的個數。初始化發生在顯示呼叫init方法,或者第一次getConnection時 |
maxActive | 8 | 最大連線池數量 |
maxIdle | 8 | 已經不再使用,配置了也沒效果 |
minIdle | 最小連線池數量 | |
maxWait | 獲取連線時最大等待時間,單位毫秒。配置了maxWait之後,預設啟用公平鎖,併發效率會有所下降,如果需要可以通過配置useUnfairLock屬性為true使用非公平鎖。 | |
poolPreparedStatements | false | 是否快取preparedStatement,也就是PSCache。PSCache對支援遊標的資料庫效能提升巨大,比如說oracle。在mysql下建議關閉。 |
maxOpenPreparedStatements | -1 | 要啟用PSCache,必須配置大於0,當大於0時,poolPreparedStatements自動觸發修改為true。在Druid中,不會存在Oracle下PSCache佔用記憶體過多的問題,可以把這個數值配置大一些,比如說100 |
validationQuery | 用來檢測連線是否有效的sql,要求是一個查詢語句。如果validationQuery為null,testOnBorrow、testOnReturn、testWhileIdle都不會其作用。 | |
testOnBorrow | true | 申請連線時執行validationQuery檢測連線是否有效,做了這個配置會降低效能。 |
testOnReturn | false | 歸還連線時執行validationQuery檢測連線是否有效,做了這個配置會降低效能 |
testWhileIdle | false | 建議配置為true,不影響效能,並且保證安全性。申請連線的時候檢測,如果空閒時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測連線是否有效。 |
timeBetweenEvictionRunsMillis | 有兩個含義: 1)Destroy執行緒會檢測連線的間隔時間2)testWhileIdle的判斷依據,詳細看testWhileIdle屬性的說明 | |
numTestsPerEvictionRun | 不再使用,一個DruidDataSource只支援一個EvictionRun | |
minEvictableIdleTimeMillis | ||
connectionInitSqls | 物理連線初始化的時候執行的sql | |
exceptionSorter | 根據dbType自動識別 當資料庫丟擲一些不可恢復的異常時,拋棄連線 | |
filters | 屬性型別是字串,通過別名的方式配置擴充套件外掛,常用的外掛有: 監控統計用的filter:stat日誌用的filter:log4j防禦sql注入的filter:wall | |
proxyFilters | 型別是List,如果同時配置了filters和proxyFilters,是組合關係,並非替換關係 |
4.資料庫連線池的應用
上面3種連線池,選擇一種之後我們就可以依靠它來連線資料庫,不需要我們自己使用驅動,如果你之前JDBC學得還行,此時就可以試試看自己使用連線池替換驅動。廢話不多說,接下來的程式碼是JDBCUtils工具類,我們之前已經自己New過一次了,這次我們使用連線池替換驅動。
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
/**
* @Description 操作資料庫的工具類
*/
public class JDBCUtils {
//使用Druid資料庫連線池技術
private static DataSource source1;
static{
try {
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("druid.properties");
Properties pros = new Properties();
pros.load(is);
source1 = DruidDataSourceFactory.createDataSource(pros);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection1() throws SQLException {
Connection conn = source1.getConnection();
return conn;
}
}
二、操作BLOB型別欄位
1.MySQL BLOB型別
-
MySQL中,BLOB是一個二進位制大型物件,是一個可以儲存大量資料的容器,它能容納不同大小的資料。
-
插入BLOB型別的資料必須使用PreparedStatement,因為BLOB型別的資料無法使用字串拼接寫的。
-
MySQL的四種BLOB型別(除了在儲存的最大資訊量上不同外,他們是等同的)
-
實際使用中根據需要存入的資料大小定義不同的BLOB型別。
-
需要注意的是:如果儲存的檔案過大,資料庫的效能會下降。
-
如果在指定了相關的Blob型別以後,還報錯:xxx too large,那麼在mysql的安裝目錄下,找my.ini檔案加上如下的配置引數: max_allowed_packet=16M。同時注意:修改了my.ini檔案之後,需要重新啟動mysql服務。
2.向資料表中插入大資料型別
//獲取連線
Connection conn = JDBCUtils.getConnection();
String sql = "insert into customers(name,email,birth,photo)values(?,?,?,?)";
PreparedStatement ps = conn.prepareStatement(sql);
// 填充佔位符
ps.setString(1, "徐海強");
ps.setString(2, "[email protected]");
ps.setDate(3, new Date(new java.util.Date().getTime()));
// 操作Blob型別的變數
FileInputStream fis = new FileInputStream("xhq.png");
ps.setBlob(4, fis);
//執行
ps.execute();
fis.close();
JDBCUtils.closeResource(conn, ps);
3.修改資料表中的Blob型別欄位
Connection conn = JDBCUtils.getConnection();
String sql = "update customers set photo = ? where id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
// 填充佔位符
// 操作Blob型別的變數
FileInputStream fis = new FileInputStream("coffee.png");
ps.setBlob(1, fis);
ps.setInt(2, 25);
ps.execute();
fis.close();
JDBCUtils.closeResource(conn, ps);
4.從資料表中讀取大資料型別
String sql = "SELECT id, name, email, birth, photo FROM customer WHERE id = ?";
conn = getConnection();
ps = conn.prepareStatement(sql);
ps.setInt(1, 8);
rs = ps.executeQuery();
if(rs.next()){
Integer id = rs.getInt(1);
String name = rs.getString(2);
String email = rs.getString(3);
Date birth = rs.getDate(4);
Customer cust = new Customer(id, name, email, birth);
System.out.println(cust);
//讀取Blob型別的欄位
Blob photo = rs.getBlob(5);
InputStream is = photo.getBinaryStream();
OutputStream os = new FileOutputStream("c.jpg");
byte [] buffer = new byte[1024];
int len = 0;
while((len = is.read(buffer)) != -1){
os.write(buffer, 0, len);
}
JDBCUtils.closeResource(conn, ps, rs);
if(is != null){
is.close();
}
if(os != null){
os.close();
}
}
三、批量插入
1.批量執行SQL語句
當需要成批插入或者更新記錄時,可以採用Java的批量更新機制,這一機制允許多條語句一次性提交給資料庫批量處理。通常情況下比單獨提交處理更有效率
JDBC的批量處理語句包括下面三個方法:
- addBatch(String):新增需要批量處理的SQL語句或是引數;
- executeBatch():執行批量處理語句;
- clearBatch():清空快取的資料
通常我們會遇到兩種批量執行SQL語句的情況:
- 多條SQL語句的批量處理;
- 一個SQL語句的批量傳參;
2.高效的批量插入
舉例:向資料表中插入20000條資料
- 資料庫中提供一個goods表。建立如下:
CREATE TABLE goods(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(20)
);
(1)實現層次一:使用PreparedStatement
long start = System.currentTimeMillis();
Connection conn = JDBCUtils.getConnection();
String sql = "insert into goods(name)values(?)";
PreparedStatement ps = conn.prepareStatement(sql);
for(int i = 1;i <= 20000;i++){
ps.setString(1, "name_" + i);
ps.executeUpdate();
}
long end = System.currentTimeMillis();
System.out.println("花費的時間為:" + (end - start));//82340
JDBCUtils.closeResource(conn, ps);
(2)實現層次二
/*
* 修改1: 使用 addBatch() / executeBatch() / clearBatch()
* 修改2:mysql伺服器預設是關閉批處理的,我們需要通過一個引數,讓mysql開啟批處理的支援。
* ?rewriteBatchedStatements=true 寫在配置檔案的url後面
* 修改3:使用更新的mysql 驅動:mysql-connector-java-5.1.37-bin.jar
*
*/
@Test
public void testInsert1() throws Exception{
long start = System.currentTimeMillis();
Connection conn = JDBCUtils.getConnection();
String sql = "insert into goods(name)values(?)";
PreparedStatement ps = conn.prepareStatement(sql);
for(int i = 1;i <= 1000000;i++){
ps.setString(1, "name_" + i);
//1.“攢”sql
ps.addBatch();
if(i % 500 == 0){
//2.執行
ps.executeBatch();
//3.清空
ps.clearBatch();
}
}
long end = System.currentTimeMillis();
System.out.println("花費的時間為:" + (end - start));//20000條:625 //1000000條:14733
JDBCUtils.closeResource(conn, ps);
}
(3)實現層次三
/*
* 層次三:在層次二的基礎上操作
* 使用Connection 的 setAutoCommit(false) / commit()
*/
@Test
public void testInsert2() throws Exception{
long start = System.currentTimeMillis();
Connection conn = JDBCUtils.getConnection();
//1.設定為不自動提交資料
conn.setAutoCommit(false);
String sql = "insert into goods(name)values(?)";
PreparedStatement ps = conn.prepareStatement(sql);
for(int i = 1;i <= 1000000;i++){
ps.setString(1, "name_" + i);
//1.“攢”sql
ps.addBatch();
if(i % 500 == 0){
//2.執行
ps.executeBatch();
//3.清空
ps.clearBatch();
}
}
//2.提交資料
conn.commit();
long end = System.currentTimeMillis();
System.out.println("花費的時間為:" + (end - start));//1000000條:4978
JDBCUtils.closeResource(conn, ps);
}
JDBC還有更多的內容如考慮事務處理後的增刪改查操作、併發問題、DAO等,這些入門並不難,不過我們暫時不需要考慮這些,前面的內容已經足夠我們應對目前遇見的需求了,如果以後有新的需求,恐怕我們已經到了學習Mybatis這些框架的時候了,所以目前還是先把這些掌握就行。Java番外篇JDBC到這裡就全部更新完啦,之後可能會出新的番外篇,未完待續~
最後附一下相關連結:
尚矽谷JDBC教學視訊