參考資料

前言

JDBC的全名是Java DataBase Connectivity,主要適用於Java程式語言 和 資料庫 之間連接的API標準,你也可以說他是Library,這裡的API到底指哪些事情呢?

  1. 連接到資料庫
  2. 創建, 執行 SQL

JDBC 架構


JDBC API提供以下接口和類:

  • Driver Manager: 管理資料庫驅動程式的列表,可以將Java Application的連接請求,與資料庫驅動程式進行配對,以建立資料庫的連接
  • Driver: 處理與資料庫的通信,很少會直接與Driver溝通,而是會透過Driver Manager來管理這個對象。
  • Connection: 連繫數據庫的方法
  • Statement: 用於執行SQL語句,並返回結果。
  • ResultSet: 用於保存從數據庫中獲取的結果集
  • SQLException: 此類處理數據庫應用程序中發生的任何錯誤

可以透過以下範例,來了解 JDBC API接口的用途:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 參考 https://medium.com/learning-from-jhipster/14-%E6%B7%B1%E5%85%A5-jdbc-connection-pool-%E4%B8%A6%E5%B0%8E%E5%85%A5-h2-db-939adee9c50
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class SQLDatabaseConnection {

// Connect to your database.
// Replace server name, username, and password with your credentials
public static void main(String[] args) {
// 1. 載入 SQLServer 的 JDBC Driver
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");

// 2. 編輯連線協定,
String connectionUrl =
"jdbc:sqlserver://yourserver.database.windows.net:1433;"
+ "database=AdventureWorks;"
+ "user=yourusername@yourserver;"
+ "password=yourpassword;";

// 用來接住回傳結果
ResultSet resultSet = null;

// https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
try (
// 3. 建立連線
Connection connection = DriverManager.getConnection(connectionUrl);
// 4. 建立Statement物件供提交SQL命令使用
Statement statement = connection.createStatement();
) {

// Create and execute a SELECT SQL statement.
String selectSql = "SELECT TOP 10 Title, FirstName, LastName from SalesLT.Customer";
// 5. 提交SQL命令,並取得結果
resultSet = statement.executeQuery(selectSql);

// Print results from select statement
while (resultSet.next()) {
System.out.println(resultSet.getString(2) + " " + resultSet.getString(3));
}

// 結束工作
// 6. 釋放資源,關閉Connection連線以及Statement
}
catch (SQLException e) {
e.printStackTrace();
}
}
}

JDBC 連線

通常要進行資料庫連線,需要以下步驟:

  1. 設定Driver: 載入 SQLServer 的 JDBC Driver
  2. 建立連線: (需要呼叫 DriverManager.getConnection()),開啟 TCP socket
  3. 提交SQL: 建立 Statement ,並提交 SQL 命令列
  4. 關閉連線: 關閉 TCP socket

然而每一次需要與資料庫進行溝通時,都要這樣開開關關,對於系統來說就會造成許多不必要的負擔。

如果我們可以於每次連線時,將那些一直重複的動作提取出來,那麼就可以減少對系統的負擔,並且加快程式的執行效能。這就是我們接下來要談的Connection Pool

Connection Pool

Connection Pool是使用「Object Pool Pattern」的設計模式來實現的。先讓我們來一段wiki的說明吧!

物件池(英語:object pool pattern)是一種設計模式。一個物件池包含一組已經初始化過且可以使用的物件,而可以在有需求時創建和銷毀物件。池的用戶可以從池子中取得物件,對其進行操作處理,並在不需要時歸還給池子而非直接銷毀它。這是一種特殊的工廠物件。

簡單來說,就是將一些常用的物件,事先建立好,並且放在一個池子裡面,當有需要時,就可以直接從池子裡面取出物件,而不需要每次都重新建立物件。使用物件池可以獲得顯著的效能提升。從池子中取得物件的時間是可預測的,但新建一個實例所需的時間是不確定。

下圖顯示了客戶端如何使用池中的連接來連接到數據源。主要工作是:

  1. 管理可用的connection
  2. 分配connection
  3. 關閉connection

那我們先來看UML圖吧!

  • Object Pool Class: 是Singleton,因此有一個private的constructor,並且有一個static的getInstance()方法,用來取得Object Pool的實例。
  • acquireReusable(): 用來取得Object Pool中的物件,如果沒有物件可以取得,則會建立新的物件。
  • releaseReusable(): 用來從client手中釋放Object物件,並且把該object拉回可用對象(avaliable)。
  • setMaxPoolSize(): 用來設定Object Pool的最大容量。

JDBC Connection Pool Example

一個簡單的 JDBC Connection Pool 可以參考Azure的做法,你可以在裡面看到一個 Connection Pool 是怎麼實做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;

public class MySQLConnectionPool {
// 連線用的必要資訊
private String databaseUrl;
private String userName;
private String password;
// pool 的大小
private int maxPoolSize = 10;
// 目前連線的數量
private int connNum = 0;

private static final String SQL_VERIFYCONN = "select 1";

// 目前可用的Connection
Stack<Connection> freePool = new Stack<>();
// 目前正在使用的Connection
Set<Connection> occupiedPool = new HashSet<>();

/**
* Constructor: 創建一個Connection Pool
*
* @param databaseUrl
* The connection url
* @param userName
* user name
* @param password
* password
* @param maxSize
* max size of the connection pool
*/
public MySQLConnectionPool(String databaseUrl, String userName,
String password, int maxSize) {
this.databaseUrl = databaseUrl;
this.userName = userName;
this.password = password;
this.maxPoolSize = maxSize;
}

/**
* Get an available connection
*
* @return An available connection
* @throws SQLException
* Fail to get an available connection
*/
public synchronized Connection getConnection() throws SQLException {
Connection conn = null;

// 沒辦法在建立新的連線 (已達到最大連線數)
if (isFull()) {
throw new SQLException("The connection pool is full.");
}

// 回傳freePool中的連線,並且標記該連線為occupied
conn = getConnectionFromPool();

// 如果conn是空的表示freePool裡面沒有任何object,因此我們需要建立新的連線
if (conn == null) {
conn = createNewConnectionForPool();
}

// 有些database一段時間沒使用會自動斷連線,所以我們可以確認一下從freePool取出的connection是否還有連線著
// For Azure Database for MySQL, if there is no action on one connection for some
// time, the connection is lost. By this, make sure the connection is
// active. Otherwise reconnect it.
conn = makeAvailable(conn);
return conn;
}

/**
* 這邊注意的是,要使用synchronized才會把singleton的object lock住,確保不會有多個thread同時還回去多個connection
* client使用完,要還回去Pool中
*
* Return a connection to the pool
*
* @param conn
* The connection
* @throws SQLException
* When the connection is returned already or it isn't gotten
* from the pool.
*/
public synchronized void returnConnection(Connection conn)
throws SQLException {
// 如果conn是空的,表示client沒有使用connection,因此不需要回傳
if (conn == null) {
throw new NullPointerException();
}
// 檢查 conn 是否在 occupiedPool 中,並且可以從 occupiedPool 中移除,如果不是,則拋出 SQLException
if (!occupiedPool.remove(conn)) {
throw new SQLException(
"The connection is returned already or it isn't for this pool");
}
// 如果沒跳Exception則已經從occupied移除,可以放回 freePool 中
freePool.push(conn);
}

/**
* 也要使用synchronized,確保資源被lock住,以正確做檢查freePool是否已經塞滿
*
* Verify if the connection is full.
*
* @return if the connection is full
*/
private synchronized boolean isFull() {
return ((freePool.size() == 0) && (connNum >= maxPoolSize));
}

/**
* 因為connection必須是singleton才不會無謂的一直建立資源
* 因此有一個private的constructor,確保只有透過getInstance()才能取得實例
*
* Create a connection for the pool
*
* @return the new created connection
* @throws SQLException
* When fail to create a new connection.
*/
private Connection createNewConnectionForPool() throws SQLException {
Connection conn = createNewConnection();
connNum++;
occupiedPool.add(conn);
return conn;
}

/**
* 創建連線,必須是private,確保只有透過createNewConnectionForPool()才能取得實例
*
* Crate a new connection
*
* @return the new created connection
* @throws SQLException
* When fail to create a new connection.
*/
private Connection createNewConnection() throws SQLException {
Connection conn = null;
conn = DriverManager.getConnection(databaseUrl, userName, password);
return conn;
}

/**
* 從freePool中取出一個連線,並且放入occupiedPool
*
* Get a connection from the pool. If there is no free connection, return
* null
*
* @return the connection.
*/
private Connection getConnectionFromPool() {
// 先reset
Connection conn = null;
// 如果有空閒的連線,就從freePool中取出,並且放入佔用中的occupiedPool
if (freePool.size() > 0) {
conn = freePool.pop();
occupiedPool.add(conn);
}
// 回傳連線
return conn;
}

/**
* 檢查連線是否還連線著,如果沒有則重新連線
*
* Make sure the connection is available now. Otherwise, reconnect it.
*
* @param conn
* The connection for verification.
* @return the available connection.
* @throws SQLException
* Fail to get an available connection
*/
private Connection makeAvailable(Connection conn) throws SQLException {

// 透過下Query來確認連線是否還連線著
if (isConnectionAvailable(conn)) {
return conn;
}

// 如果發現沒有連線,就把該連線從occupiedPool中移除,並且關閉該連線
occupiedPool.remove(conn);
connNum--;
conn.close();

// 然後重新建立一個新的,並且放入occupiedPool
conn = createNewConnection();
occupiedPool.add(conn);
connNum++;
return conn;
}

/**
* 確認一下目前連線是否還連線著,可以透過下Query來確認
*
* By running a sql to verify if the connection is available
*
* @param conn
* The connection for verification
* @return if the connection is available for now.
*/
private boolean isConnectionAvailable(Connection conn) {
try (Statement st = conn.createStatement()) {
st.executeQuery(SQL_VERIFYCONN);
return true;
} catch (SQLException e) {
return false;
}
}

// Just an Example
public static void main(String[] args) throws SQLException {
Connection conn = null;
// 建立資源池
MySQLConnectionPool pool = new MySQLConnectionPool(
"jdbc:mysql://mysqlaasdevintic-sha.cloudapp.net:3306/<Your DB name>",
"<Your user>", "<Your Password>", 2);
try {
// 取得連線
conn = pool.getConnection();

// 準備一個Query來執行
try (Statement statement = conn.createStatement())
{
ResultSet res = statement.executeQuery("show tables");
System.out.println("There are below tables:");
while (res.next()) {
String tblName = res.getString(1);
System.out.println(tblName);
}
}
}
finally {
if (conn != null) {
// 釋放連線
pool.returnConnection(conn);
}
}
}

}

關於第三方套件Connection Pool

現在我們可以理解 Connection Pool 的運作原理了,其實已經有許多的第三方套件供我們使用,而且這些第三方的 Connection Pool 套件也已經非常成熟,在效能上以及減少重工的基礎上。

會發展出不同的第三方的 Pool 主要原因就是因為效能,因為 JDBC 針對JDBC Driver 的介面有做了規範,因此各家廠商做出來的 Connection Pool 也都會符合 JDBC Driver Interface 的 API,也就是 API 都相同,差別就在於不同第三方套件的內部實做所影響的效能。

目前最厲害效能最好的第三方Connection Pool是HikariCP,而且它的效能是其他的Connection Pool的兩倍以上。
下圖中:

  • Connection Cycle 比較連線速度,表示從 DataSource.getConnection()Connection.close() 結束。Hikari可以在1毫秒內完成4萬到5萬次的連線
  • Statement Cycle 比較執行sql速度,表示 Connection.prepareStatement()Statement.execute()Statement.close()。執行sql的速度上,在1毫秒內可以完成約1萬5000次的cycle。比較可以跟的上的大概就只有 Tomcat 的 JDBC Connection Pool ,其他的效能也就不值一提了。

想了解為什麼這麼快可以讀這篇:為什麼hikari這麼快的原因

關於Spring boot使用的第三方套件

前面說明了JDBC與資料庫進行連線的方式,也說明了 Connection Pool 的重要性。而在 Spring Boot 他幫我們整合了一系列最常使用的工具,當然對於 JDBC 或是 Connection Pool 或是 JPA 等等相關的內容也是一併的簡化配置作業。

因此現在只需要簡單的 Import Dependencies 並加上 Properties 的設定,就可以快速又方便的與資料庫進行連線。我們如果要透過Java的應用程式跟資料庫做溝通,通常一定會import這個套件spring-boot-starter-jdbc

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

但是現在主要使用 spring-boot-starter-data-jdbc,多了一個data的單字!

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

spring boot starter (data) jdbc

如果我們使用 spring-boot-starter-jdbcspring-boot-starter-data-jpa “starters”,我們會自動獲得對 HikariCP 的依賴關係。 (請參閱下面的快照)

有沒有data的套件到底差異在哪裡呢?如果你是使用JPA進行開發,那最常用的就是spring-boot-starter-data-jdbc

  • spring-boot-starter-jdbc: 這個套件是最基礎的包,如果你只是要使用JDBC的話,那就只需要引入這個套件就可以了。
  • spring-boot-starter-data-jdbc: 這個套件是在spring-boot-starter-jdbc的基礎上,還提供了curdReporitory的快速開發接口,如果你要使用JPA的話,那就需要引入這個套件。

spring-jdbc (from: https://blog.csdn.net/assember/article/details/121976290)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

public class RunAQuery {

private JdbcTemplate jdbcTemplate;

// 只提供基本的連線
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}

public int getCount() {
return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class);
}

public String getName() {
return this.jdbcTemplate.queryForObject("select name from mytable", String.class);
}
}

spring-data-jdbc 除了基礎的資料庫連接還提供CrudReporitoy

1
2
3
4
import org.springframework.data.repository.CrudRepository;
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
}

properties設定

如果你有仔細去看看這個 Starter 的內容,會發現他其實主要包含兩個Dependency:

  1. spring-JDBC: Spring針對JDBC封裝後的Library,如果使用這個套件必須在Properties(application.yml)設定資料庫的連線資訊。
  2. HikariCP: Spring Boot將HikariCP做為預設的Connection Pool,並包含在spring-boot-starter-jdbc中。(人家效率太高了)
1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>

可以參考Spring Boot - Data Properties 的設定方式:

1
2
3
4
5
6
7
8
spring:
datasource:
# 如果想指定自己的ConnectionPool應該就要從這裡下手
type: 設定ConnectionPool的DataSource (或從classPath中自動檢測)
url: 設定jdbc連線至資料庫的url
username: 設定jdbc連線至資料庫的帳號
password: 設定jdbc連線至資料庫的密碼
driver-class-name: 設定JDBC的Driver (或依照url自動檢測)

spring.datasource.type

因為導入了spring-boot-starter-data-jdbc,所以我們可以直接使用spring.datasource.type來設定我們要使用的Connection Pool,預設上spring boot會從classpath中自動檢測,如果沒有找到的話就會使用HikariCP。
如果你想要知道知道他是如何自動偵測的,或是可以自動偵測哪些 class name,可以參考 DataSourceBuilder.java — Source Code,或參考下方程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  private static <T extends DataSource> MappedDataSourceProperties<T> lookupPooled(ClassLoader classLoader,
Class<T> type) {
MappedDataSourceProperties<T> result = null;
result = lookup(classLoader, type, result, "com.zaxxer.hikari.HikariDataSource",
HikariDataSourceProperties::new);
result = lookup(classLoader, type, result, "org.apache.tomcat.jdbc.pool.DataSource",
TomcatPoolDataSourceProperties::new);
result = lookup(classLoader, type, result, "org.apache.commons.dbcp2.BasicDataSource",
MappedDbcp2DataSource::new);
result = lookup(classLoader, type, result, "oracle.ucp.jdbc.PoolDataSourceImpl",
OraclePoolDataSourceProperties::new, "oracle.jdbc.OracleConnection");
result = lookup(classLoader, type, result, "com.mchange.v2.c3p0.ComboPooledDataSource",
ComboPooledDataSourceProperties::new);
return result;
}

在這個片段程式碼中,我們可以看到 Spring Boot 會自動偵測如下幾個 Connection Pool,但是預設是HikariDataSource

  • org.springframework.jdbc.datasource.SimpleDriverDataSource
  • com.zaxxer.hikari.HikariDataSource
  • org.apache.tomcat.jdbc.pool.DataSource
  • oracle.ucp.jdbc.PoolDataSourceImpl
  • oracle.jdbc.OracleConnection
  • etc.

If you use the spring-boot-starter-jdbc or spring-boot-starter-data-jpa “starters”, you automatically get a dependency to HikariCP.

org.springframework.jdbc.datasource.SimpleDriverDataSource 查看他的API說明文件可以發現上面寫NOTE: This class is not an actual connection pool; it does not actually pool Connections表示:這個類別並不是真正的Connection Pool,他並不會真的去做Connection Pool的動作。