Druid源码阅读4-DruidDataSource的getConnection过程

本文详细分析了Druid连接池获取数据库连接的内部机制,包括过滤器的责任链模式实现,以及getConnection方法中涉及的连接创建、检测和重试策略。Druid通过FilterChainImpl实现过滤器链,使用DoubleCheck机制确保DataSource初始化,并通过自旋和等待策略确保连接获取。同时,文章介绍了getConnectionDirect方法中如何确保连接有效性,包括testOnBorrow、testWhileIdle等参数的作用,以及removeAbandoned参数对防止连接泄露的处理。整个过程展示了Druid在性能和稳定性方面的精细设计。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

DruidDataSource连接池实现了javaX.sql包中,DataSource接口的全部方法。getConnection也来自于javaX.sql.DataSource接口。
而DruidPooledConnection也实现了接口java.sql.Connection。
这样就能在各种场景中通过这个接口来获取数据库连接。

1.fileter处理–责任链模式

在执行getConnection方法的过程中,首先确认DataSource是否完成了初始化。由于 init方法采用了Double Check机制,如果初始化完成则不会再次执行,因此这里不会造成系统多次初始化。

public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
    //调用初始化,以避免在获取连接的时候DruidDataSource的初始化工作还没完成。
    init();
	
	//这里有两个分支,判断filters是否存在过滤器,如果存在则先执行过滤器中的内容,这采用责任链模式实现。
    if (filters.size() > 0) {
        //责任链执行过程
        FilterChainImpl filterChain = new FilterChainImpl(this);
        return filterChain.dataSource_connect(this, maxWaitMillis);
    } else {
        //直接创建连接
        return getConnectionDirect(maxWaitMillis);
    }
}

这个filter的处理过程是一个经典的责任链模式。
new了一个FilterChainImpl对象,而这个对象的构造函数 this 。
查看了一下,DruidDataSource的父类DruidAbstractDataSource正好实现了DataSourceProxy接口,也就是说,DruidDataSource本身就是一个DataSourceProxy。
这样做的好处是,FilterChainImpl本身不用维护任何存放filters的数组,这个数组可以直接复用DruidDataSource中的数据结构。
在FilterChainImpl中:


public FilterChainImpl(DataSourceProxy dataSource){
    this.dataSource = dataSource;
    this.filterSize = getFilters().size();
}

public FilterChainImpl(DataSourceProxy dataSource, int pos){
    this.dataSource = dataSource;
    this.pos = pos;
    this.filterSize = getFilters().size();
}

public List<Filter> getFilters() {
    return dataSource.getProxyFilters();
}

private Filter nextFilter() {
    return getFilters()
            .get(pos++);
}

在DruidAbstractDataSource中,这个filters的数据结构:

protected List<Filter>  filters = new CopyOnWriteArrayList<Filter>();

这样所有的filters都将存放到责怪list中。
再查看 FilterChainImpl的dataSource_connect方法:

@Override
public DruidPooledConnection dataSource_connect(DruidDataSource dataSource, long maxWaitMillis) throws SQLException {
    //判断当前filter的指针是否小于filterSize的大小,如果小于,则执行filter的dataSource_getConnection
    if (this.pos < filterSize) {
        DruidPooledConnection conn = nextFilter().dataSource_getConnection(this, dataSource, maxWaitMillis);
        return conn;
    }
    //反之 调用getConnectionDirect 创建数据库连接。
    return dataSource.getConnectionDirect(maxWaitMillis);
}

这样看上去将调用filter的dataSource_getConnection方法。
但是这个地方实际上涉及比较巧妙,采用了一个父类FilterAdapter,所有的Filter都集成这个父类FilterAdapter,而父类本身又实现了Filter接口,本身是一个Filter.
StartFilter等Filter的实现类,没有实现dataSource_getConnection方法。
因此这个方法实际上执行的逻辑就是FilterAdapter类的dataSource_getConnection方法。

@Override
public DruidPooledConnection dataSource_getConnection(FilterChain chain, DruidDataSource dataSource,
                                                      long maxWaitMillis) throws SQLException {
    return chain.dataSource_connect(dataSource, maxWaitMillis);
}

此时调用dataSource_connect之后,又回到了FilterChainImpl的dataSource_connect方法中。
不过此时pos会增加,if判断中的逻辑不会执行。那么就会执行 dataSource.getConnectionDirect(maxWaitMillis);直接创建一个连接之后返回。
这就是getConnection过程中处理filter的责任链模式,这也是我们在编码的过程中值得借鉴的地方。
在getConnection中,无论是否存在filter,那么最终将通过getConnectionDirect来创建连接。getConnectionDirect才是连接被创建的最终方法。

2.getConnectionDirect

getConnectionDirect方法也不是最终创建数据库连接的方法。
这个方法会通过一个for循环自旋,确保连接的创建。
在GetConnectionTimeoutException异常处理中,这个地方有一个重试次数notFullTimeoutRetryCount,每次重试的时间为maxWaitMillis。

int notFullTimeoutRetryCnt = 0;
//自旋
for (;;) {
    // handle notFullTimeoutRetry
    DruidPooledConnection poolableConnection;
    try {
    //调用getConnectionInternal 获取连接
        poolableConnection = getConnectionInternal(maxWaitMillis);
    } catch (GetConnectionTimeoutException ex) {
    //超时异常处理,判断是否达到最大重试次数 且连接池是否已满
        if (notFullTimeoutRetryCnt <= this.notFullTimeoutRetryCount && !isFull()) {
            notFullTimeoutRetryCnt++;
            //日志打印
            if (LOG.isWarnEnabled()) {
                LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
            }
            continue;
        }
        throw ex;
    }
    //后续代码略
    ... ...
}

通过自旋的方式确保获取到连接。之后对获取到的连接进行检测,主要的检测参数有:

参数说明
testOnBorrow默认值通常为false,用在获取连接的时候执行validationQuery检测连接是否有效。这个配置会降低性能。
testOnReturn默认值通常为false,用在归还连接的时候执行validationQuery检测连接是否有效,这个配置会降低性能。
testWhileIdle这个值通常建议为true,连接空闲时间大于timeBetweenEvictionRunsMillis指定的毫秒,就会执行参数validationQuery指定的SQL来检测连接是否有效。这个参数会定期执行。
validationQuery用来检测连接是否有效的sql,如果validationQuery为空,那么testOnBorrow、testOnReturn、testWhileIdle这三个参数都不会起作用,配置参考:validationQuery=SELECT 1

在getConnection中,将会发生的检测过程伪代码:

if (testOnBorrow){
  //获取连接时检测
}else {
    if (poolableConnection.conn.isClosed()) {
      //检测连接是否关闭
    }
    
     if (testWhileIdle) {
       //空闲检测 
     }

}

上述检测过程都会调用testConnectionInternal(poolableConnection.holder, poolableConnection.conn);进行检测。

此外还有一个很重要的参数removeAbandoned。这个参数相关的配置参数有:

参数说明
removeAbandoned如果连接泄露,是否需要回收泄露的连接,默认false;
logAbandoned如果回收了泄露的连接,是否要打印一条log,默认false;
removeAbandonedTimeoutMillis连接回收的超时时间,默认5分钟;

参数removeAbandoned的作用在于,如果有线程从Druid中获取到了连接并没有及时归还,那么Druid就会定期检测该连接是否会处于运行状态,如果不处于运行状态,则被获取时间超过removeAbandonedTimeoutMillis就会强制回收该连接。
这个检测的过程是在回收线程中完成的,在getConnection的过程中,只是判断该参数是否被设置,然后加上对应的标识。

if (removeAbandoned) {
    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
    //设置 stackTrace
    poolableConnection.connectStackTrace = stackTrace;
    //设置setConnectedTimeNano
    poolableConnection.setConnectedTimeNano();
    //打开traceEnable
    poolableConnection.traceEnable = true;

    activeConnectionLock.lock();
    try {
        activeConnections.put(poolableConnection, PRESENT);
    } finally {
        activeConnectionLock.unlock();
    }
}

最后还需要对defaultAutoCommit参数进行处理:

if (!this.defaultAutoCommit) {
    poolableConnection.setAutoCommit(false);
}

至此,一个getConnetion方法创建完毕。

3.getConnectionInternal

getConnectionInternal方法中创建连接:
首先判断连接池状态 closed 和enable状态是否正确,如果不正确则抛出异常退出。

之后的逻辑:

 for (boolean createDirect = false;;){
    if(createDirect){
        //直接创建连接的逻辑
    }
    
    if (maxWaitThreadCount > 0
        && notEmptyWaitThreadCount >= maxWaitThreadCount) {
        // 判断最大等待线程数如果大于0且notEmpty上的等待线程超过了这个值 那么抛出异常
        
        }

    //其他相关参数检测 抛出异常
    
    //判断如果createScheduler存在,且executor.getQueue().size()大于0 那么将启用createDirect逻辑,退出本持循环
    if (createScheduler != null
        && poolingCount == 0
        && activeCount < maxActive
        && creatingCountUpdater.get(this) == 0
        && createScheduler instanceof ScheduledThreadPoolExecutor) {
    ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) createScheduler;
    if (executor.getQueue().size() > 0) {
        createDirect = true;
        continue;
    }
    }
	
    //如果maxWait大于0,调用 pollLast(nanos),反之则调用takeLast()
    //获取连接的核心逻辑
    if (maxWait > 0) {
        holder = pollLast(nanos);
    } else {
        holder = takeLast();
    }

}

getConnectionInternal 方法内部存在一大堆参数检测功能,根据一系列参数判断,是否需要直接创建一个连接。
反之,则调用pollLast 或 takeLast 方法。这两个方法如果获取不到连接,将会wait,之后通知生产者线程创建连接。
这个地方有一个风险就是,如果仅仅采用单线程的方式创建连接,一旦生产者线程由于其他原因阻塞,那么getConnection将会产被长时间阻塞。

之后获得holder之后,通过holder产生连接。

holder.incrementUseCount();
DruidPooledConnection poolalbeConnection = new DruidPooledConnection(holder);

4.pollLast 与 takeLast

polllast的方法核心逻辑是自旋,判断是否有可用连接,然后发送empty消息,通知生产者线程可以创建连接。之后阻塞wait。只不过这个方法需要设置超时时间。

for (;;) {
        //如果没有可用的连接
        if (poolingCount == 0) {
         emptySignal(); // send signal to CreateThread create connection

           estimate = notEmpty.awaitNanos(estimate); // signal by
           
         }
         //之后获取最后一个连接
           DruidConnectionHolder last = connections[poolingCount];
}       

而takeLast方法与pollLast方法类似,只是等待的过程中,不增加超时时间,一直等到生产者的通知为止。

 while (poolingCount == 0) {
 
    emptySignal(); // send signal to CreateThread create connection
     
    try {
        notEmpty.await(); // signal by recycle or creator
    } finally {
        notEmptyWaitThreadCount--;
    }
    
 }
decrementPoolingCount();
//最后获取数组中的最后一个连接。
DruidConnectionHolder last = connections[poolingCount];
connections[poolingCount] = null;

至此,getConnection的过程大致分析完毕。

http-nio-8080-exec-4 [ERROR] c.alibaba.druid.pool.DruidDataSource recyle error java.sql.SQLSyntaxErrorException: (conn=4199621) Connection.clearWarnings cannot be called on a closed connection at com.oceanbase.jdbc.internal.util.exceptions.ExceptionFactory.createException(ExceptionFactory.java:110) at com.oceanbase.jdbc.internal.util.exceptions.ExceptionFactory.create(ExceptionFactory.java:211) at com.oceanbase.jdbc.OceanBaseConnection.clearWarnings(OceanBaseConnection.java:1325) at com.alibaba.druid.pool.DruidConnectionHolder.reset(DruidConnectionHolder.java:295) at com.alibaba.druid.pool.DruidDataSource.recycle(DruidDataSource.java:1662) at com.alibaba.druid.pool.DruidPooledConnection.recycle(DruidPooledConnection.java:323) at com.alibaba.druid.pool.DruidPooledConnection.close(DruidPooledConnection.java:268) at org.springframework.jdbc.datasource.DataSourceUtils.doCloseConnection(DataSourceUtils.java:404) at org.springframework.jdbc.datasource.DataSourceUtils.doReleaseConnection(DataSourceUtils.java:391) at org.springframework.jdbc.datasource.DataSourceUtils.releaseConnection(DataSourceUtils.java:358) at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:302) at org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:400) at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:373) at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:574) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:361) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMet
最新发布
07-12
<think>我们面对的问题是:Druid连接池在回收连接时,尝试调用`Connection.clearWarnings()`方法,但此时连接已经关闭(closed),导致抛出`java.sql.SQLSyntaxErrorException: Connection.clearWarnings cannot be called on a closed connection`异常。 根据引用[1]和引用[2]的报错信息,我们可以确定问题发生在连接回收(recycle)的过程中。具体来说,在`DruidDataSource.recycle`方法中,会调用`DruidConnectionHolder.reset`方法,而在该方法中调用了`Connection.clearWarnings()`,而此时连接已经被关闭,因此抛出异常。 解决这个问题的思路: 1. 避免在连接关闭后还调用`clearWarnings`方法。 2. 检查连接池的配置,确保连接在回收时没有被提前关闭。 3. 考虑升级Druid版本,因为可能是一个已知问题,在新版本中已经修复。 4. 如果问题仍然存在,可以尝试通过配置Druid来避免调用`clearWarnings`方法。 具体步骤: 步骤1:检查连接是否被提前关闭 在应用程序中,确保没有在将连接归还给连接池之前手动关闭连接。另外,检查是否有代码在事务结束后没有正确释放连接,或者异常处理中关闭了连接。 步骤2:配置Druid连接池 Druid提供了一些配置选项,可以避免在连接关闭后执行不必要的操作。我们可以尝试设置`clearFiltersEnable`为`false`,或者禁用一些可能会导致此问题的过滤器。 另外,Druid有一个配置项`testOnBorrow`、`testOnReturn`等,我们可以使用`testWhileIdle`来代替,并配置一个有效的验证查询(例如OceanBase的`SELECT 1`),以确保连接的有效性。 但是,对于`clearWarnings`的问题,我们可以尝试在Druid的配置中设置: ```properties # 是否在回收连接时执行reset(包括clearWarnings) druid.poolPreparedStatements=false druid.maxPoolPreparedStatementPerConnectionSize=-1 # 或者尝试关闭连接的reset druid.resetStatements=false ``` 注意:`resetStatements`是Druid的一个配置项,用于控制在回收连接时是否执行`reset`方法(其中包含`clearWarnings`)。我们可以将其设置为`false`来避免调用`clearWarnings`。 步骤3:升级Druid版本 查看Druid的官方issue,看是否有类似的问题以及是否已经修复。如果有,升级到修复该问题的版本。 步骤4:自定义过滤器 如果以上方法都不行,我们可以考虑自定义一个Druid过滤器,跳过`clearWarnings`的调用。这需要我们对Druid源码有一定的了解,并编写自定义过滤器。 步骤5:检查OceanBase驱动 另外,检查使用的OceanBase JDBC驱动版本,确保其与Druid兼容,并且没有已知的bug。可能需要升级OceanBase驱动。 具体配置示例(在Spring Boot的application.properties中): ```properties # Druid连接池配置 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.druid.initial-size=5 spring.datasource.druid.min-idle=5 spring.datasource.druid.max-active=20 spring.datasource.druid.test-while-idle=true spring.datasource.druid.validation-query=SELECT 1 # 关闭回收时的reset spring.datasource.druid.reset-statements=false ``` 如果使用YAML配置: ```yaml spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: initial-size: 5 min-idle: 5 max-active: 20 test-while-idle: true validation-query: SELECT 1 reset-statements: false ``` 注意:关闭`reset-statements`可能会带来一些副作用,比如连接状态(如事务隔离级别、警告等)没有被重置,可能会影响后续使用。因此,需要确保应用程序不会依赖这些状态。 如果问题仍然存在,我们可以考虑在Druid的issue列表中搜索相关问题,或者向Druid社区反馈。 总结:首先尝试配置`reset-statements=false`,然后升级Druid和OceanBase驱动版本,如果问题依旧,再考虑自定义过滤器。 相关问题: 1. 如何配置Druid连接池以避免在连接关闭后调用clearWarnings? 2. 在关闭Druid的reset-statements后,可能会有什么副作用? 3. 如何升级Druid版本? 4. 如何自定义Druid过滤器来跳过clearWarnings的调用?</think>### Druid连接池在OceanBase中调用`clearWarnings`报错排查与解决 #### 问题分析 根据报错信息 `java.sql.SQLSyntaxErrorException: Connection.clearWarnings cannot be called on a closed connection`,核心问题是:**Druid连接池在回收连接时,尝试对已关闭的数据库连接调用`clearWarnings()`方法**。具体原因如下: 1. **连接提前关闭** OceanBase驱动可能在连接归还给Druid池前已关闭连接,而Druid的`recycle()`方法仍会调用`clearWarnings()`[^1]。 2. **驱动兼容性问题** OceanBase的JDBC驱动(如`oceanbase-client`)与Druid的兼容性可能导致此异常,尤其当连接状态管理不一致时[^2]。 3. **连接泄漏** 应用程序未正确释放连接(如未关闭`ResultSet`/`Statement`),导致Druid误判连接状态。 --- #### 解决方案 ##### 方法1:升级驱动与Druid版本(推荐) ```xml <!-- pom.xml 示例 --> <dependency> <groupId>com.oceanbase</groupId> <artifactId>oceanbase-client</artifactId> <version>2.4.0</version> <!-- 使用较新版本 --> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.16</version> <!-- 确认兼容性 --> </dependency> ``` **注意**:检查[OceanBase官方文档](https://open.oceanbase.com/docs)确认驱动与Druid的兼容性。 --- ##### 方法2:修改Druid配置(临时规避) 在`application.properties`中添加: ```properties # 关闭连接回收时的reset操作(避免调用clearWarnings) druid.resetStatements=false # 启用空闲连接检测 druid.testWhileIdle=true druid.validationQuery=SELECT 1 druid.timeBetweenEvictionRunsMillis=60000 ``` **副作用**:关闭`resetStatements`可能导致连接状态未重置(如事务隔离级别),需确保应用能处理此场景。 --- ##### 方法3:修复连接泄漏 1. **检查代码资源释放** 确保所有数据库操作遵循模式: ```java try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT ...")) { // 业务逻辑 } // 自动关闭资源 ``` 2. **启用Druid监控** 在配置中开启泄漏检测: ```properties druid.removeAbandoned=true druid.removeAbandonedTimeoutMillis=300000 // 5分钟 druid.logAbandoned=true ``` --- ##### 方法4:自定义Druid扩展(高级) 通过过滤器跳过`clearWarnings`: ```java public class OceanBaseDruidFilter extends FilterAdapter { @Override public void connection_clearWarnings(FilterChain chain, ConnectionProxy connection) { if (!connection.isClosed()) { // 仅对未关闭连接操作 super.connection_clearWarnings(chain, connection); } } } ``` 注册过滤器: ```properties druid.filters=com.example.OceanBaseDruidFilter ``` --- #### 验证步骤 1. 部署修复后,监控Druid日志: ```bash grep "recycle error" druid.log ``` 2. 使用`jstack`检查连接状态: ```bash jstack <PID> | grep -A10 "Druid-ConnectionPool" ``` --- #### 根本原因总结 $$ \text{报错根源} = \frac{ \text{驱动兼容性} + \text{连接状态管理冲突} }{ \text{Druid回收机制} } $$ 当OceanBase驱动提前关闭连接,而Druid仍按MySQL协议规范调用`clearWarnings()`时,触发此异常。优先升级驱动或调整配置可解决[^1][^2]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值