资源关闭
还记得上一节中的这段代码么?
java
try {
if (resultSet != null) resultSet.close();
if (preparedStatement != null) preparedStatement.close();
if (connection != null) connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
这是我们在查询完毕之后,关闭和释放资源。我们都知道数据库可接受的链接是有限的,如果不进行关闭,那么可用连接就会被耗尽,这样新的访问就无法被处理了。更全面的说明如下:
为什么必须关闭数据库连接
-
资源管理
- 解释:数据库连接是有限的资源。每个连接都需要占用数据库服务器的内存、线程和其他系统资源。如果不关闭连接,这些资源将一直被占用,可能导致资源耗尽。
- 后果:当数据库连接耗尽时,新的连接请求将被拒绝,应用程序将无法继续正常工作。
-
性能优化
- 解释:频繁地建立和关闭连接会导致性能下降。每次建立连接都需要进行上述的多个步骤,耗时较长。
- 后果:这会降低应用程序的响应速度和吞吐量。
-
避免资源泄漏
- 解释:如果连接没有被正确关闭,它们会一直保持打开状态,导致资源泄漏。
- 后果:长时间运行的应用程序可能会因为资源泄漏而变得不稳定,甚至崩溃。
-
事务管理
- 解释:未关闭的连接可能会导致事务管理问题。如果连接没有正确关闭,事务可能不会被正确提交或回滚。
- 后果:这可能导致数据不一致和其他事务相关的问题。
-
安全性
- 解释:未关闭的连接可能会被恶意利用,导致安全风险。
- 后果:攻击者可能利用未关闭的连接进行未经授权的操作,导致数据泄露或其他安全问题。
上面出现了多次"泄露"这个词,为什么用这个词呢?因为资源被耗尽这种现象类似于水装满了,从容器中意外流出的场景,就像下面这个图:
给个池子
怎么来解决这个问题呢?
解决任何问题都可以在中间加一层
数据库连接池
产生背景
在早期的数据库应用开发中,每次应用程序需要访问数据库时,都会创建一个新的数据库连接。这种做法存在以下几个问题:
- 性能开销大:建立和关闭数据库连接是一个相对耗时的操作,频繁的连接操作会显著降低应用程序的性能。
- 资源浪费:每次创建新的连接都会消耗系统资源,过多的连接会导致资源耗尽,影响系统的稳定性。
- 并发控制困难:在高并发环境下,频繁创建和关闭连接会导致数据库服务器负载过高,难以有效控制并发访问。
为了解决这些问题,数据库连接池应运而生。连接池预先创建一定数量的数据库连接,并将这些连接保存在一个池中,应用程序需要访问数据库时,可以直接从连接池中获取已建立的连接,使用完毕后再将连接归还到池中,而不是关闭连接。这种方式大大提高了性能和资源利用率。
特性
- 连接复用:连接池中的连接可以被多个请求复用,减少了创建和关闭连接的开销。
- 性能优化:通过预先创建和管理连接,连接池能够显著提高数据库访问的性能。
- 资源管理:连接池能够有效管理数据库连接的生命周期,避免资源浪费。
- 并发控制:通过配置连接池的最大连接数等参数,可以有效控制并发访问的数量,防止数据库服务器过载。
- 简化开发:开发者无需手动管理数据库连接的生命周期,由连接池自动处理,降低了代码复杂度。
场景
- Web应用:在Web应用中,数据库连接池可以显著提高响应速度和并发处理能力。
- 分布式系统:在分布式系统中,连接池能够有效管理多个节点之间的数据库连接,提高系统的整体性能和稳定性。
- 大数据处理:在大数据处理场景中,连接池能够有效管理大量的数据库连接,确保数据处理的高效性和稳定性。
常见的高性能数据库连接池框架
-
HikariCP
- 特点:HikariCP是一个高性能的JDBC连接池,设计目标是成为最快的JDBC连接池。
- 优点:低延迟、高性能、易于配置。
- 适用场景:适用于需要高性能和低延迟的应用场景。
-
C3P0
- 特点:C3P0是一个开源的JDBC连接池,支持JNDI绑定和JMX管理。
- 优点:功能丰富、支持多种数据库。
- 适用场景:适用于需要多种数据库支持和复杂配置的应用场景。
-
Druid
- 特点:Druid是一个阿里巴巴开源的数据库连接池,提供了强大的监控和扩展功能。
- 优点:监控功能强大、支持多种数据库、易于扩展。
- 适用场景:适用于需要监控和扩展功能的应用场景。
-
Tomcat JDBC Pool
- 特点:Tomcat JDBC Pool是Apache Tomcat自带的连接池,轻量级且易于集成。
- 优点:轻量级、易于集成。
- 适用场景:适用于轻量级应用和嵌入式应用。
代码示例
以下是一个使用HikariCP作为数据库连接池的Spring Boot配置示例:
yaml
# application.yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: secret
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10 # 最大连接数
minimum-idle: 5 # 最小空闲连接数
idle-timeout: 30000 # 空闲连接超时时间(毫秒)
connection-timeout: 20000 # 获取连接超时时间(毫秒)
Spring Boot 2.0之后,只要你依赖了spring-boot-starter-jdbc自动配置类,那么在启动之后,会默认使用连接池,且默认连接池就是hikari,即使不显示的配置hikari参数,那么也会使用默认的参数
Hikari数据库连接池的实现原理
1.核心组件
- HikariDataSource :作为应用程序与 HikariCP 交互的主要接口,实现了
javax.sql.DataSource
接口。 - PoolBase:负责管理连接池的基本功能,如创建新连接、维护连接状态等。
- HikariPool :继承自
PoolBase
,是连接池的核心实现类,负责连接的分配、回收以及健康检查。 - ConnectionProxy:代理实际的 JDBC 连接对象,用于拦截和处理连接的操作,确保连接在使用完毕后能正确返回到池中。
2.初始化过程
- 当创建
HikariDataSource
实例时,会根据配置参数初始化 HikariCP 的内部状态。 - 调用
initializePool()
方法启动后台线程(如果启用了连接测试),并预填充一定数量的空闲连接到池中。
3. 获取连接
- 应用程序调用
getConnection()
方法请求连接。 - 如果池中有可用连接,则直接返回给调用者;否则,尝试创建新的物理连接或等待其他线程归还连接。
- 使用完连接后,必须显式地关闭它,这实际上只是将连接归还给池而不是真正关闭。
4. 连接回收
- 当连接被归还给池时,会先进行健康检查(可选)。
- 如果连接仍然有效,则将其标记为可用;若无效则销毁该连接并在需要时补充新的连接。
5. 空闲连接清理
- 定期扫描池中的空闲连接,并根据配置规则移除那些长时间未使用的连接以节省资源。
6.源码分析
java
// HikariDataSource.java
public class HikariDataSource implements DataSource {
private final HikariPool pool;
public HikariDataSource() {
this.pool = new HikariPool(this);
}
@Override
public Connection getConnection() throws SQLException {
return pool.getConnection();
}
}
// HikariPool.java (部分代码)
public class HikariPool extends PoolBase {
private final ConcurrentBag<PoolEntry> connectionBag;
// 获取连接的方法
public Connection getConnection() throws SQLException {
final long startTime = System.nanoTime();
final TimeTracker tracker = new TimeTracker(startTime);
try {
// 尝试从池中获取连接
PoolEntry entry = connectionBag.borrow(timeout, MILLISECONDS);
if (entry == null) {
throw new SQLException("Timeout after " + timeout + "ms waiting for connection.");
}
// 返回代理后的连接
return entry.getConnection();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new SQLException("Interrupted while waiting for a connection.", e);
}
}
}
以上代码片段展示了 HikariCP 中如何通过 HikariDataSource
和 HikariPool
类来管理数据库连接的获取与释放。实际源码更为复杂,包含了更多细节处理逻辑,例如连接超时、异常处理等。这里不做过多阐述,有兴趣的朋友可以私聊共学。
7.获取连接的时序图
应用程序 HikariDataSource HikariPool ConcurrentBag 数据库 Connection getConnection() getConnection() borrow() 返回PoolEntry 返回代理后的Connection 创建新连接 返回新连接 添加新连接到池 返回PoolEntry 返回代理后的Connection alt [池中有可用连接] [池中无可用连接] 使用连接 close() 归还连接 返回PoolEntry到池中 应用程序 HikariDataSource HikariPool ConcurrentBag 数据库 Connection
为什么jdbc中不直接给池子?
那很多童鞋会问了,为啥JDBC不直接提供一个连接池呢?搞这么麻烦,我要一站式服务。其实这是有原因的。如下:
1. 灵活性和可扩展性
- 不同需求:不同的应用程序对连接池的需求可能不同。一些应用程序可能需要高性能的连接池,而另一些可能需要更复杂的连接管理功能。内置连接池可能会限制这种灵活性。
- 第三方库:通过将连接池功能分离到第三方库中,JDBC允许开发者根据具体需求选择合适的连接池实现。例如,HikariCP、C3P0、Druid等都是高性能的连接池库,开发者可以根据自己的需求选择最适合的库。
2. 标准与实现分离
- 标准定义:JDBC作为标准API,主要定义了与数据库交互的基本接口和方法。连接池功能属于更高层次的实现细节,不适合直接包含在标准API中。
- 实现多样性:通过将连接池功能分离,不同的数据库厂商和第三方开发者可以提供多种实现,满足不同场景的需求。这种多样性有助于推动技术的发展和创新。
3. 性能优化
- 定制化:不同的应用场景可能需要不同的性能优化策略。内置连接池可能无法提供针对特定场景的最佳优化,而第三方库可以根据具体需求进行定制和优化。
- 轻量级:JDBC作为基础API,保持轻量级有助于提高整体性能。内置连接池可能会增加不必要的开销,影响JDBC的性能表现。
4. 复杂性管理
- 简化核心:JDBC的核心功能是提供数据库访问的基本接口和方法。如果将连接池功能内置,会增加JDBC的复杂性,使得API更加庞大和难以维护。
- 模块化设计:通过模块化设计,JDBC保持了核心功能的简洁性,而将连接池等功能交由第三方库实现,使得整个生态系统更加灵活和高效。
5. 历史原因
- 早期设计:JDBC的设计始于1997年,当时连接池技术尚未成熟。随着时间的推移,连接池技术得到了快速发展,形成了多种成熟的第三方库。
- 社区贡献:连接池功能主要由社区贡献和维护,这种方式促进了技术的快速迭代和优化。
6. 兼容性
- 跨平台:JDBC作为跨平台的标准API,内置连接池可能会导致兼容性问题。第三方库可以针对不同的平台和环境进行优化,确保更好的兼容性。
- 版本管理:通过将连接池功能分离,可以独立管理连接池库的版本,避免与JDBC版本之间的冲突和兼容性问题。
7. 安全性
- 隔离性:将连接池功能分离可以提高安全性,避免核心API中引入不必要的复杂性和潜在的安全漏洞。
- 模块化安全:第三方库可以独立进行安全审计和更新,确保连接池功能的安全性。
JDBC不直接内置数据库连接池遵循的核心原则是:让你的模块遵循单一职责原则,这样可以让整个模块的安全性、扩展性等更好,要小而精,不要大而全。这也是我们设计模块可以借鉴的地方。