part3-connection-lifecycle
约 3527 字大约 12 分钟
2026-05-24
系列文章:《深入Spring事务与数据源:从注解到数据库连接的全链路解析》
系列二第3篇:Druid连接获取与回收完整源码解析
基于版本:Druid 1.2.x、Spring Framework 5.3.x
前置阅读:系列二第2篇:Druid连接池架构与核心流程
前言
上一篇文章咱们把Druid的整体架构搞清楚了,知道了连接池是怎么初始化的,后台线程是怎么工作的。
但是,咱们还没看到最核心的部分——连接是怎么获取的?又是怎么归还的?
你可能觉得:"这有啥难的,getConnection()拿连接,close()还连接,不就完了吗?"
嘿,还真没这么简单!你想啊:
- 如果连接池里没有空闲连接怎么办?
- 如果连接池满了怎么办?
- 如果获取连接的线程太多,怎么保证公平?
close()真的是关闭连接吗?还是归还到池中?- 连接泄漏是怎么检测的?
这些问题,都藏在getConnection()和close()的源码里。今天,咱们就来把这两个方法的完整执行链路扒个明明白白。
而且我敢说,看完这篇文章,你对连接池的理解就能超过90%的Java开发!
好了,废话不多说,开干!
一、回顾一下:Spring Boot是怎么配置Druid的?
还是老规矩,快速回顾一下自动装配的入口。
核心入口:DruidDataSource的Bean创建
@Configuration
public class DruidDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
// 通过@ConfigurationProperties自动绑定配置
return dataSource;
}
}配置文件:
spring:
datasource:
druid:
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
max-active: 20
max-wait: 60000好了,回顾完毕,正式进入咱们的主题!
二、getConnection:获取连接的完整流程
好了,现在来看重头戏——getConnection()方法。
核心方法1:getConnection
文件位置:druid/src/main/java/com/alibaba/druid/pool/DruidDataSource.java
@Override
public DruidPooledConnection getConnection() throws SQLException {
// 调用带超时时间的方法
return getConnection(maxWait);
}
@Override
public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
// ========== 第一步:检查连接池状态 ==========
init(); // 如果没有初始化,先初始化
// 检查连接池是否被禁用
if (disable) {
throw new DataSourceDisableException("dataSource disable");
}
// ========== 第二步:创建连接持有者 ==========
DruidConnectionHolder holder;
// ========== 第三步:获取连接(核心逻辑)==========
try {
// ★★★★★ 核心:调用pollLast获取连接 ★★★★★
holder = pollLast(nanos);
} catch (InterruptedException e) {
throw new SQLException("interrupted", e);
}
// ========== 第四步:创建代理连接 ==========
if (holder == null) {
// 如果获取不到连接,抛异常
throw new GetConnectionTimeoutException(
"wait millis " + maxWaitMillis + ", active " + activeCount);
}
// ★★★★★ 核心:创建DruidPooledConnection(代理对象)★★★★★
DruidPooledConnection poolableConnection = new DruidPooledConnection(holder);
return poolableConnection;
}这个方法看着简单,但核心逻辑都在pollLast()方法里。
关键点:返回的不是Connection,而是DruidPooledConnection!这是个代理对象,内部包装了真实的连接。
为什么要用代理?因为要拦截close()方法,改为归还到池中,而不是真的关闭。
扩展场景1:为什么不直接返回Connection?
// 如果直接返回Connection
Connection conn = dataSource.getConnection();
conn.close(); // 真的关闭了连接,没法复用!
// 返回代理对象
DruidPooledConnection conn = dataSource.getConnection();
conn.close(); // 拦截close(),归还到池中三、pollLast:从连接池获取连接的核心方法
好,现在来看pollLast()方法,这是获取连接的核心逻辑:
核心方法2:pollLast
文件位置:druid/src/main/java/com/alibaba/druid/pool/DruidDataSource.java
private DruidConnectionHolder pollLast(long nanos) throws InterruptedException {
// ========== 第一步:计算超时时间 ==========
long estimate = nanos;
long startEstimate = estimate;
// ========== 第二步:循环获取连接 ==========
for (;;) {
// 获取锁
lock.lock();
try {
// ========== 第三步:检查是否有空闲连接 ==========
if (poolingCount > 0) {
// ★★★★★ 有空闲连接,直接获取 ★★★★★
// poolingCount减1
DruidConnectionHolder holder = connections[--poolingCount];
// 活跃连接数加1
activeCount++;
// 返回连接
return holder;
}
// ========== 第四步:如果没有空闲连接,检查是否可以创建 ==========
if (activeCount < maxActive) {
// 可以创建新连接
// 信号通知创建线程
emptySignal();
}
// ========== 第五步:如果不能创建,等待 ==========
if (estimate <= 0) {
// 超时,返回null
return null;
}
// 等待连接可用
notEmpty.await(estimate, TimeUnit.NANOSECONDS);
// 重新计算剩余等待时间
estimate = notEmpty.getWaitNanos();
} finally {
lock.unlock();
}
}
}这个方法的逻辑是这样的:
- 有空闲连接:直接从数组获取,返回
- 没有空闲连接,但未超过maxActive:信号通知创建线程创建新连接,然后等待
- 没有空闲连接,且超过maxActive:等待其他线程归还连接
关键点1:connections[--poolingCount]这行代码
// 假设有5个空闲连接
poolingCount = 5
connections[0] → Connection1
connections[1] → Connection2
connections[2] → Connection3
connections[3] → Connection4
connections[4] → Connection5
// 获取连接
DruidConnectionHolder holder = connections[--poolingCount];
// poolingCount先减1,变成4
// 然后获取connections[4],也就是Connection5
// 现在连接池的状态:
poolingCount = 4
connections[0] → Connection1
connections[1] → Connection2
connections[2] → Connection3
connections[3] → Connection4
connections[4] → Connection5 (但poolingCount=4,所以这个不算空闲)看到没?获取连接是从数组的末尾获取的!
关键点2: otEmpty.await()`等待机制
// 如果没有空闲连接,且达到maxActive
notEmpty.await(estimate, TimeUnit.NANOSECONDS);
// 这里会释放锁,并等待
// 直到其他线程归还连接,调用notEmpty.signal()这就是经典的生产者-消费者模型中的等待机制。
扩展场景2:如果等待超时了怎么办?
// 计算超时时间
long nanos = TimeUnit.MILLISECONDS.toNanos(maxWaitMillis);
// 等待
notEmpty.await(nanos, TimeUnit.NANOSECONDS);
// 如果超时,返回null
if (estimate <= 0) {
return null;
}
// 调用方会抛异常
throw new GetConnectionTimeoutException("wait millis " + maxWaitMillis);所以,如果连接池满了,且等待超时,就会抛出GetConnectionTimeoutException。
扩展场景3:max-wait配置多少合适?
# 场景1:高并发系统
max-wait: 5000 # 5秒,快速失败,避免雪崩
# 场景2:低并发系统
max-wait: 60000 # 60秒,耐心等待
# 场景3:不等待(立即失败)
max-wait: 0 # 如果没有连接,立即抛异常一般推荐5-10秒,快速失败比长时间等待更好。
四、emptySignal:信号通知创建连接
当连接池没有空闲连接时,会发送信号通知创建线程:
核心方法3:emptySignal
文件位置:druid/src/main/java/com/alibaba/druid/pool/DruidDataSource.java
private void emptySignal() {
// 如果正在创建连接,不发送信号
if (creating) {
return;
}
// 检查是否需要创建连接
if (activeCount + poolingCount >= maxActive) {
return; // 已经达到最大连接数,不需要创建
}
// ★★★★★ 核心:发送信号,唤醒创建线程 ★★★★★
empty.signal();
}这个方法很简单,就是发送一个信号,唤醒创建线程。
创建线程收到信号后,会执行:
// CreateConnectionThread.run()
while (true) {
lock.lock();
try {
// 等待信号
empty.await();
// 收到信号,创建连接
PhysicalConnectionInfo connInfo = createPhysicalConnection();
DruidConnectionHolder holder = new DruidConnectionHolder(connInfo);
connections[poolingCount++] = holder;
// 通知等待获取连接的线程
notEmpty.signal();
} finally {
lock.unlock();
}
}看到没?这就是完整的信号通知机制:
获取连接线程 创建连接线程
↓ ↓
检查连接池 等待信号
↓ ↓
没有空闲连接 收到empty.signal()
↓ ↓
发送empty.signal() 创建连接
↓ ↓
等待notEmpty.signal() 放入连接池
↓ ↓
收到notEmpty.signal() 发送notEmpty.signal()
↓ ↓
获取连接 继续等待五、recycle:归还连接到连接池
好,获取连接的流程搞清楚了,现在来看归还连接的流程。
还记得吗?咱们获取到的是DruidPooledConnection代理对象,当调用close()时,会触发归还逻辑。
核心方法4:DruidPooledConnection.close
文件位置:druid/src/main/java/com/alibaba/druid/pool/DruidPooledConnection.java
@Override
public void close() throws SQLException {
// 如果已经关闭,不重复处理
if (this.disable) {
return;
}
// ★★★★★ 核心:调用recycle归还连接 ★★★★★
dataSource.recycle(this);
}就一行,调用dataSource.recycle()。
核心方法5:recycle
文件位置:druid/src/main/java/com/alibaba/druid/pool/DruidDataSource.java
public void recycle(DruidPooledConnection pooledConnection) throws SQLException {
// ========== 第一步:获取连接持有者 ==========
DruidConnectionHolder holder = pooledConnection.getConnectionHolder();
// ========== 第二步:检查连接是否有效 ==========
if (holder.isDiscard()) {
// 连接已标记为废弃,不归还
closeConnection(holder);
return;
}
// ========== 第三步:获取锁 ==========
final ReentrantLock lock = this.lock;
lock.lock();
try {
// ========== 第四步:检查连接池是否已满 ==========
if (poolingCount >= maxActive) {
// 连接池已满,不归还,直接关闭
closeConnection(holder);
return;
}
// ========== 第五步:归还连接到池中 ==========
// ★★★★★ 核心:放入数组末尾 ★★★★★
connections[poolingCount++] = holder;
// 活跃连接数减1
activeCount--;
// 更新最后活跃时间
holder.setLastActiveTime(System.currentTimeMillis());
// ========== 第六步:通知等待的线程 ==========
// ★★★★★ 核心:唤醒等待获取连接的线程 ★★★★★
notEmpty.signal();
} finally {
lock.unlock();
}
}这个方法的逻辑是:
- 检查连接是否有效(如果已废弃,直接关闭)
- 检查连接池是否已满(如果满了,不归还,直接关闭)
- 放入数组末尾(
connections[poolingCount++]) - 活跃连接数减1
- 更新最后活跃时间
- 通知等待的线程( otEmpty.signal()`)
关键点:归还连接是放入数组末尾,而获取连接是从数组末尾获取。这就是后进先出(LIFO)的策略。
扩展场景4:为什么要用LIFO而不是FIFO?
LIFO(后进先出):
获取 → connections[--poolingCount] (从末尾获取)
归还 → connections[poolingCount++] (放入末尾)
优势:
1. 热点连接复用率高(最近归还的连接,很可能马上又被获取)
2. CPU缓存命中率高(连续访问相邻内存)
3. 其他连接有机会被回收(空闲超时)
FIFO(先进先出):
获取 → connections[0] (从头部获取)
归还 → connections[poolingCount++] (放入末尾)
劣势:
1. 所有连接都会被均匀使用
2. 没有连接有机会空闲超时
3. CPU缓存命中率低所以,Druid选择LIFO是有道理的!
扩展场景5:如果连接池满了,归还的连接怎么处理?
if (poolingCount >= maxActive) {
// 连接池已满,不归还,直接关闭
closeConnection(holder);
return;
}这种情况一般不会发生,因为poolingCount + activeCount <= maxActive。
但如果发生了(比如bug),就会直接关闭连接,而不是归还。
六、连接泄漏检测
连接泄漏是个很严重的问题,如果一个连接被获取后没有归还,就会导致连接池可用连接越来越少,最终耗尽。
Druid是怎么检测连接泄漏的呢?
核心机制:removeAbandoned
文件位置:druid/src/main/java/com/alibaba/druid/pool/DruidDataSource.java
public void recycle(DruidPooledConnection pooledConnection) throws SQLException {
// ... 正常归还逻辑
// 如果开启了泄漏检测
if (isRemoveAbandoned()) {
// 从泄漏检测列表中移除
getDataSourceStat().getConnectionPool().getActiveConnections().remove(pooledConnection);
}
}
/**
* 后台线程检测泄漏连接
*/
private class DestroyConnectionThread extends Thread {
public void run() {
while (true) {
// 定期执行
Thread.sleep(timeBetweenEvictionRunsMillis);
// 检测泄漏连接
if (isRemoveAbandoned()) {
removeAbandoned();
}
}
}
}
/**
* 移除泄漏连接
*/
public int removeAbandoned() {
int removeCount = 0;
long currrentNanos = System.nanoTime();
List<DruidPooledConnection> abandonedList = new ArrayList<>();
lock.lock();
try {
// 遍历所有活跃连接
for (DruidPooledConnection pooledConnection : activeConnections) {
// 如果连接持有时间超过removeAbandonedTimeoutMillis
if (pooledConnection.getConnectedTimeNano() +
removeAbandonedTimeoutMillis * 1000 * 1000 < currrentNanos) {
// 标记为泄漏
abandonedList.add(pooledConnection);
}
}
} finally {
lock.unlock();
}
// 关闭泄漏连接
for (DruidPooledConnection pooledConnection : abandonedList) {
pooledConnection.abandon();
removeCount++;
}
return removeCount;
}检测逻辑很简单:
- 遍历所有活跃连接
- 检查连接持有时间是否超过阈值
- 如果超过,标记为泄漏,关闭连接
扩展场景6:如何配置连接泄漏检测?
spring:
datasource:
druid:
# 开启泄漏检测
remove-abandoned: true
# 泄漏超时时间(秒)
remove-abandoned-timeout: 180 # 3分钟
# 记录泄漏日志
log-abandoned: true扩展场景7:连接泄漏的常见场景
// 场景1:忘记关闭连接
Connection conn = dataSource.getConnection();
// 使用连接
// 忘记调用conn.close(),导致泄漏!
// 场景2:异常导致没有关闭
Connection conn = null;
try {
conn = dataSource.getConnection();
// 使用连接
throw new RuntimeException("异常");
} catch (Exception e) {
// 没有finally块,连接没有关闭
}
// 场景3:正确做法
try (Connection conn = dataSource.getConnection()) {
// 使用连接
} // try-with-resources自动关闭所以,一定要用try-with-resources或者在finally块中关闭连接!
七、完整的连接获取与归还流程图
说了这么多,咱们画个图把整个流程串起来:
获取连接流程:
┌──────────────────────────────────────────┐
│ getConnection() │
│ │
│ 1. init() - 如果没有初始化,先初始化 │
│ 2. pollLast() - 获取连接 │
│ → 有空闲连接 │
│ → connections[--poolingCount] │
│ → activeCount++ │
│ → 返回连接 │
│ → 没有空闲连接 │
│ → emptySignal() - 通知创建线程 │
│ → notEmpty.await() - 等待 │
│ → 超时返回null │
│ 3. new DruidPooledConnection(holder) │
│ → 创建代理对象 │
│ 4. 返回代理对象 │
└──────────────────────────────────────────┘
归还连接流程:
┌──────────────────────────────────────────┐
│ DruidPooledConnection.close() │
│ │
│ → dataSource.recycle(this) │
│ │
│ 1. 检查连接是否有效 │
│ 2. lock() - 获取锁 │
│ 3. connections[poolingCount++] = holder │
│ → 放入数组末尾 │
│ 4. activeCount-- │
│ 5. setLastActiveTime() │
│ 6. notEmpty.signal() │
│ → 唤醒等待的线程 │
│ 7. unlock() - 释放锁 │
└──────────────────────────────────────────┘
信号通知机制:
获取连接线程 创建连接线程
↓ ↓
poolLast() 等待empty信号
↓ ↓
没有空闲连接 收到empty.signal()
↓ ↓
empty.signal() 创建连接
↓ ↓
notEmpty.await() 放入连接池
↓ ↓
收到notEmpty.signal() notEmpty.signal()
↓ ↓
获取连接 继续等待八、关键问题解答
Q1:如果多个线程同时调用getConnection(),怎么保证线程安全?
答案:用ReentrantLock锁:
lock.lock();
try {
// 操作连接池
if (poolingCount > 0) {
holder = connections[--poolingCount];
activeCount++;
}
} finally {
lock.unlock();
}Q2:为什么要用DruidPooledConnection包装Connection?
答案:为了拦截close()方法:
public class DruidPooledConnection extends ConnectionProxy {
@Override
public void close() {
// 不是真的关闭,而是归还到池中
dataSource.recycle(this);
}
}Q3:如果获取连接超时,会发生什么?
答案:抛出GetConnectionTimeoutException:
if (holder == null) {
throw new GetConnectionTimeoutException(
"wait millis " + maxWaitMillis + ", active " + activeCount);
}Q4:连接池的性能瓶颈在哪里?
答案:主要在锁竞争:
lock.lock(); // 这里会阻塞
try {
// 操作连接池
} finally {
lock.unlock();
}如果并发很高,很多线程会在这里等待。这也是为什么HikariCP要用更精细的锁控制。
九、总结与伏笔
通过这篇文章的分析,咱们搞清楚了连接获取与归还的完整流程:
- 获取连接:
getConnection()→pollLast()→ 从数组末尾获取 - 信号通知:没有连接时,通知创建线程创建
- 归还连接:
close()→recycle()→ 放入数组末尾 - LIFO策略:后进先出,提高缓存命中率
- 泄漏检测:超时未归还的连接会被回收
但是,咱们还有个关键问题没深入:
Druid的Filter链是怎么工作的?
- Filter接口是怎么设计的?
- 多个Filter是怎么串联执行的?
- StatFilter是怎么做统计监控的?
- WallFilter是怎么做SQL防火墙的?
这些问题,将在下一篇文章《Druid Filter链机制》中为你揭晓。到时候咱们会深入到Filter链的源码,看看Druid是怎么实现强大的监控和安全功能的。
敬请期待!
参考资料
- Druid 1.2.x 源码
src/main/java/com/alibaba/druid/pool/DruidDataSource.javasrc/main/java/com/alibaba/druid/pool/DruidPooledConnection.javasrc/main/java/com/alibaba/druid/pool/DruidConnectionHolder.java
- 《阿里巴巴Druid官方文档》
- 《Java并发编程实战》- 生产者消费者模型