part2-druid-architecture
约 4063 字大约 14 分钟
2026-05-24
系列文章:《深入Spring事务与数据源:从注解到数据库连接的全链路解析》
系列二第2篇:Druid连接池架构与核心流程源码解析
基于版本:Druid 1.2.x、Spring Framework 5.3.x
前置阅读:系列二第1篇:DataSource接口规范与SPI机制
前言
上一篇文章咱们把DataSource接口和连接池的基础概念搞清楚了。今天,咱们就正式进入Druid的源码,看看这个号称"国产最强连接池"的家伙到底是怎么设计的。
你可能用过Druid,在application.yml里配过URL、用户名、密码,然后就能用了。但是你有没有想过:
- Druid是怎么管理连接的?
- 连接池是怎么初始化的?
- 那些配置参数都是啥意思?
- Druid为什么比DBCP、C3P0性能更好?
别急,这篇文章咱们就来把Druid的架构和核心流程扒个明明白白。
而且我敢说,看完这篇文章,你再去看其他连接池的源码,那简直就是降维打击!因为Druid的设计思想,基本上代表了连接池设计的最高水平。
好了,废话不多说,开干!
一、回顾一下:Spring Boot是怎么配置Druid的?
还是老规矩,先看看自动装配的入口。不过这次咱们看的是Druid自己的配置。
核心入口1:DruidDataSource的Bean创建
如果你是自己配置的,大概是这样的:
@Configuration
public class DruidDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSource dataSource() {
return new DruidDataSource();
}
}看到没?就一行 ew DruidDataSource(),然后通过@ConfigurationProperties`绑定配置。
核心入口2:配置参数绑定
文件位置:application.yml
spring:
datasource:
druid:
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 连接池配置
initial-size: 5 # 初始连接数
max-active: 20 # 最大连接数
min-idle: 5 # 最小空闲连接数
max-wait: 60000 # 获取连接最大等待时间(毫秒)
# 连接有效性检测
test-while-idle: true # 空闲时检测
test-on-borrow: false # 获取时检测
test-on-return: false # 归还时检测
validation-query: SELECT 1 # 检测SQL
# 保活配置
keep-alive: true # 开启保活
keep-alive-between-time-millis: 60000 # 保活间隔(毫秒)
# 监控配置
stat-view-servlet:
enabled: true
url-pattern: /druid/*这些配置参数,最终都会通过Setter方法设置到DruidDataSource的属性上。
扩展场景1:Druid的starter自动配置
如果你用的是druid-spring-boot-starter,配置就更简单了:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>然后Druid会自动配置,不需要自己写配置类。这是因为它有自己的自动配置类:
文件位置:druid-spring-boot-starter/src/main/java/com/alibaba/druid/spring/boot/autoconfigure/DruidDataSourceAutoConfigure.java
@Configuration
@ConditionalOnClass(DruidDataSource.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@Import({DruidDataSourceWrapper.class,
DruidFilterConfiguration.class,
DruidSpringAopConfiguration.class,
DruidStatViewServletConfiguration.class,
DruidWebStatFilterConfiguration.class})
public class DruidDataSourceAutoConfigure {
@Bean
@ConditionalOnBean(DataSourceProperties.class)
public DruidDataSourceWrapper dataSource(DataSourceProperties properties) {
DruidDataSourceWrapper wrapper = new DruidDataSourceWrapper();
wrapper.setUrl(properties.determineUrl());
wrapper.setUsername(properties.determineUsername());
wrapper.setPassword(properties.determinePassword());
wrapper.setDriverClassName(properties.determineDriverClassName());
return wrapper;
}
}看到了吧?DruidDataSourceWrapper就是个包装类,实际还是DruidDataSource。
好了,自动装配的部分搞清楚了,现在正式进入Druid的核心架构!
二、DruidDataSource的整体架构
先来看看DruidDataSource的类继承关系:
核心类1:DruidDataSource
文件位置:druid/src/main/java/com/alibaba/druid/pool/DruidDataSource.java
java.lang.Object
↓
com.alibaba.druid.pool.DruidAbstractDataSource (抽象类)
↓
com.alibaba.druid.pool.DruidDataSource (实现类)看到没?DruidDataSource继承了DruidAbstractDataSource。
为什么要分两个类?因为DruidAbstractDataSource里放了所有连接池的通用逻辑,而DruidDataSource里放了具体的实现细节。
这种设计叫做模板方法模式,和Spring的AbstractPlatformTransactionManager是一个套路!
核心类2:DruidAbstractDataSource
文件位置:druid/src/main/java/com/alibaba/druid/pool/DruidAbstractDataSource.java
public abstract class DruidAbstractDataSource implements DataSource, DataSourceExt {
// ========== 数据库连接配置 ==========
protected volatile String url;
protected volatile String username;
protected volatile String password;
protected volatile String driverClass;
// ========== 连接池配置 ==========
protected int initialSize = 0; // 初始连接数
protected int maxActive = 8; // 最大连接数
protected int minIdle = 0; // 最小空闲连接数
protected int maxWait = -1; // 最大等待时间(毫秒)
// ========== 连接检测配置 ==========
protected String validationQuery; // 检测SQL
protected boolean testOnBorrow = false; // 获取时检测
protected boolean testOnReturn = false; // 归还时检测
protected boolean testWhileIdle = false; // 空闲时检测
// ========== 连接池数据结构 ==========
// 空闲连接数组(核心!)
protected DruidConnectionHolder[] connections;
// 连接池统计信息
protected AtomicLong createCount = new AtomicLong();
protected AtomicLong destroyCount = new AtomicLong();
protected AtomicLong connectCount = new AtomicLong();
protected AtomicLong closeCount = new AtomicLong();
// ========== 锁 ==========
// 连接池的锁
protected final ReentrantLock lock = new ReentrantLock();
// 不为空的条件(用于等待连接)
protected final Condition notEmpty = lock.newCondition();
// 不为满的条件(用于等待创建)
protected final Condition notFull = lock.newCondition();
// ========== 抽象方法 ==========
// 创建物理连接
protected abstract PhysicalConnectionInfo createPhysicalConnection() throws SQLException;
// 其他抽象方法...
}看到这个类的核心成员变量,你应该能猜到连接池是怎么工作的了吧?
关键点:
connections数组:存储空闲连接的核心数据结构lock锁:保证线程安全- ** otEmpty`条件**:当连接池为空时,获取连接的线程等待
- ** otFull`条件**:当连接池满时,创建连接的线程等待
这就是经典的生产者-消费者模型!
扩展场景2:为什么用数组而不是LinkedList?
// 方式1:使用数组(Druid早期版本)
protected DruidConnectionHolder[] connections;
// 方式2:使用双端队列(Druid新版本)
protected Deque<DruidConnectionHolder> connections = new ArrayDeque<>();
// 方式3:使用LinkedList(不推荐)
protected List<DruidConnectionHolder> connections = new LinkedList<>();Druid早期版本用数组,是因为数组的内存布局更紧凑,缓存命中率更高。但后来改成了ArrayDeque,因为操作更方便。
LinkedList不推荐,因为它的节点是分散在堆上的,缓存命中率低。
三、DruidDataSource的核心数据结构
好,现在来看看连接池的核心数据结构:
核心数据结构:连接池的数组
public class DruidDataSource extends DruidAbstractDataSource {
// 连接池数组
private DruidConnectionHolder[] connections;
// 连接池中的连接数量
private int poolingCount;
// 创建连接线程数
private int createTaskCount;
// 是否正在创建连接
private volatile boolean creating = false;
}连接池的结构是这样的:
connections数组:
[0] → DruidConnectionHolder → Connection1
[1] → DruidConnectionHolder → Connection2
[2] → DruidConnectionHolder → Connection3
...
[poolingCount-1] → DruidConnectionHolder → ConnectionN
[poolingCount] → null
...
[maxActive-1] → nullpoolingCount表示当前有多少个空闲连接。
核心类3:DruidConnectionHolder
文件位置:druid/src/main/java/com/alibaba/druid/pool/DruidConnectionHolder.java
public class DruidConnectionHolder {
// 实际的物理连接
private Connection connection;
// 连接最后一次使用时间
private long lastActiveTime;
// 连接创建时间
private long connectTime;
// 是否被使用
private boolean inUse;
// Statement缓存
private PreparedStatementHolder[] statementPool;
// 其他元数据
private String schema;
private String catalog;
private int holdability;
public DruidConnectionHolder(Connection connection) {
this.connection = connection;
this.lastActiveTime = System.currentTimeMillis();
this.connectTime = System.currentTimeMillis();
}
public Connection getConnection() {
return connection;
}
public long getLastActiveTime() {
return lastActiveTime;
}
public void setLastActiveTime(long lastActiveTime) {
this.lastActiveTime = lastActiveTime;
}
}DruidConnectionHolder就是个包装类,里头保存了实际的JDBC连接和一些元数据。
扩展场景3:为什么要用Holder包装Connection?
因为需要保存额外的信息:
lastActiveTime:用于检测空闲连接connectTime:用于检测连接存活时间statementPool:用于缓存PreparedStatementinUse:标记连接是否正在使用
如果直接用Connection,这些信息没法保存。
四、连接池的初始化流程
好了,现在来看重头戏——连接池是怎么初始化的?
核心方法1:init
文件位置:druid/src/main/java/com/alibaba/druid/pool/DruidDataSource.java
public void init() throws SQLException {
// ========== 第一步:检查是否已经初始化 ==========
if (inited) {
return;
}
// ========== 第二步:获取锁,保证线程安全 ==========
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 双重检查,防止重复初始化
if (inited) {
return;
}
// ========== 第三步:加载驱动 ==========
initFromSPIServiceLoader(); // 通过SPI加载驱动
resolveDriver(); // 解析驱动
// ========== 第四步:检查配置 ==========
checkConfig(); // 检查配置参数是否合法
// ========== 第五步:初始化连接池数组 ==========
if (this.maxActive > 0) {
// ★★★★★ 核心:创建连接池数组 ★★★★★
this.connections = new DruidConnectionHolder[this.maxActive];
} else {
throw new IllegalArgumentException("maxActive must > 0");
}
// ========== 第六步:创建初始连接 ==========
// ★★★★★ 核心:创建initialSize个连接 ★★★★★
for (int i = 0; i < initialSize; ++i) {
// 创建物理连接
PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
// 包装成DruidConnectionHolder
DruidConnectionHolder holder = new DruidConnectionHolder(this, pyConnectInfo);
// 放入连接池数组
connections[poolingCount++] = holder;
}
// ========== 第七步:检查最小空闲连接数 ==========
if (poolingCount < minIdle) {
// 如果初始连接数小于minIdle,继续创建
for (int i = poolingCount; i < minIdle; ++i) {
PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
DruidConnectionHolder holder = new DruidConnectionHolder(this, pyConnectInfo);
connections[poolingCount++] = holder;
}
}
// ========== 第八步:创建创建连接的线程 ==========
// 这个线程负责异步创建连接
if (createScheduler == null && asyncInit) {
createAndStartCreatorThread();
}
// ========== 第九步:创建销毁连接的线程 ==========
// 这个线程负责回收无效连接
if (destroyScheduler == null) {
createAndStartDestroyThread();
}
// 标记为已初始化
inited = true;
// 记录启动时间
startNanos = System.nanoTime();
if (LOG.isInfoEnabled()) {
LOG.info("{dataSource-" + this.getID() + "} inited");
}
} finally {
lock.unlock();
}
}这个初始化方法做了9件事:
- 检查是否已初始化(防止重复初始化)
- 获取锁(保证线程安全)
- 加载驱动(通过SPI机制)
- 检查配置(参数合法性)
- 创建连接池数组(大小为maxActive)
- 创建初始连接(数量为initialSize)
- 检查最小空闲连接(如果不够,继续创建)
- 创建异步创建线程(用于后续创建连接)
- 创建销毁线程(用于回收无效连接)
关键点:初始化完成后,连接池的结构是这样的:
connections数组(maxActive = 20):
[0] → Connection1 (空闲)
[1] → Connection2 (空闲)
[2] → Connection3 (空闲)
[3] → Connection4 (空闲)
[4] → Connection5 (空闲)
[5] → null (未使用)
...
[19] → null (未使用)
poolingCount = 5 (initialSize = 5)扩展场景4:initialSize、minIdle、maxActive的关系
initial-size: 5 # 初始化时创建5个连接
min-idle: 5 # 保持最少5个空闲连接
max-active: 20 # 最多20个连接(包括使用中的)
场景1:系统启动
→ 创建5个连接(initial-size)
→ poolingCount = 5
场景2:5个连接都在使用
→ 继续创建连接,直到min-idle(5个)
→ poolingCount = 5(但都在使用中)
场景3:20个连接都在使用
→ 不能再创建了(达到max-active)
→ 后续请求只能等待五、createPhysicalConnection:创建物理连接
初始化时要创建连接,那连接是怎么创建的呢?
核心方法2:createPhysicalConnection
文件位置:druid/src/main/java/com/alibaba/druid/pool/DruidDataSource.java
protected PhysicalConnectionInfo createPhysicalConnection() throws SQLException {
// ========== 第一步:获取驱动 ==========
Driver driver = this.driver;
// ========== 第二步:设置连接属性 ==========
Properties connectProperties = new Properties();
if (this.connectProperties != null) {
connectProperties.putAll(this.connectProperties);
}
// 设置用户名和密码
if (this.username != null) {
connectProperties.setProperty("user", username);
}
if (this.password != null) {
connectProperties.setProperty("password", password);
}
// ========== 第三步:通过驱动创建连接 ==========
Connection conn;
try {
// ★★★★★ 核心:调用Driver.connect创建连接 ★★★★★
conn = driver.connect(url, connectProperties);
if (conn == null) {
throw new SQLException("Failed to create connection: driver returned null");
}
} catch (SQLException e) {
throw e;
}
// ========== 第四步:包装成PhysicalConnectionInfo ==========
PhysicalConnectionInfo info = new PhysicalConnectionInfo(conn);
info.setConnectTime(System.currentTimeMillis());
return info;
}这个方法的核心就是driver.connect(url, connectProperties),通过JDBC驱动创建连接。
扩展场景5:Driver.connect的底层实现是什么?
以MySQL驱动为例:
文件位置:mysql-connector-java/ConnectionImpl.java
public Connection connect(String url, Properties info) throws SQLException {
// 1. 解析URL
String host = parseHost(url);
int port = parsePort(url);
String database = parseDatabase(url);
// 2. 建立TCP连接(三次握手)
Socket socket = new Socket(host, port);
// 3. MySQL握手协议
// 服务器发送握手包
// 客户端发送认证包
// 服务器验证用户名密码
// 4. 创建Connection对象
Connection conn = new ConnectionImpl(socket, database, info);
return conn;
}所以,创建一个连接的过程是:
- 建立TCP连接(三次握手,约1-10毫秒)
- MySQL握手协议(认证用户名密码,约1-5毫秒)
- 创建Connection对象(分配内存,约1毫秒)
总共大约需要10-50毫秒。这就是为什么要用连接池!
六、createAndStartCreatorThread:异步创建连接线程
初始化完成后,会创建一个后台线程,负责异步创建连接。
核心方法3:createAndStartCreatorThread
文件位置:druid/src/main/java/com/alibaba/druid/pool/DruidDataSource.java
protected void createAndStartCreatorThread() {
// 创建线程
CreateConnectionThread creator = new CreateConnectionThread("Druid-Connection-Creator");
creator.setDaemon(true); // 设置为守护线程
creator.start();
}
/**
* 创建连接的后台线程
*/
private class CreateConnectionThread extends Thread {
public void run() {
initedLatch.countDown();
while (true) {
try {
// ========== 第一步:等待创建信号 ==========
// 如果不需要创建连接,就等待
lock.lock();
try {
// 计算需要创建的连接数
int needCreateCount = calcNeedCreateCount();
if (needCreateCount <= 0) {
// 不需要创建,等待
empty.await();
continue;
}
} finally {
lock.unlock();
}
// ========== 第二步:创建连接 ==========
PhysicalConnectionInfo connInfo = createPhysicalConnection();
// ========== 第三步:放入连接池 ==========
lock.lock();
try {
DruidConnectionHolder holder = new DruidConnectionHolder(
DruidDataSource.this, connInfo);
connections[poolingCount++] = holder;
// 通知等待的线程:有连接可用了
notEmpty.signal();
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
break;
} catch (SQLException e) {
// 创建失败,记录日志
LOG.error("create connection error", e);
}
}
}
/**
* 计算需要创建的连接数
*/
private int calcNeedCreateCount() {
// 当前连接总数
int totalConnection = poolingCount + activeCount;
// 需要创建的连接数
int needCreate = 0;
if (totalConnection < minIdle) {
// 如果小于minIdle,创建到minIdle
needCreate = minIdle - totalConnection;
}
// 不能超过maxActive
if (totalConnection + needCreate > maxActive) {
needCreate = maxActive - totalConnection;
}
return needCreate;
}
}这个线程的工作很简单:
- 等待创建信号
- 计算需要创建的连接数
- 创建连接
- 放入连接池
- 通知等待的线程
关键点:这个线程是守护线程,也就是说,当主线程结束时,它会自动退出。
七、createAndStartDestroyThread:销毁连接的后台线程
除了创建线程,还有一个销毁线程,负责回收无效连接。
核心方法4:createAndStartDestroyThread
文件位置:druid/src/main/java/com/alibaba/druid/pool/DruidDataSource.java
protected void createAndStartDestroyThread() {
// 创建线程
DestroyConnectionThread destroyer = new DestroyConnectionThread("Druid-Connection-Destroyer");
destroyer.setDaemon(true); // 守护线程
destroyer.start();
}
/**
* 销毁连接的后台线程
*/
private class DestroyConnectionThread extends Thread {
public void run() {
while (true) {
try {
// ========== 第一步:定期执行 ==========
// 每10秒执行一次(可配置)
Thread.sleep(timeBetweenEvictionRunsMillis);
// ========== 第二步:检测无效连接 ==========
List<DruidConnectionHolder> evictList = new ArrayList<>();
lock.lock();
try {
for (int i = 0; i < poolingCount; ++i) {
DruidConnectionHolder holder = connections[i];
// 检查连接是否空闲超时
if (isTimeBetweenEviction(holder)) {
evictList.add(holder);
}
// 检查连接是否存活超时
if (isPhysicallyTimeout(holder)) {
evictList.add(holder);
}
}
} finally {
lock.unlock();
}
// ========== 第三步:关闭无效连接 ==========
for (DruidConnectionHolder holder : evictList) {
JdbcUtils.close(holder.getConnection());
}
} catch (InterruptedException e) {
break;
}
}
}
}这个线程的工作是:
- 定期执行(每10秒)
- 检测无效连接(空闲超时、存活超时)
- 关闭无效连接
扩展场景6:什么是空闲超时和存活超时?
空闲超时(minEvictableIdleTimeMillis):
→ 连接空闲超过60秒,回收
→ 防止连接长时间不用占用资源
存活超时(phyTimeoutMillis):
→ 连接创建超过30分钟,回收
→ 防止连接老化失效八、Druid的整体架构图
说了这么多,咱们画个图来总结Druid的整体架构:
┌──────────────────────────────────────────────┐
│ DruidDataSource │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 连接池管理 │ │
│ │ │ │
│ │ connections数组 │ │
│ │ [0] → Connection1 (空闲) │ │
│ │ [1] → Connection2 (空闲) │ │
│ │ [2] → Connection3 (使用中) │ │
│ │ ... │ │
│ │ [19] → null │ │
│ └─────────────┬───────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────┐ │
│ │ 后台线程 │ │
│ │ │ │
│ │ 1. CreateConnectionThread │ │
│ │ → 异步创建连接 │ │
│ │ → 保持minIdle个空闲连接 │ │
│ │ │ │
│ │ 2. DestroyConnectionThread │ │
│ │ → 检测无效连接 │ │
│ │ → 回收空闲超时连接 │ │
│ │ → 回收存活超时连接 │ │
│ └─────────────┬───────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────┐ │
│ │ Filter链 │ │
│ │ │ │
│ │ StatFilter → 统计监控 │ │
│ │ WallFilter → SQL防火墙 │ │
│ │ LogFilter → 日志记录 │ │
│ │ ... │ │
│ └─────────────┬───────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────┐ │
│ │ 配置参数 │ │
│ │ │ │
│ │ initialSize、maxActive、minIdle │ │
│ │ testOnBorrow、testWhileIdle │ │
│ │ keepAlive、validationQuery │ │
│ └─────────────────────────────────────┘ │
└──────────────────────────────────────────────┘九、关键问题解答
Q1:为什么Druid比DBCP、C3P0性能更好?
答案:主要有几个原因:
- 连接池用数组/ArrayDeque:内存布局紧凑,缓存命中率高
- 异步创建连接:不阻塞获取连接的线程
- 精细的锁控制:用ReentrantLock而不是synchronized
- 连接有效性检测优化:只在必要时检测
Q2:连接池的线程安全是怎么保证的?
答案:用ReentrantLock锁:
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 操作连接池
} finally {
lock.unlock();
}Q3:如果创建连接失败怎么办?
答案:会记录日志,然后继续重试:
catch (SQLException e) {
LOG.error("create connection error", e);
// 不抛异常,继续循环
}Q4:初始化是在什么时候触发的?
答案:有两种方式:
// 方式1:手动初始化
DruidDataSource dataSource = new DruidDataSource();
dataSource.init();
// 方式2:第一次获取连接时自动初始化
Connection conn = dataSource.getConnection();
// 内部会调用init()十、总结与伏笔
通过这篇文章的分析,咱们搞清楚了Druid的整体架构:
- 类继承关系:DruidAbstractDataSource + DruidDataSource
- 核心数据结构:connections数组 + DruidConnectionHolder
- 初始化流程:创建连接池数组,创建初始连接
- 后台线程:创建线程 + 销毁线程
- 配置参数:initialSize、maxActive、minIdle等
但是,咱们还有个关键问题没深入:
连接是怎么获取和归还的?
getConnection()的完整流程是啥?- 如果连接池满了怎么办?
- 连接是怎么归还到池中的?
- 连接泄漏是怎么检测的?
这些问题,将在下一篇文章《连接获取与回收的完整链路》中为你揭晓。到时候咱们会深入到getConnection()和close()的源码,看看Druid是怎么高效管理连接的。
敬请期待!
参考资料
- Druid 1.2.x 源码
src/main/java/com/alibaba/druid/pool/DruidDataSource.javasrc/main/java/com/alibaba/druid/pool/DruidAbstractDataSource.javasrc/main/java/com/alibaba/druid/pool/DruidConnectionHolder.java
- 《阿里巴巴Druid官方文档》
- 《Java并发编程实战》- ReentrantLock章节