[ Druid ] 源码拆解 —— 3. 连接池到底是如何做到 收缩的 ?

Posted 削尖的螺丝刀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[ Druid ] 源码拆解 —— 3. 连接池到底是如何做到 收缩的 ?相关的知识,希望对你有一定的参考价值。

        开头的文章我们从 Druid 的入口了解了它从初始化到创建获取链接,然后到最终销毁的过程,但还有一块没有细说,那就是 Shrink 它的英文本意有一层缩水的意思,没错就像你新买的牛仔裤洗完就缩水了。但是这里的缩水明显更加智能,它是池化的一项必备技能,你可以在各种池化工具中看到它的身影 

同时文中还提到各种参数的初始化,我们这里再从 Druid 官方文档 来看看,主要的配置参数都有哪些:

配置

缺省值

说明

name

配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。如果没有配置,将会生成一个名字,格式是:"DataSource-" + System.identityHashCode(this). 另外配置此属性至少在1.0.5版本中是不起作用的,强行设置name会出错。详情-点此处

url

连接数据库的url,不同数据库不一样。例如:
mysql : jdbc:mysql://10.20.153.104:3306/druid2
oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto

username

连接数据库的用户名

password

连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里

driverClassName

根据url自动识别

这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName

initialSize

0

初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时

maxActive

8

最大连接池数量

maxIdle

8

已经不再使用,配置了也没效果

minIdle

最小连接池数量

maxWait

获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。

poolPreparedStatements

false

是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。

maxPoolPreparedStatementPerConnectionSize

-1

要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100

validationQuery

用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。

validationQueryTimeout

单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法

testOnBorrow

true

申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。

testOnReturn

false

归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。

testWhileIdle

false

建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。

keepAlive

false
(1.0.28)

连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。

timeBetweenEvictionRunsMillis

1分钟(1.0.14)

有两个含义:
1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明

numTestsPerEvictionRun

30分钟(1.0.14)

不再使用,一个DruidDataSource只支持一个EvictionRun

minEvictableIdleTimeMillis

连接保持空闲而不被驱逐的最小时间

connectionInitSqls

物理连接初始化的时候执行的sql

exceptionSorter

根据dbType自动识别

当数据库抛出一些不可恢复的异常时,抛弃连接

filters

属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:
监控统计用的filter:stat
日志用的filter:log4j
防御sql注入的filter:wall

proxyFilters

类型是List<com.alibaba.druid.filter.Filter>,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系


        在上篇文章中提到过,初始化时有一个  CountdownLatch  等待两个守护线程的逻辑, 而其中一个关键守护线程就和本次要讲的内容有关 —— <createAndStartDestroyThread>。我们再来看看上面的参数列表:

  • minIdle
  • keepAlive
  • timeBetweenEvictionRunsMillis
  • minEvictableIdleTimeMillis
  • validationQuery

        等等,这些关键参数都参与其中,组成了该方法中对闲置连接的剔除,达到收缩的目的。同时还涵盖了对保活连接的校验,如果失效,同样会剔除,下面我们就来看看该守护线程要完成的任务细节把 ~~


0.入口

 protected void createAndStartDestroyThread() 
        destroyTask = new DestroyTask();

        if (destroyScheduler != null) 

            long period = timeBetweenEvictionRunsMillis;
            if (period <= 0) 
                period = 1000;
            
            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();
    

1.心跳时长开启校验

 public class DestroyConnectionThread extends Thread 

        public DestroyConnectionThread(String name)
            super(name);
            this.setDaemon(true);
        

        public void run() 
            initedLatch.countDown();

            for (;;) 
                // 从前面开始删除
                try 
                    if (closed || closing) 
                        break;
                    
                    //timeBetweenEvictionRunsMillis是触发心跳间隔时间 ,如果有设置则休眠指定时间段后开始心跳检查 (默认1分钟)

                    if (timeBetweenEvictionRunsMillis > 0) 
                        Thread.sleep(timeBetweenEvictionRunsMillis);
                     else 
                        Thread.sleep(1000); //
                    

                    if (Thread.interrupted()) 
                        break;
                    

                    destroyTask.run();
                 catch (InterruptedException e) 
                    break;
                
            
        

    

2.动态收缩核心逻辑

public void shrink(boolean checkTime, boolean keepAlive) 
        try 
            // 螺丝刀补充: 获取重入锁
            lock.lockInterruptibly();
         catch (InterruptedException e) 
            return;
        

        boolean needFill = false;
        // 待剔除连接数
        int evictCount = 0;
        // 保活连接数
        int keepAliveCount = 0;
        int fatalErrorIncrement = fatalErrorCount - fatalErrorCountLastShrink;
        fatalErrorCountLastShrink = fatalErrorCount;

        try 
            if (!inited) 
                return;
            

            // 螺丝刀补充:通过池中连接数 减去 最小连接池数量, 获取需要检测连接的数量
            final int checkCount = poolingCount - minIdle;
            final long currentTimeMillis = System.currentTimeMillis();
            for (int i = 0; i < poolingCount; ++i) 
                DruidConnectionHolder connection = connections[i];

                // 螺丝刀补充: 如果连接发生了致命性异常,则会加入保活连接数组,接下来校验有效性
                if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis))  
                    keepAliveConnections[keepAliveCount++] = connection;
                    continue;
                

                if (checkTime) 
                    // 螺丝刀补充: 是否设置了物理连接的超时时间phyTimoutMills
                    if (phyTimeoutMillis > 0) 
                       // 当前时间 减去 连接时长  —— 根据判断连接时间存活时间是否已经超过phyTimeoutMills,是则把该连接放入准备剔除的数组中。
                        long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
                        if (phyConnectTimeMillis > phyTimeoutMillis) 
                            evictConnections[evictCount++] = connection;
                            continue;
                        
                    

                    // 当前时间 减去 最近一次活跃时间 算出闲置时长
                    long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;

                    if (idleMillis < minEvictableIdleTimeMillis
                            && idleMillis < keepAliveBetweenTimeMillis
                    ) 
                        break;
                    

                    // 闲置时间大于minEvictableIdleTimeMillis
                    if (idleMillis >= minEvictableIdleTimeMillis) 

                        // 并且 索引(在连接池中的index)小于checkCount的连接 , 放入准备剔除的数组中
                        if (checkTime && i < checkCount) 
                            evictConnections[evictCount++] = connection;
                            continue;

                            // 闲置时长 大于 maxEvictableIdleTimeMillis , 放入准备剔除的数组中
                         else if (idleMillis > maxEvictableIdleTimeMillis) 
                            evictConnections[evictCount++] = connection;
                            continue;
                        
                    

                    // 如果开启保活机制并且空闲时间大于等于保活间隔时间,则加入保活连接数组
                    if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) 
                        keepAliveConnections[keepAliveCount++] = connection;
                    
                 else 
                    if (i < checkCount) 
                        evictConnections[evictCount++] = connection;
                     else 
                        break;
                    
                
            

            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;
            
         finally 
            lock.unlock();
        

        // 螺丝刀补充: 待剔除连接数大于0 ,遍历准备剔除的连接,逐个关闭, 并记录数量
        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);
        

        // 螺丝刀补充:保活连接数大于0 , 倒序校验连接的有效性,如果有效则重新加入队列,反之则会关闭连接
        if (keepAliveCount > 0) 
            // keep order
            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
                

                // 螺丝刀补充:如果上面的校验抛出了异常,这里的discard(抛弃)的则会为true
                boolean discard = !validate;
                // 螺丝刀补充: 有效则重新放回池子中
                if (validate) 
                    
                    holer.lastKeepTimeMillis = System.currentTimeMillis();
                    // 放入细节
                    boolean putOk = put(holer, 0L, true);
                    if (!putOk) 
                        discard = true;
                    
                    
                

                // 螺丝刀补充: 校验过为失效连接,直接关闭,并做记录
                if (discard) 
                    try 
                        connection.close();
                     catch (Exception e) 
                        // skip
                    

                    lock.lock();
                    try 
                        discardCount++;

                        if (activeCount + poolingCount <= minIdle) 
                            emptySignal();
                        
                     finally 
                        lock.unlock();
                    
                
            
            this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount);
            Arrays.fill(keepAliveConnections, null);
        

        // 螺丝刀补充: 这里就是上面走闲置连接校验时判断的是否需要补充线程逻辑
        if (needFill) 
            lock.lock();
            try 
                int fillCount = minIdle - (activeCount + poolingCount + createTaskCount);
                for (int i = 0; i < fillCount; ++i) 
                    emptySignal();
                
             finally 
                lock.unlock();
            
         else if (onFatalError || fatalErrorIncrement > 0) 
            lock.lock();
            try 
                emptySignal();
             finally 
                lock.unlock();
            
        
    

2.1 收缩细节

        上方代码中说明了: 筛选出的闲置连接会放入待剔除的数组中,而最终的连接剔除细节如下:

   // 螺丝刀补充: 待剔除连接数大于0 ,遍历准备剔除的链接,逐个关闭, 并记录数量
        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);
        

2.2 保活校验

 // 螺丝刀补充:保活连接数大于0 , 倒序校验连接的有效性,如果有效则重新加入队列,反之则会关闭连接
        if (keepAliveCount > 0) 
            // keep order
            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
                

                // 螺丝刀补充:如果上面的校验抛出了异常,这里的discard(抛弃)的则会为true
                boolean discard = !validate;
                // 螺丝刀补充: 有效则重新放回池子中
                if (validate) 
                    holer.lastKeepTimeMillis = System.currentTimeMillis();
                    boolean putOk = put(holer, 0L, true);
                    if (!putOk) 
                        discard = true;
                        
                    
                
                
            

2.2.1 保活校验细节

public void validateConnection(Connection conn) throws SQLException 
    
        // 螺丝刀补充, 获取我们设置的保活校验sql
        String query = getValidationQuery();
        if (conn.isClosed()) 
            throw new SQLException("validateConnection: connection closed");
        

        if (validConnectionChecker != null) 
            boolean result;
            Exception error = null;
            try 
                result = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);

                if (result && onFatalError) 
                    lock.lock();
                    try 
                        if (onFatalError) 
                            onFatalError = false;
                        
                     finally 
                        lock.unlock();
                    
                
             catch (SQLException ex) 
                throw ex;
             catch (Exception ex) 
                result = false;
                error = ex;
            

            // 有效连接校验失效则抛出异常
            if (!result) 
                SQLException sqlError = error != null ? //
                    new SQLException("validateConnection false", error) //
                    : new SQLException("validateConnection false");
                throw sqlError;
            
            return;
        

        // 存在校验SQL,则执行校验
        if (null != query) 
            Statement stmt = null;
            ResultSet rs = null;
            try 
                stmt = conn.createStatement();
                if (getValidationQueryTimeout() > 0) 
                    stmt.setQueryTimeout(getValidationQueryTimeout());
                
                rs = stmt.executeQuery(query);

                // 是否得到结果,无则抛出
                if (!rs.next()) 
                    throw new SQLException("validationQuery didn't return a row");
                

                if (onFatalError) 
                    lock.lock();
                    try 
                        if (onFatalError) 
                            onFatalError = false;
                        
                    
                    finally 
                        lock.unlock();
                    
                
             finally 
                JdbcUtils.close(rs);
                JdbcUtils.close(stmt);
            
        
    

2.3 连接补充细节

// 螺丝刀补充:如果上面的校验抛出了异常,这里的discard(抛弃)的则会为true
                boolean discard = !validate;
                // 螺丝刀补充: 有效则重新放回池子中
                if (validate) 
                    holer.lastKeepTimeMillis = System.currentTimeMillis();
                    boolean putOk = put(holer, 0L, true);
                    if (!putOk) 
                        discard = true;
                    
                

                // 螺丝刀补充: 校验过为失效连接,直接关闭,并做记录
                if (discard) 
                    try 
                        connection.close();
                     catch (Exception e) 
                        // skip
                    

                    lock.lock();
                    try 
                        discardCount++;

                        if (activeCount + poolingCount <= minIdle) 
                            emptySignal();
                        
                     finally 
                        lock.unlock();
                    
                
            
            this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount);
            Arrays.fill(keepAliveConnections, null);


                以上就是连接动态收缩 涵 保活校验 的核心逻辑,因为他是守护线程,所以该逻辑会不断周期性的循环,伴随程序走完一生,直至程序被关闭为止。而其核心目的也只不过是围绕着两个关键数组做文章 , 分别是 一个待剔除的连接数组 和 一个保活连接的校验数组,概括总结如下:

  • 进入保活连接数组的条件:
    • 连接发生了致命性异常
    • 开启保活机制并且空闲时间大于等于保活间隔时间

  • 进入待剔除连接数组的条件:
    • 连接的空闲时间大于设置的物理连接超时时间
    • 连接的空闲时间大于最小驱逐空闲时间,并且轮询索引小于合并计数器
    • 空闲时间大于最大驱逐空闲时间

—— 当然过程中还涵盖一个补充连接的校验,这也离不开小连接数minIdle参数的控制

以上是关于[ Druid ] 源码拆解 —— 3. 连接池到底是如何做到 收缩的 ?的主要内容,如果未能解决你的问题,请参考以下文章

[ Druid ] 源码拆解 —— 2. 连接是如何创建的 ?

[ Druid ] 源码拆解 —— 1. 初始化过程的全局概览

[ Druid ] 源码拆解 —— 1. 初始化过程的全局概览

从连接池到内存池

真难!通过源码告诉你阿里的数据库连接池Druid为啥如此牛逼!

Druid数据库连接池源码分析