目录
1. 简介
最早接触Druid 还是使用 Spring 的 xml 配置时代,一堆配置参数。
然后就是SpringBoot,使用配置类集成。
Druid 是一款很优秀的连接池,之前一直没有读过源码,只是项目搭建时集成一下,现在有机会,静下心来,随我一起看看 Druid 值得我们学习和借鉴的细节吧。
2. 准备工作
先到 github 上把源码 clone 下来。
https://github.com/alibaba/druid.git
然后,把 1.2.8 tag 代码 checkout 下来,此次分析基于 1.2.8 版本。
运行 maven 命令编译代码
mvn clean install -DskipTests
3. Demo
去年时在搭建一个新服务时,在开发服务器上出现了连接 MySQL 报错的问题。试了很多方法,一直找不到问题根源。老大这时提出了一个思路,最小化实现一个 Demo,放到服务器上能否复现这个错误。这应该就是“控制变量法”吧,缩小排查范围。
众所周知,Druid 最重要的一个类就是 DruidDataSource,源码中也有相关的单测类:src/test/java/com/alibaba/druid/pool/demo/Demo0.java
找到它,把里面的参数改成自己的 MySQL 参数,并增加SQL查询并打印:
String sql = "select * from user limit 2;";
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet result = pstmt.executeQuery();
while (result.next()) {
String account = result.getString("account");
String userId = result.getString("userId");
System.out.println("account = " + account + ", userId = " + userId);
System.out.println("****************");
}
还需要把 maxPoolSize 设置最小为10,否则运行时会报错(maxPoolSize小于initialSize)。
开启 Debug 模式,逐步探索 Druid 是如何获取到数据库连接的。
4. 获取数据库连接
上面 Demo 最核心的一行代码,就是获取数据库连接:
Connection conn = dataSource.getConnection();
跟到 DruidDataSource 类中
@Override
public DruidPooledConnection getConnection() throws SQLException {
return getConnection(maxWait);
}
public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
init();
if (filters.size() > 0) {
FilterChainImpl filterChain = new FilterChainImpl(this);
return filterChain.dataSource_connect(this, maxWaitMillis);
} else {
return getConnectionDirect(maxWaitMillis);
}
}
可以看到 getConnection(long) 方法分了两部分:
- 初始化方法 init;
- 获取数据库连接;
我们先粗略看看初始化方法 init()。
大概分为如下几大块(后面有源码,可以开2个tab页对比着看):
- 判断是否已经初始化(只有第一次获取连接时才真正执行初始化方法);
- 使用继承自父类 DruidAbstractDataSource 的可重入锁 ReentrantLock lock 加锁(强制串行化,避免线程安全问题);
- 获取到锁后,再次验证是否已经初始化;
- 初始化 DruidDataSource 类中属性值,包括线程栈信息、id、一系列对象属性更新器、url、过滤器、数据库类型等;
- 连接池配置校验(最大活跃、初始化、最小空闲等);
- 以ServiceLoader方式初始化过滤器: initFromSPIServiceLoader();
- 获取驱动 Driver:resolveDriver();
- 根据数据库类型初始化检查:initCheck();
- 初始化 ExceptionSorter(剔除“不可用连接”的机制):initExceptionSorter();
- 根据数据库类型初始化合法连接校验器:initValidConnectionChecker();
- 校验 validationQuery 是否正确设置(默认为 “SELECT 1”):validationQueryCheck();
- 实例化 JdbcDataSourceStat 用于统计信息;
- 实例化3个数组,用于存放所有连接、将要被剔除连接、保活连接;
- 初始化连接,数量为设置的初始连接数,并存放到数组中;(真正创建连接在这一步中)
- 创建并运行日志线程:createAndLogThread();
- 创建并运行创建连接线程:createAndStartCreatorThread();
- 创建并运行销毁连接线程:createAndStartDestroyThread();
- 注册 MBean:registerMbean();
- 解锁,打印日志;
其中,需要重点关注的有2、9、14、16、17。
其中,第2点,加锁代码如下:
final ReentrantLock lock = this.lock;
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
throw new SQLException("interrupt", e);
}
父类 DruidAbstractDataSource 构造方法中,初始化了可重入锁,以及2个条件(非空、为空);
public DruidAbstractDataSource(boolean lockFair){
lock = new ReentrantLock(lockFair);
notEmpty = lock.newCondition();
empty = lock.newCondition();
}
默认创建的是 非公平锁,当然也可以自行指定为公平锁;
public DruidDataSource(){
this(false);
}
public DruidDataSource(boolean fairLock){
super(fairLock);
configFromPropety(System.getProperties());
}
这2个条件在创建和销毁连接时起到关键作用,这个后续再分析。
最后,把 init 方法源码贴在这里,方便大家不用打开开发工具就可以查看:
public void init() throws SQLException {
// 1. 判断是否已经初始化
if (inited) {
return;
}
// bug fixed for dead lock, for issue #2980
DruidDriver.getInstance();
// 2. 加锁
final ReentrantLock lock = this.lock;
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
throw new SQLException("interrupt", e);
}
boolean init = false;
try {
// 3. 获取到锁后,再次验证是否已经初始化;
if (inited) {
return;
}
// 4. 初始化 DruidDataSource 类中属性值
initStackTrace = Utils.toString(Thread.currentThread().getStackTrace());
this.id = DruidDriver.createDataSourceId();
if (this.id > 1) {
long delta = (this.id - 1) * 100000;
this.connectionIdSeedUpdater.addAndGet(this, delta);
this.statementIdSeedUpdater.addAndGet(this, delta);
this.resultSetIdSeedUpdater.addAndGet(this, delta);
this.transactionIdSeedUpdater.addAndGet(this, delta);
}
if (this.jdbcUrl != null) {
this.jdbcUrl = this.jdbcUrl.trim();
initFromWrapDriverUrl();
}
for (Filter filter : filters) {
filter.init(this);
}
if (this.dbTypeName == null || this.dbTypeName.length() == 0) {
this.dbTypeName = JdbcUtils.getDbType(jdbcUrl, null);
}
DbType dbType = DbType.of(this.dbTypeName);
if (JdbcUtils.isMysqlDbType(dbType)) {
boolean cacheServerConfigurationSet = false;
if (this.connectProperties.containsKey("cacheServerConfiguration")) {
cacheServerConfigurationSet = true;
} else if (this.jdbcUrl.indexOf("cacheServerConfiguration") != -1) {
cacheServerConfigurationSet = true;
}
if (cacheServerConfigurationSet) {
this.connectProperties.put("cacheServerConfiguration", "true");
}
}
// 5. 连接池配置校验
if (maxActive <= 0) {
throw new IllegalArgumentException("illegal maxActive " + maxActive);
}
if (maxActive < minIdle) {
throw new IllegalArgumentException("illegal maxActive " + maxActive);
}
if (getInitialSize() > maxActive) {
throw new IllegalArgumentException("illegal initialSize " + this.initialSize + ", maxActive " + maxActive);
}
if (timeBetweenLogStatsMillis > 0 && useGlobalDataSourceStat) {
throw new IllegalArgumentException("timeBetweenLogStatsMillis not support useGlobalDataSourceStat=true");
}
if (maxEvictableIdleTimeMillis < minEvictableIdleTimeMillis) {
throw new SQLException("maxEvictableIdleTimeMillis must be grater than minEvictableIdleTimeMillis");
}
if (keepAlive && keepAliveBetweenTimeMillis <= timeBetweenEvictionRunsMillis) {
throw new SQLException("keepAliveBetweenTimeMillis must be grater than timeBetweenEvictionRunsMillis");
}
if (this.driverClass != null) {
this.driverClass = driverClass.trim();
}
// 6. 以ServiceLoader方式初始化过滤器
initFromSPIServiceLoader();
// 7. 获取驱动 Driver
resolveDriver();
// 8. 根据数据库类型初始化检查
initCheck();
// 9. 初始化 ExceptionSorter(剔除“不可用连接”的机制)
initExceptionSorter();
// 10. 根据数据库类型初始化合法连接校验器
initValidConnectionChecker();
// 11. 校验 validationQuery 是否正确设置
validationQueryCheck();
// 12. 实例化 JdbcDataSourceStat 用于统计信息
if (isUseGlobalDataSourceStat()) {
dataSourceStat = JdbcDataSourceStat.getGlobal();
if (dataSourceStat == null) {
dataSourceStat = new JdbcDataSourceStat("Global", "Global", this.dbTypeName);
JdbcDataSourceStat.setGlobal(dataSourceStat);
}
if (dataSourceStat.getDbType() == null) {
dataSourceStat.setDbType(this.dbTypeName);
}
} else {
dataSourceStat = new JdbcDataSourceStat(this.name, this.jdbcUrl, this.dbTypeName, this.connectProperties);
}
dataSourceStat.setResetStatEnable(this.resetStatEnable);
// 13. 实例化3个数组,用于存放所有连接、将要被剔除连接、保活连接;
connections = new DruidConnectionHolder[maxActive];
evictConnections = new DruidConnectionHolder[maxActive];
keepAliveConnections = new DruidConnectionHolder[maxActive];
SQLException connectError = null;
// 14. 初始化连接
if (createScheduler != null && asyncInit) {
for (int i = 0; i < initialSize; ++i) {
submitCreateTask(true);
}
} else if (!asyncInit) {
// init connections
while (poolingCount < initialSize) {
try {
PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
DruidConnectionHolder holder = new DruidConnectionHolder(this, pyConnectInfo);
connections[poolingCount++] = holder;
} catch (SQLException ex) {
LOG.error("init datasource error, url: " + this.getUrl(), ex);
if (initExceptionThrow) {
connectError = ex;
break;
} else {
Thread.sleep(3000);
}
}
}
if (poolingCount > 0) {
poolingPeak = poolingCount;
poolingPeakTime = System.currentTimeMillis();
}
}
// 15. 创建并运行日志线程
createAndLogThread();
// 16. 创建并运行创建连接线程
createAndStartCreatorThread();
// 17. 创建并运行销毁连接线程
createAndStartDestroyThread();
initedLatch.await();
init = true;
initedTime = new Date();
// 18. 注册 MBean
registerMbean();
if (connectError != null && poolingCount == 0) {
throw connectError;
}
if (keepAlive) {
// async fill to minIdle
if (createScheduler != null) {
for (int i = 0; i < minIdle; ++i) {
submitCreateTask(true);
}
} else {
this.emptySignal();
}
}
} catch (SQLException e) {
LOG.error("{dataSource-" + this.getID() + "} init error", e);
throw e;
} catch (InterruptedException e) {
throw new SQLException(e.getMessage(), e);
} catch (RuntimeException e){
LOG.error("{dataSource-" + this.getID() + "} init error", e);
throw e;
} catch (Error e){
LOG.error("{dataSource-" + this.getID() + "} init error", e);
throw e;
} finally {
// 19. 解锁,打印日志;
inited = true;
lock.unlock();
if (init && LOG.isInfoEnabled()) {
String msg = "{dataSource-" + this.getID();
if (this.name != null && !this.name.isEmpty()) {
msg += ",";
msg += this.name;
}
msg += "} inited";
LOG.info(msg);
}
}
}