资源关闭
还记得上一节中的这段代码么?
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不直接内置数据库连接池遵循的核心原则是:让你的模块遵循单一职责原则,这样可以让整个模块的安全性、扩展性等更好,要小而精,不要大而全。这也是我们设计模块可以借鉴的地方。
