2

HikariPool之后,今天研究另一主流连接池Druid。

关于数据库连接池的基本认知

先对数据库连接池的基本工作原理做个了解,不管是HikariPool、还是druid,所有的数据库连接池应该都是按照这个基本原理工作和实现的,带着这个思路去学习数据库连接池,避免盲人摸象。

数据库连接池一定会包含以下基本逻辑:

  1. 创建连接并池化:初始化的时候创建、或者是在应用获取连接的过程中创建,连接创建好之后放在连接池(内存中的容器,比如List)中保存。
  2. 获取数据库连接:接管了获取数据库连接的方法,从连接池中获取、而不是创建连接。
  3. 关闭数据库连接:接管了关闭数据库连接的方法,将连接归还到连接池、而不是真正的关闭数据库连接。
  4. 连接池维护:连接池容量、连接超时清理等工作。

带着这个思路研究HikariPool的源码,会有事半功倍的功效。

认识Druid的结构

包括以下几个部分:

  1. DruidConnectionHolder
  2. Connections
  3. evictConnections
  4. keepAliveConnections
  5. destroyConnectionThread/destroySchedulerFuture
  6. createConnectionThread/createSchedulerFuture

DruidConnectionHolder

HikariPool通过poolEntry持有数据库连接,Druid通过DruidConnectionHolder持有数据库连接。

DruidConnectionHolder持有物理数据库连接Connectin对象,以及该连接的相关属性,比如connectTimeMillis、lastActiveTimeMillis、lastExecTimeMillis,以及underlyingReadOnly、underlyingAutoCommit、underlyingTransactionIsolation等等。连接池可以根据这些属性以及相关参数执行相应的houseKeep。

Connections

Connections是DruidConnectionHolder组成的数组,是Druid连接池中唯一存储可用连接的地方,看起来会比HikariPool简单许多(HikariPool有三个存储连接的地方),但是这可能也是Druid在性能上稍逊于HikariPool的原因之一。

evictConnections

存储需要被回收的连接的数组,在连接池进行清理的时候用来存储需要被关闭的连接。

keepAliveConnections

存储保持活动的连接的数组。

createConnectionThread/createSchedulerFuture

Druid根据配置,可以通过createConnectionThread线程、或者createSchedulerFuture线程任务创建数据库连接并加入连接池。

Druid也许并没有默认的createSchedulerFuture的实现,如果要启用createSchedulerFuture,需要配置createSchedulerFuture的实现类。

createConnectionThread是Druid默认的创建连接的线程,负责获取物理连接、组装物理连接为DruidConnectionHolder并加入到connections数组中。

destroyConnectionThread/destroySchedulerFuture

与创建连接的方式类似,Druid提供两种不同的方式销毁(或者关闭)过期的数据库连接。默认实现是destroyConnectionThread。

好了,Druid的基础结构了解完了,我们采用和HikariPool完全一样的分析套路,接下来要进入源码分析了,主要包括:

  1. Druid连接池的初始化
  2. 获取数据库连接 - getConnection方法
  3. 关闭数据库连接 - close方法

Druid的初始化

Druid的初始化过程貌似和HikariPool稍有不同,因为HikariPool默认的在获取连接之前的HikariPool实例化过程中就完成了连接池的初始化。

所谓完成连接池的初始化,指的是按照参数的设定,完成了数据库连接的创建和池化,也就是说连接池已经准备好了,应用在通过getConnecton方法获取连接的时候,直接从连接池中borrow就可以了。

Druid貌似不这样。我们看一下DruidDataSource的实例化方法:

public DruidDataSource(){
        this(false);
    }

    public DruidDataSource(boolean fairLock){
        super(fairLock);

        configFromPropety(System.getProperties());
    }

super指的是DruidAbstractDataSource,他的构造方法:

public DruidAbstractDataSource(boolean lockFair){
        lock = new ReentrantLock(lockFair);

        notEmpty = lock.newCondition();
        empty = lock.newCondition();
    }

只初始化了ReentrantLock,以及他的两个Condition:empty和notEmpty。

而configFromPropety只是负责把参数从配置文件中读入,不做其他的事情。

所以,连接池的初始化过程没有放在DruidDataSource的创建过程中。

既然构造方法中没有完成连接池的初始化,我们自然而然的就想到去看看getConnection方法,不做初始化、怎么能获取到数据库连接?

果然,getConnectin方法的第一行代码就是:调用init()方法。

init的方法很长,不过有很多代码都是检查参数合理性的,这部分代码我们直接跳过:

            ...忽略n多行代码
            connections = new DruidConnectionHolder[maxActive];
            evictConnections = new DruidConnectionHolder[maxActive];
            keepAliveConnections = new DruidConnectionHolder[maxActive];

创建了我们前面说过的存储数据库连接的connections(其实就是池),以及另外两个辅助数组evictConnections和keepAliveConnections,连接池的大小初始化为maxActive。

接下来:

            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();
                }
            }

检查createScheduler不空、并且参数设置为asyncInit(异步初始化)的话,则提交initialSize个连接创建任务。我们说过createScheduler并非Druid的默认实现,所以我们暂时不管这部分代码。

下面的代码逻辑是:如果不是异步初始化的话,那就是要同步初始化,也就是当前线程要负责完成连接池的初始化,则循环创建initialSize个物理连接、封装为DruidConnectionHolder后直接加入到connections中。

这个过程就相当于完成了数据库连接池的初始化,但是建议设置asyncInit参数为true:异步初始化。因为如果同步初始化、并且initialSize设置比较大的话,应用的首个getConnection方法肯定会耗时比较长,用户体验不好。

继续看代码,如果asyncInit设置为true,异步初始化的话:

            createAndLogThread();
            createAndStartCreatorThread();
            createAndStartDestroyThread();

createAndLogThread创建logStatsThread线程,logStatsThread线程负责定期收集Druid连接池的状态并通过log打印出来,是Druid统计分析的一部分。

createAndStartCreatorThread是启动连接创建线程,异步初始化的情况下,连接池就是通过createAndStartCreatorThread线程创建的。

createAndStartDestroyThread是启动连接销毁的线程,作用类似于HikariPool的houseKeep线程。

createAndStartCreatorThread、createAndStartDestroyThread这两个方法的源码我们先放放,稍后分析。

我们先一鼓作气完成init()方法源码的剩余部分:

            initedLatch.await();
            init = true;

            initedTime = new Date();
            registerMbean();

            if (connectError != null && poolingCount == 0) {
                throw connectError;
            }

initedLatch是数量=2的CountDownLatch,在DruidDataSource类成员变量初始化的时候定义好的,这里initedLatch.await();的意思是等待连接创建线程和连接销毁线程完成启动。

之后,打标签init=true,表明初始化已完成,进行异常处理、锁释放等扫尾工作。

初始化完成!

createAndStartCreatorThread

创建并启动“创建连接线程”:

   protected void createAndStartCreatorThread() {
        if (createScheduler == null) {
            String threadName = "Druid-ConnectionPool-Create-" + System.identityHashCode(this);
            createConnectionThread = new CreateConnectionThread(threadName);
            createConnectionThread.start();
            return;
        }

        initedLatch.countDown();
    }

代码非常简单,就是创建并启动createConnectionThread。createConnectionThread线程负责物理连接的创建、以及连接的池化。

createConnectionThread#run

连接的创建及池化是在createConnectionThread线程的run方法中完成的。

       public void run() {
            initedLatch.countDown();

            long lastDiscardCount = 0;
            int errorCount = 0;
            for (;;) {
                // addLast
                try {
                    lock.lockInterruptibly();
                } catch (InterruptedException e2) {
                    break;
                }

               long discardCount = DruidDataSource.this.discardCount;
                boolean discardChanged = discardCount - lastDiscardCount > 0;
                lastDiscardCount = discardCount;

                try {
                    boolean emptyWait = true;

                    if (createError != null
                            && poolingCount == 0
                            && !discardChanged) {
                        emptyWait = false;
                    }

                    if (emptyWait
                            && asyncInit && createCount < initialSize) {
                        emptyWait = false;
                    }

线程启动之后会不断检测是否需要创建连接,在run方法的无条件for循环中完成。

首先获得锁。

discardChanged变量表示在每次循环检测过程中,被discardCount的连接数是否有增长。

然后定义了一个emptyWait变量,用来表示是否需要暂缓创建连接、直到等待获取连接的线程唤醒之后才创建。

对emptyWait的处理逻辑是:如果创建连接发生了错误并且当前连接池空、并且没有发生discardChanged,则不等待。或者,如果是异步初始化并且初始化的连接池数量尚未满足initialSize的要求,则不等待。

然后:

                  if (emptyWait) {
                        // 必须存在线程等待,才创建连接
                        if (poolingCount >= notEmptyWaitThreadCount //
                                && (!(keepAlive && activeCount + poolingCount < minIdle))
                                && !isFailContinuous()
                        ) {
                            empty.await();
                        }

                        // 防止创建超过maxActive数量的连接
                        if (activeCount + poolingCount >= maxActive) {
                            empty.await();
                            continue;
                        }
                    }

                } catch (InterruptedException e) {
                    lastCreateError = e;
                    lastErrorTimeMillis = System.currentTimeMillis();

                    if ((!closing) && (!closed)) {
                        LOG.error("create connection Thread Interrupted, url: " + jdbcUrl, e);
                    }
                    break;
                } finally {
                    lock.unlock();
                }

这段代码是需要等待的情况,判断当前线程池数量满足等待条件(连接池数量大于notEmptyWaitThreadCount数量,activeCount + poolingCount数量大于等于minIdle或者是keepAlive)。或者,activeCount + poolingCount已经大于等于maxActive了,则调用empty.await();也就是暂时不再创建连接了,等待获取连接的线程唤醒之后再创建。当然,等待是需要释放锁资源的。

接下来是不等待的代码:

               PhysicalConnectionInfo connection = null;

                try {
                    connection = createPhysicalConnection();
                } catch (SQLException e) {
         //下面是一堆创建连接失败的异常处理,忽略

创建数据库物理连接。之后:


                if (connection == null) {
                    continue;
                }

                boolean result = put(connection);
                if (!result) {
                    JdbcUtils.close(connection.getPhysicalConnection());
                    LOG.info("put physical connection to pool failed.");
                }

                errorCount = 0; // reset errorCount

                if (closing || closed) {
                    break;
                }
            }
        }
    }

如果创建连接过程中发生异常,connection==null,则continue,继续创建。

否则,调用put方法,将新创建的连接加入连接池。当然,如果加入失败的话则关闭刚创建好的连接,以免资源浪费。

接下来看一下连接放入connections的put方法。

put方法

   protected boolean put(PhysicalConnectionInfo physicalConnectionInfo) {
        DruidConnectionHolder holder = null;
        try {
            holder = new DruidConnectionHolder(DruidDataSource.this, physicalConnectionInfo);
        } catch (SQLException ex) {
            lock.lock();
            try {
                if (createScheduler != null) {
                    clearCreateTask(physicalConnectionInfo.createTaskId);
                }
            } finally {
                lock.unlock();
            }
            LOG.error("create connection holder error", ex);
            return false;
        }

        return put(holder, physicalConnectionInfo.createTaskId);
    }

将connection封装为DruidConnectionHolder,之后调用put:

private boolean put(DruidConnectionHolder holder, long createTaskId) {
        lock.lock();
        try {
            if (this.closing || this.closed) {
                return false;
            }

            if (poolingCount >= maxActive) {
                if (createScheduler != null) {
                    clearCreateTask(createTaskId);
                }
                return false;
            }
            connections[poolingCount] = holder;
            incrementPoolingCount();

            if (poolingCount > poolingPeak) {
                poolingPeak = poolingCount;
                poolingPeakTime = System.currentTimeMillis();
            }

            notEmpty.signal();
            notEmptySignalCount++;

            if (createScheduler != null) {
                clearCreateTask(createTaskId);

                if (poolingCount + createTaskCount < notEmptyWaitThreadCount //
                    && activeCount + poolingCount + createTaskCount < maxActive) {
                    emptySignal();
                }
            }
        } finally {
            lock.unlock();
        }
        return true;
    }

首先获取锁资源。

之后判断如果创建的连接数大于最大活动连接数poolingCount >= maxActive的话,则不放入连接池直接返回。

接下来,连接放入连接池connections中,poolingCount加1。

接下来notEmpty.signal();通知等待获取连接的线程。

之后释放锁资源,连接加入连接池完成!

createAndStartDestroyThread方法

创建并启动DestroyThread,直接看destroyConnectionThread的run方法:

        public void run() {
            initedLatch.countDown();

            for (;;) {
                // 从前面开始删除
                try {
                    if (closed || closing) {
                        break;
                    }

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

                    if (Thread.interrupted()) {
                        break;
                    }

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

    }

检查timeBetweenEvictionRunsMillis,该参数的意思是执行连接回收的间隔时间,如果该参数设置为>0,则线程睡眠timeBetweenEvictionRunsMillis之后再执行,否则线程每秒执行一次。

调用destroyTask.run();-> shrink()方法执行线程回收。

连接回收的主要工作是shrink方法完成的,篇幅原因,放在下一篇文章研究。

小结

了解了Druid连接池的基础结构及其初始化过程,以及连接池中连接销毁的发起机制:通过destroyConnectionThread线程调用destroyTask对空闲超时的连接进行回收,确保连接池中的连接保持在健康状态。连接回收的主要逻辑是在shrink方法中,其实也是Druid连接池中比较关键的一部分,下一篇文章分析。

Thanks a lot!

上一篇 连接池 HikariPool (二) - 获取及关闭连接
下一篇 连接池 Druid (二) - 连接回收 DestroyThread


45 声望17 粉丝