Druid源码解析-参数篇

前言:

    作为一个比较流行的数据库连接池框架,Druid以其功能强大、易于扩展而著称。

    使用连接池的好处不言而喻,在之前的Mysql-java的博客中,我们看到,创建一个数据库连接不仅需要常规的三次握手,还要进行用户名密码验证,经历这么多验证之后,才算真正完成一个连接。如果我们执行一次SQL操作均要重新创建连接,那将是巨大的浪费。

    故使用连接池,可以在项目启动时预先创建好一定数量的连接,执行SQL时,从连接池中获取即可,执行完毕后,将连接释放,还回连接池中。

    DruidPool在创建时,需要N多参数,这些参数有些是必须项,有些是优化项,有些则是扩展项。我们就从这些参数的角度来从分析下Druid的源码。

    建议可以带着问题来看Druid的源码,比如Druid是如何创建连接的,有哪些必备参数,连接如何被回收,连接的有效性如何鉴定。这样的话,我们再来看这些参数对应的源码就会恍然大悟。

1.环境准备:

    mysql-server:5.7.17

    mysql-connector-java:5.1.41

    druid:1.1.23

2.代码准备:

DruidDataSource dataSource = new DruidDataSource();
// 以下四个参数为必输项
dataSource.setUrl("jdbc:mysql://localhost:3306/db1");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("root");

// 如果使用在项目中,建议主动调用init方法,初始化连接池
dataSource.init();

// 获取连接
Connection connection = dataSource.getConnection();

// 执行SQL
Statement stmt = connection.createStatement();
stmt.execute("select 1;");

// 获取结果集
ResultSet rs = stmt.getResultSet();

总结:在创建DruidDataSource的时候,我们定义了四个必须参数,实际这个也是我们之前使用DriverManager创建连接时的四个必须参数。这里就不再赘述。

3.连接池中连接数量相关参数

    initialSize:启动连接时,在连接池中初始化的连接个数

    maxActive:连接池中最多支持的活动会话个数

    minIdle:回收空闲连接时,将保证至少有minIdle个连接

    

3.1 initialSize的使用

// DruidDataSource.init 初始化方法,在创建DruidDataSource完成后,默认初始化initialSize个连接
public void init() throws SQLException {
    // 初始化状态,只会被初始化一次
    if (inited) {
        return;
    }
    ...
    initFromSPIServiceLoader();
    resolveDriver();
    initCheck();
	...
    // init connections
    while (poolingCount < initialSize) {
            try {
                // initialSize的使用在这里,poolingCount默认为0,故会初始化initialSize个连接
                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);
                }
            }
     }
}
总结:initialSize比较简单,在DruidDataSource.init()方法调用完成后,默认会初始化initialSize个连接。所以建议在使用Spring创建DruidDataSource bean时,初始调用一下init方法,否则在第一次获取连接时会先执行init方法,拖慢第一次获取连接的速度

3.2 maxActive的使用

// DruidDataSource.getConnectionDirect --> DruidDataSource.getConnectionInternal
// 直接获取连接时,要么直接从已创建好的连接池中获取,要么创建新的连接,这时maxActive就会派上用场
private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {
    if (closed) {
        connectErrorCountUpdater.incrementAndGet(this);
        throw new DataSourceClosedException("dataSource already closed at " + new Date(closeTimeMillis));
    }
    ...
    for (boolean createDirect = false;;) {
        // createDirect什么时候会为true,看下面2的逻辑
        if (createDirect) {
            PhysicalConnectionInfo pyConnInfo = DruidDataSource.this.createPhysicalConnection();
            holder = new DruidConnectionHolder(this, pyConnInfo);
            holder.lastActiveTimeMillis = System.currentTimeMillis();
            
            try {
                // 此时maxActive排上用场,如果活跃的连接数大于maxActive,则discard
                if (activeCount < maxActive) {
                    activeCount++;
                    holder.active = true;
                    if (activeCount > activePeak) {
                        activePeak = activeCount;
                        activePeakTime = System.currentTimeMillis();
                    }
                    break;
                } else {
                    discard = true;
                }
            } finally {
                lock.unlock();
            }

            // 超过maxActive的连接,则直接close掉
            if (discard) {
                JdbcUtils.close(pyConnInfo.getPhysicalConnection());
            }
        }
        
        ...
        // 2
        if (createScheduler != null
            && poolingCount == 0
            && activeCount < maxActive
            && creatingCountUpdater.get(this) == 0
            && createScheduler instanceof ScheduledThreadPoolExecutor) {
            ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) createScheduler;
            if (executor.getQueue().size() > 0) {
                // 当poolingCount==0,也就是预创建连接池中的连接全部被占用
                // 此时会执行上面的直接创建连接的逻辑
                createDirect = true;
                continue;
            }
        }
    }
}
总结:当连接池中的连接全部被占用时,允许创建新的连接,但是总的连接数不能大于maxActive

3.3 minIdle的使用

// DruidDataSource.shrink
public void shrink(boolean checkTime, boolean keepAlive) {
    final int checkCount = poolingCount - minIdle;
    final long currentTimeMillis = System.currentTimeMillis();
    for (int i = 0; i < poolingCount; ++i) {
        DruidConnectionHolder connection = connections[i];
        if (checkTime) {
            if (phyTimeoutMillis > 0) {
                long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
                if (phyConnectTimeMillis > phyTimeoutMillis) {
                    evictConnections[evictCount++] = connection;
                    continue;
                }
            }
        }
        ...    
    }
    
    // 对minIdle之外的连接进行回收操作
    int removeCount = evictCount + keepAliveCount;
    if (removeCount > 0) {
        System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
        Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
        poolingCount -= removeCount;
    }
    keepAliveCheckCount += keepAliveCount;

    if (keepAlive && poolingCount + activeCount < minIdle) {
        needFill = true;
    }
}
总结:当连接长时间不被使用时,会对其进行回收,当然,不是全部空闲连接都被回收,至少要保留minIdle个

4.连接池中连接有效性校验相关参数

4.1 获取连接时有效性检查

testWhileIdle:当程序请求连接时,连接池在分配连接时,是否先检查该连接是否有效
validationQuery:检查池中的连接是否仍有可用的SQL语句,druid会连接到数据库执行该SQL,如果正常返回,则表示连接可用,否则连接不可用
testOnBorrow:程序申请连接时,进行有效性检查
testOnReturn:程序返还连接时,进行连接有效性检查

1)testWhileIdle、testOnBorrow的使用 

// DruidDataSource.getConnectionDirect
public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
 
    // testOnBorrow参数
    if (testOnBorrow) {
        // mysql下,默认使用MySqlValidConnectionChecker
        // 实际上就是使用MysqlConnection.pingInternal方法进行ping操作
        boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
        if (!validate) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("skip not validate connection.");
            }

            discardConnection(poolableConnection.holder);
            continue;
        }
    }else {
        ...
        if (testWhileIdle) {
            final DruidConnectionHolder holder = poolableConnection.holder;
            long idleMillis = currentTimeMillis - lastActiveTimeMillis;
            long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
            if (timeBetweenEvictionRunsMillis <= 0) {
                timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
            }

            // 如果当前连接空闲时间过长,超过我们指定的阈值后,则执行连接检查
            if (idleMillis >= timeBetweenEvictionRunsMillis
                || idleMillis < 0) {
                // 若连接已不可用,则discard,重新获取新的连接
                boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
                if (!validate) {
                    discardConnection(poolableConnection.holder);
                    continue;
                }
            }
        }
}

总结:当申请连接时,我们可以选择进行探活检查,对申请到的连接进行ping操作(mysql)。当然这个功能不建议开启,会对我们的业务有影响

2)validationQuery

// DruidDataSource.init

public void init() throws SQLException {
    if (inited) {
        return;
    }
    ...
    initFromSPIServiceLoader();
    resolveDriver();
    // 初始化检查会使用到validationQuery
    initCheck();
	...
}

protected void initCheck() throws SQLException {
    if (JdbcUtils.ORACLE.equals(this.dbType)) {
        isOracle = true;
		...

        // oracle检验
        oracleValidationQueryCheck();
    } else if (JdbcUtils.DB2.equals(dbType)) {
        // db2校验
        db2ValidationQueryCheck();
    } else if (JdbcUtils.MYSQL.equals(this.dbType)
               || JdbcUtils.MYSQL_DRIVER_6.equals(this.dbType)) {
        // mysql的话没有校验
        isMySql = true;
    }
	...
}

3)testOnReturn

// DruidDataSource.cecycle
protected void recycle(DruidPooledConnection pooledConnection) throws SQLException {
	...
    // 回收连接,即将使用完毕的连接放回连接池
    if (testOnReturn) {
        // 同样进行可用性测试
        boolean validate = testConnectionInternal(holder, physicalConnection);
        if (!validate) {
            JdbcUtils.close(physicalConnection);

            destroyCountUpdater.incrementAndGet(this);

            lock.lock();
            try {
                if (holder.active) {
                    activeCount--;
                    holder.active = false;
                }
                closeCount++;
            } finally {
                lock.unlock();
            }
            return;
        }
    }
}

总结:在连接获取或者连接回收时进行的探活操作,(mysql)下就是执行ping操作

4.2 获取连接超时检查

    maxWait:从连接池中请求连接时,当超过maxWait后,则认为本次请求失败

// DruidDataSource.getConnection
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);
    }
}

// DruidDataSource.getConnectionDirect --> DruidDataSource.getConnectionInternal
private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {

    final long nanos = TimeUnit.MILLISECONDS.toNanos(maxWait);
    ...
    // 当从连接池中获取连接时,可以选择等待maxWait,也可以选择一直等待下去
    // 等待连接池的连接释放
    if (maxWait > 0) {
        holder = pollLast(nanos);
    } else {
        holder = takeLast();
    }
}

总结:当我们使用连接池的连接时,若所有连接均被占用,则应用需要等待maxWait,若maxWait时间内没有等待连接释放,则报错

4.3 空闲连接检查

    minEvictableIdleTimeMillis:当连接池中的某个连接的空闲时间达到N毫秒后,连接池在下次检查空闲连接时,将回收该连接

    timeBetweenEvictionRunsMillis:检查空闲连接的频率,单位为毫秒

    keepAlive:程序没有close连接且空闲时长超过minEvictableIdleTimeMillis,则会执行validationQuery指定的SQL,以保证该程序连接不会被kill,其范围不超过minIdle指定的连接个数

源码角度分析下这三个参数:

// DruidDataSource.init()方法执行时,也会默认创建CreatorThread和DestroyThread
public void init() throws SQLException {
	...
    createAndLogThread();
    createAndStartCreatorThread();
    // 创建销毁连接线程
    createAndStartDestroyThread();
}

// DruidDataSource.createAndStartDestroyThread
protected void createAndStartDestroyThread() {
    // 销毁任务
    destroyTask = new DestroyTask();

    if (destroyScheduler != null) {
        long period = timeBetweenEvictionRunsMillis;
        if (period <= 0) {
            period = 1000;
        }
        // 定时器的检查周期即为timeBetweenEvictionRunsMillis
        destroySchedulerFuture = destroyScheduler.scheduleAtFixedRate(destroyTask, period, period,
                                                                      TimeUnit.MILLISECONDS);
        initedLatch.countDown();
        return;
    }

    String threadName = "Druid-ConnectionPool-Destroy-" + System.identityHashCode(this);
    destroyConnectionThread = new DestroyConnectionThread(threadName);
    destroyConnectionThread.start();
}

// DestroyTask.java
public class DestroyTask implements Runnable {
    @Override
    public void run() {
        // 
        shrink(true, keepAlive);

        if (isRemoveAbandoned()) {
            removeAbandoned();
        }
    }
}

// DruidDataSource.shrink
public void shrink(boolean checkTime, boolean keepAlive) {
    for (int i = 0; i < poolingCount; ++i) {
       // 若当前连接空闲时间,超过minEvictableIdleTimeMillis,则回收该连接
        long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
        if (idleMillis >= minEvictableIdleTimeMillis) {
           if (checkTime && i < checkCount) {
               evictConnections[evictCount++] = connection;
               continue;
           } else if (idleMillis > maxEvictableIdleTimeMillis) {
               evictConnections[evictCount++] = connection;
               continue;
           }
        } 
        
        // 若空闲时间超过keepAliveBetweenTimeMillis,且当前支持keepAlive,则执行一次有效性检查操作
        // 以更新连接的lastActiveTimeMillis,使其不被销毁
        if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) {
            keepAliveConnections[keepAliveCount++] = connection;
        }
        ...
        // 销毁长期空闲的连接(空闲时间大于minEvictableIdleTimeMillis)
        if (evictCount > 0) {
            for (int i = 0; i < evictCount; ++i) {
                DruidConnectionHolder item = evictConnections[i];
                Connection connection = item.getConnection();
                JdbcUtils.close(connection);
                destroyCountUpdater.incrementAndGet(this);
            }
            Arrays.fill(evictConnections, null);
        }
        
        // 对于空闲连接,执行SQL操作,更新其最新活跃时间,防止其被kill掉(空闲时间大于keepAliveBetweenTimeMillis)
        for (int i = keepAliveCount - 1; i >= 0; --i) {
            DruidConnectionHolder holer = keepAliveConnections[i];
            Connection connection = holer.getConnection();
            holer.incrementKeepAliveCheckCount();

            boolean validate = false;
            try {
                // 探活检查
                this.validateConnection(connection);
                validate = true;
            } catch (Throwable error) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("keepAliveErr", error);
                }
                // skip
            }
        }
    }
}

总结:对于长期没被使用的连接,则在keepAlive情况下,会默认发送validationQuery指定的SQL,以更新其最后活跃时间;否则,在超过最小空闲时间后,该连接会被close掉

4.4 连接使用回收检查

    removeAbandoned:要求程序从连接池中获取到连接后,N秒后必须close,否则druid会强制回收该连接

    removeAbandonedTimeout:设置druid强制回收连接的时限,当程序从池中获取到连接开始算起,超过此值后,druid强制回收该连接

// 在DestroyTask.java中,
public class DestroyTask implements Runnable {
    @Override
    public void run() {
        // 检查空闲连接
        shrink(true, keepAlive);

        // 回收超时连接
        if (isRemoveAbandoned()) {
            removeAbandoned();
        }
    }
}

// DruidDataSource.removeAbandoned
public int removeAbandoned() {
    ...
    Iterator<DruidPooledConnection> iter = activeConnections.keySet().iterator();
    for (; iter.hasNext();) {
        DruidPooledConnection pooledConnection = iter.next();

        if (pooledConnection.isRunning()) {
            continue;
        }

        long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000);

        // 连接的使用时间超过我们指定的阈值后,则直接close并丢弃
        if (timeMillis >= removeAbandonedTimeoutMillis) {
            iter.remove();
            pooledConnection.setTraceEnable(false);
            abandonedList.add(pooledConnection);
        }
    }
}

总结:这两个参数主要为了防止,应用在获取连接执行完相应的SQL处理后忘记归还连接。

有了这连个参数,就可以强制删除未归还的连接,后续会重新创建新的连接。

总结:

此时我们就可以回答下在文章开头,我们提出的几个问题

A:Druid是如何创建连接的,有哪些必备参数

Q:Druid创建连接本质上还是通过对应的Driver(根据不同的的数据库类型)的connect方法来创建连接的,具体可见之前的mysql-java篇博客;必备参数就是常用的minIdle、maxActive等

A:连接如何被回收

Q:长时间不用的连接会被回收掉。回收是DestroyTask做的工作,并保持minIdle最小活跃连接。

A:连接的有效性如何鉴定

Q:在获取连接、归还连接、空闲时检查,都可以对连接的有效性进行检查。针对mysql而言就是执行ping命令来检查有效性。    

Guess you like

Origin blog.csdn.net/qq_26323323/article/details/121341007