JDBC 连接池
# 30.JDBC 连接池
概述:在操作系统中,创建线程是一个昂贵的操作,如果有大量的小任务需要执行,并且频繁地创建和销毁线程,实际上会消耗大量的系统资源,往往创建和消耗线程所耗费的时间比执行任务的时间还长,所以,为了提高效率,可以用线程池。
类似的,在执行 JDBC 的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁 JDBC 连接的开销就太大了。为了避免频繁地创建和销毁 JDBC 连接,我们可以通过连接池复用已经创建好的连接。
# 使用数据库连接池之前
假设我们不使用数据库连接池,应用程序和数据库建立连接的过程是这样的:
- 首先通过 TCP 协议的三次握手和数据库服务器建立连接,然后发送数据库用户账号密码,等待数据库验证用户身份。
- 完成用户身份验证后,系统才可以提交 SQL 语句到数据库执行。
- 完成一次 SQL 查询后,我们要把连接关闭,关闭连接就需要和数据库通信,告诉它我们要断开连接了,然后再 TCP 四次挥手最后完成关闭。
这个过程中每一次发起 SQL 查询所经历的 TCP 建立连接,数据库验证用户身份,数据库用户登出,TCP 断开连接消耗的等待时间都是可以避免的,这明显是一种浪费。
打个比方,你去网吧去玩游戏,每次去到呢先插网线,然后开机登录游戏,玩了一会儿要去上厕所,你就退出游戏,然后关机拔网线。去完厕所回来就又重新插网线开机登游戏。
# 数据库连接池的概念
每次 SQL 查询都创建链接,查询完后又关闭连接这个做法,对操作系统的开销很大。
合理的做法就应该是系统启动的时候就创建数据库连接,然后需要使用 SQL 查询的时候,就从系统拿出数据库连接对象并提交查询,查询完了就把连接对象还给系统。系统在整个程序运行结束的时候再把数据库连接关闭。
考虑到一般数据库应用都是 Web 多用户并发应用,那么只有一个数据库连接对象肯定不够用,所以系统启动的时候就应该多创建几个数据库连接对象供多个线程使用,这一批数据库连接对象集合在一起就被称之为数据库连接池(Connection Pool)。
数据库连接池就是典型的用空间换时间的思想,系统启动预先创建多个数据库连接对象虽然会占用一定的内存空间,但是可以省去后面每次 SQL 查询时创建连接和关闭连接消耗的时间。
其实不仅仅是数据库有连接池的概念,很多其他技术也用到的连接池的概念。
# JDBC 中的连接池
JDBC 连接池有一个标准的接口 javax.sql.DataSource
,注意这个类位于 Java 标准库中,但仅仅是接口。要使用 JDBC 连接池,我们必须选择一个 JDBC 连接池的实现,这很好理解,因为 JDBC 都只是一套接口而已,而连接池是基于 JDBC 的。接口有如下方法:
- 获取连接:
Connection getConnection()
- 归还连接:
Connection.close()
。如果连接对象 Connection 是从连接池中获取的,那么调用Connection.close()
方法,则不会再关闭连接了,而是归还连接
一般来说,我们也不会自己去实现数据库连接池,而是用数据库厂商提供的。常用的 JDBC 连接池有:
- C3P0:早期开源的 JDBC 连接池。单线程,性能较差,适用于小型系统,代码 600KB 左右。
- Druid:翻译为德鲁伊,几乎是 Java 语言中最好的数据库连接池,高效,由阿里巴巴开发。开源项目,GitHub 地址:alibaba/druid: 为监控而生的数据库连接池 (opens new window)
- HikariCP:非常快,官网地址:brettwooldridge/HikariCP: 光 HikariCP (opens new window)
- .....
因为会了后切换到其他连接池技术也很简单,这里仅仅介绍下 C3P0 和 Druid。
# C3P0
GitHub 地址:swaldman/c3p0 (opens new window)
使用步骤:
先导入数据库驱动 jar 包
导入 jar 包 (两个)
c3p0-0.9.5.2.jar
,mchange-commons-java-0.2.12.jar
定义配置文件:
- 名称: c3p0.properties 或者 c3p0-config.xml
- 路径:直接将文件放在 src 目录下即可,会自动去该路径下寻找配置文件
创建核心对象:数据库连接池对象 ComboPooledDataSource
获取连接:
Connection getConnection()
# 导入依赖
可以从我的 GitHub 仓库里下载 jar 包:
Gitee:lib · /LearnJavaEE - Gitee (opens new window)
GitHub:LearnJavaEE/lib at master · Peter-JXL/LearnJavaEE (opens new window)
# c3p0-config.xml
这里直接给出一个配置文件:连接参数就是 JDBC 连接字符串和用户名密码等,
initialPoolSize:初始化申请的连接数量
maxPoolSize:连接池最大的连接数量,这里配置的比较小,仅仅是演示用,具体多少得看数据库性能
checkoutTimeout:超时时间,单位毫秒,3000 就是 3000 毫秒
<c3p0-config>
<!-- 使用默认的配置读取连接池对象 -->
<default-config>
<!-- 连接参数 -->
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/learnjdbc</property>
<property name="user">learn</property>
<property name="password">learnpassword</property>
<!-- 连接池参数 -->
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">10</property>
<property name="checkoutTimeout">3000</property>
</default-config>
</c3p0-config>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用 c3p0
接下来我们就可以验证下是否可以正常获取 Connection 对象了:
package chapter2JDBC;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
public class JDBCDemo13C3P0 {
public static void main(String[] args) throws Exception{
// 1. 创建数据库连接池对象
DataSource ds = new ComboPooledDataSource();
// 2.获取连接对象
Connection conn = ds.getConnection();
// 3. 查看是否正常获取 Connection,不为 null 则正常
System.out.println(conn);
conn.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 验证连接池配置
我们在配置文件里定义了连接池的一些配置,例如最大连接数量是 10,接下来我们验证下这个配置。
例如,我们申请 11 个会怎么样呢?会等待超时(这里配了 3 秒),然后报错:
package chapter2JDBC;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
public class JDBCDemo14C3P0 {
public static void main(String[] args) throws Exception{
// 1. 创建数据库连接池对象
DataSource ds = new ComboPooledDataSource();
for (int i = 1; i <= 11; i++) {
Connection conn = ds.getConnection();
System.out.println(conn);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
运行结果:
com.mchange.v2.c3p0.impl.NewProxyConnection@30c7da1e [wrapping: com.mysql.cj.jdbc.ConnectionImpl@5b464ce8]
com.mchange.v2.c3p0.impl.NewProxyConnection@19dfb72a [wrapping: com.mysql.cj.jdbc.ConnectionImpl@17c68925]
com.mchange.v2.c3p0.impl.NewProxyConnection@3d24753a [wrapping: com.mysql.cj.jdbc.ConnectionImpl@59a6e353]
com.mchange.v2.c3p0.impl.NewProxyConnection@71be98f5 [wrapping: com.mysql.cj.jdbc.ConnectionImpl@6fadae5d]
com.mchange.v2.c3p0.impl.NewProxyConnection@2d6e8792 [wrapping: com.mysql.cj.jdbc.ConnectionImpl@2812cbfa]
com.mchange.v2.c3p0.impl.NewProxyConnection@506e6d5e [wrapping: com.mysql.cj.jdbc.ConnectionImpl@96532d6]
com.mchange.v2.c3p0.impl.NewProxyConnection@67b64c45 [wrapping: com.mysql.cj.jdbc.ConnectionImpl@4411d970]
com.mchange.v2.c3p0.impl.NewProxyConnection@60f82f98 [wrapping: com.mysql.cj.jdbc.ConnectionImpl@35f983a6]
com.mchange.v2.c3p0.impl.NewProxyConnection@edf4efb [wrapping: com.mysql.cj.jdbc.ConnectionImpl@2f7a2457]
com.mchange.v2.c3p0.impl.NewProxyConnection@6108b2d7 [wrapping: com.mysql.cj.jdbc.ConnectionImpl@1554909b]
Exception in thread "main" java.sql.SQLException: An attempt by a client to checkout a Connection has timed out.
at com.mchange.v2.sql.SqlUtils.toSQLException(SqlUtils.java:118)
at com.mchange.v2.sql.SqlUtils.toSQLException(SqlUtils.java:77)
at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutPooledConnection(C3P0PooledConnectionPool.java:690)
at com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource.getConnection(AbstractPoolBackedDataSource.java:140)
at chapter2JDBC.JDBCDemo14C3P0.main(JDBCDemo14C3P0.java:13)
Caused by: com.mchange.v2.resourcepool.TimeoutException: A client timed out while waiting to acquire a resource from com.mchange.v2.resourcepool.BasicResourcePool@36d64342 -- timeout at awaitAvailable()
at com.mchange.v2.resourcepool.BasicResourcePool.awaitAvailable(BasicResourcePool.java:1467)
at com.mchange.v2.resourcepool.BasicResourcePool.prelimCheckoutResource(BasicResourcePool.java:644)
at com.mchange.v2.resourcepool.BasicResourcePool.checkoutResource(BasicResourcePool.java:554)
at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutAndMarkConnectionInUse(C3P0PooledConnectionPool.java:758)
at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutPooledConnection(C3P0PooledConnectionPool.java:685)
... 2 more
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
如果我们及时归还了数据库连接对象,那么后续的申请连接就能获取到了已有的连接了,例如:
DataSource ds = new ComboPooledDataSource();
for (int i = 1; i <= 11; i++) {
Connection conn = ds.getConnection();
System.out.println(i + ": "+ conn.toString());
if( 5 == i){
conn.close();
}
}
2
3
4
5
6
7
8
9
部分运行结果:可以看到第 5 个和第 6 个申请的对象是同一个(哈希地址一样),也就是用的是同一个连接
5: com.mchange.v2.c3p0.impl.NewProxyConnection@2d6e8792 [wrapping: com.mysql.cj.jdbc.ConnectionImpl@2812cbfa]
6: com.mchange.v2.c3p0.impl.NewProxyConnection@3796751b [wrapping: com.mysql.cj.jdbc.ConnectionImpl@2812cbfa]
2
# 多个数据源
有时候我们需要多个数据源,因此可以在配置文件里定义多个数据源:
<c3p0-config>
<!-- 使用默认的配置读取连接池对象 -->
<default-config>
<!-- 连接参数 -->
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/learnjdbc</property>
<property name="user">learn</property>
<property name="password">learnpassword</property>
<!-- 连接池参数 -->
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">10</property>
<property name="checkoutTimeout">3000</property>
</default-config>
<name-config name="otherc3p0">
<!-- 连接参数 -->
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/learnjdbc</property>
<property name="user">learn</property>
<property name="password">learnpassword</property>
<!-- 连接池参数 -->
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">8</property>
<property name="checkoutTimeout">1000</property>
</name-config>
</c3p0-config>
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
不同数据源用名字来区分,例如第 16 行 <name-config name="otherc3p0">
获取数据源时,如果不传任何参数,则获取的是默认数据源,也就是用 <default-config>
定义的数据源。
如果指定名称,则可以获取指定的数据源:
DataSource ds = new ComboPooledDataSource("otherc3p0");
# Druid
使用步骤
导入 jar 包 druid-1.0.9.jar
定义配置文件:
- 是 properties 形式的
- 可以叫任意名称,可以放在任意目录下
加载配置文件
获取数据库连接池对象,通过工厂来来获取 DruidDataSourceFactory
获取连接:getConnection
# 下载依赖
可以去一个叫做 Maven 仓库的地方下载 jar 包:Maven Repository: com.alibaba » druid (opens new window)
如果你使用 Maven,可以这样配置:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid-version}</version>
</dependency>
2
3
4
5
也可以从我的 GitHub 仓库里下载:
Gitee:lib · /LearnJavaEE - Gitee (opens new window)
GitHub:LearnJavaEE/lib at master · Peter-JXL/LearnJavaEE (opens new window)
# 定义配置文件
例如我们定义一个 druid.properties 文件,文件路径为 src,文件内容如下:里面写的内容相信大家都能看到,这里就不解释了
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql:///learnjdbc
username=learn
password=learnpassword
# 初始化连接数量
initialSize=5
# 最大连接数
maxActive=10
# 最大等待时间
maxWait=3000
2
3
4
5
6
7
8
9
10
11
12
13
# 使用 Druid
接下来我们定义 Druid 来获取 Connection:
package chapter2JDBC;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;
public class JDBCDemo15Druid {
public static void main(String[] args) throws Exception {
Properties pro = new Properties();
InputStream is = JDBCDemo15Druid.class.getClassLoader().getResourceAsStream("durid.properties");
pro.load(is);
DataSource ds = DruidDataSourceFactory.createDataSource(pro);
Connection conn = ds.getConnection();
System.out.println(conn);
conn.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
能正常打印出 Connection 对象,说明获取成功。其他连接池参数就不一一验证了。
# Druid 工具类
如果每次都要创建连接池对象,然后获取连接,还是有点麻烦的,并且连接池应该只有一个。为此,我们还是会定义一个工具类,需求如下:
定义一个类 DruidUtils
提供静态代码块加载配置文件,初始化连接池对象
提供方法
- 获取连接方法:通过数据库连接池获取连接
- 释放资源
- 获取连接池的方法(有的框架里面需要自己去调用获取连接的方法,这里为了通用,因此提供了这样的代码)
完整代码
package chapter2JDBC;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
/**
* Druid 连接池的工具类
*/
public class DruidUtils {
private static DataSource ds;
static {
try {
// 1。加载配置文件
Properties pro = new Properties();
pro.load(DruidUtils.class.getClassLoader().getResourceAsStream("druid.properties"));
ds = DruidDataSourceFactory.createDataSource(pro);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 获取连接
* @return
* @throws SQLException
*/
public static Connection getConnecton() throws SQLException {
return ds.getConnection();
}
/**
* 释放资源
*/
public static void close(Statement statement, Connection conn){
if( null != statement){
try{
statement.close();
}catch (SQLException e){
e.printStackTrace();
}
}
if( null != conn){
try{
conn.close();
}catch (SQLException e){
e.printStackTrace();
}
}
}
/**
* 释放资源
*/
public static void close(ResultSet rs, Statement statement, Connection conn){
if( null != rs){
try{
statement.close();
}catch (SQLException e){
e.printStackTrace();
}
}
close(statement, conn);
}
public static DataSource getDataSourse(){
return ds;
}
}
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
# 总结
数据库连接池是一种复用 Connection
的组件,它可以避免反复创建新连接,提高 JDBC 代码的运行效率,还可以配置连接池的详细参数并监控连接池。
参考
我们为什么要使用数据库连接池? - 知乎 (opens new window)
主流 Java 数据库连接池比较及前瞻 - 知乎 (opens new window)
黑马系列九之 JDBC 入门至精通(c3p0/druid/)_哔哩哔哩_bilibili (opens new window)