数据库连接池

在MybatisPlus启动阶段曾介绍过数据源的实例化,那时还没有真正连接数据库,MP系列中对于数据库的使用也是一笔带过,本篇将以druid连接池为例,介绍数据库连接池相关内容。

与线程池概念相似,数据库连接池是对数据库连接的管理,我们知道Web应用与数据库的连接非常频繁,倘若每次都连接再关闭,这是对资源和性能的一种浪费,数据库连接池的出现,使得与数据库连接时可以直接从池中获取,用完再还给连接池,连接池还能对资源进行管理,如资源的创建、回收、关闭等等。

连接

// 数据源dataSource是DruidDataSourceWrapper类,它是DruidDataSource的子类
// getConnection()是获取数据库连接的入口方法
DruidDataSource->getConnection():
    return getConnection(maxWait);
DruidDataSource->getConnection():
    // 实例化DruidDataSourceWrapper的时候,已经调用过一次了,这里直接返回
    init();
    // 使用责任链模式,最终调用getConnectionDirect()方法
    ......
DruidDataSource->getConnectionDirect():
    poolableConnection = getConnectionInternal(maxWaitMillis);
DruidDataSource->getConnectionInternal():
    // maxWait默认是-1,走下面的else方法
    if (maxWait > 0) {
        holder = pollLast(nanos);
    } else {
        holder = takeLast();
    }
DruidDataSource->takeLast():
    // poolingCount是连接池的数量
    while (poolingCount == 0) {
        emptySignal();  // --1
        ......
        notEmpty.await(); // --2
复制代码

第一次获取数据库连接,一定会走到–1处的方法,看这个方法的名称,我们就知道与多线程有关。

DruidDataSource->emptySignal():
    if (createScheduler == null) {
        empty.signal(); 
        return;
    }
复制代码

启动阶段实例化数据源的时候,创建连接的线程调用了empty.await(),这里调用empty.signal()将其唤醒。主线程返回,在–2处阻塞。

DruidDataSource->CreateConnectionThread->run():
    for (;;) {
        ......
        // 防止创建超过maxActive数量的连接
        // 这里的activeCount是资源分配出去但是还没有回收的连接数量
        if (activeCount + poolingCount >= maxActive) {
            empty.await(); // --3
            continue;
        }
        // 建立连接,返回的是PhysicalConnectionInfo
        // 连接过程与JDBC大同小异,不做解释
        connection = createPhysicalConnection();
        ......
        // 连接成功的情况下调用此方法
        boolean result = put(connection);
    }
DruidDataSource->put():
    // holder将物理连接又做了一次封装
    return put(holder);
DruidDataSource->put():
    if (poolingCount >= maxActive) {
        return false;
    }
    connections[poolingCount] = holder;
    // 连接池数量poolingCount加1
    incrementPoolingCount();
    ......
    notEmpty.signal(); // --4
复制代码

调用notEmpty.signal()唤醒–2处的阻塞,此时poolingCount已经不为0了,跳出循环。

DruidDataSource->takeLast():
    // 连接池数量poolingCount减1
    decrementPoolingCount();
    // 从connections数组中获取一个连接并返回
    DruidConnectionHolder last = connections[poolingCount];
    connections[poolingCount] = null;
    return last;
复制代码

从代码中可以知道,从连接池获取连接实际上是从connections数组中取值,然后将其下标置null,获取连接有非常多种情况,这里分析的只是最简单的一种。

回收

用完连接后,调用connection.close()关闭连接,这里的connection是DruidPooledConnection对象,它的关闭方法最终会调用dataSource.recycle()

DruidDataSource->recycle():
    ......
    result = putLast(holder, lastActiveTimeMillis);
DruidDataSource->putLast():
    // 这里的e即DruidConnectionHolder对象,是一个物理连接
    connections[poolingCount] = e;
    incrementPoolingCount();
    ......
    notEmpty.signal(); // --5
复制代码

对于线程池来说是回收连接,过程比较简单,就是把物理连接赋值给connections的下标,然后某种情况下,–2处有线程阻塞的话将其唤醒。

属性配置

yml配置:
    datasource:
      # 数据源属性
      ......
      druid:
        min-idle: 1
        max-active: 2
        max-wait: 10
        validation-query: SELECT 1
        time-between-eviction-runs-millis: 5
        min-evictable-idle-time-millis: 5
        max-evictable-idle-time-millis: 50
复制代码

第一次连接并回收后,再次获取连接,此时poolingCount已经不为0了,直接从connections数组中取值即可

最大连接数

在同一线程中获取连接是同步的,如果在不同线程同时获取,线程池会创建多个物理连接以供使用,但这一数目并不是无限大的,如果没有设置max-active,默认是8个。

如果最大连接数已经满了,创建连接的线程在–3处阻塞,主线程在–2处阻塞。直到有线程池回收其他线程的连接后,在–5处调用notEmpty.signal()唤醒–2处的阻塞,这里如果有多个线程的话,唤醒最先阻塞的那个。再一次循环,poolingCount已经不为0了,从connections数组中取值返回。

最大等待时间

如果设置了max-wait,调用的是holder = pollLast(nanos)方法。

DruidDataSource->pollLast():
    estimate = notEmpty.awaitNanos(estimate);
复制代码

本来调用notEmpty.await()无限等待,这里调用了notEmpty.awaitNanos(estimate)设置一定的等待时间,如果estimate返回小于0,说明已经超时了,后续会对estimate进行判断,小于0将会抛出相对应超时的异常。

关闭连接、检测时间与空闲时间

CreateConnectionThread负责创建数据库连接,相应的,DestroyConnectionThread负责关闭数据库连接。

DruidDataSource->DestroyConnectionThread->run():
    for (;;) {
        // 这里的timeBetweenEvictionRunsMillis即检测时间,默认是60s
        if (timeBetweenEvictionRunsMillis > 0) {
            Thread.sleep(timeBetweenEvictionRunsMillis);
        } else {
            Thread.sleep(1000); 
        }
        // 同步执行关闭数据库连接的线程方法
        destroyTask.run();
    }
DruidDataSource->DestroyTask->run():
    shrink(true, keepAlive);
DruidDataSource->shrink():
    // 连接池数量与最小连接数的差值
    final int checkCount = poolingCount - minIdle;
    // 当前时间毫秒数
    final long currentTimeMillis = System.currentTimeMillis();
    for (int i = 0; i < poolingCount; ++i) {
        // 从connections下标为0开始遍历
        DruidConnectionHolder connection = connections[i];
        ......
        // 空闲时间=当前时间减去上次活跃时间
        long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
        // 这里的minEvictableIdleTimeMillis即最小空闲时间
        if (idleMillis < minEvictableIdleTimeMillis) {
            // 如果空闲时间小于上次活跃时间,直接跳出循环了,这是因为随着connections下标的增大,活跃时间也是增大的
            break;
        }
        // 确保关闭后数量不会小于最小连接数,否则就不关闭了
        if (checkTime && i < checkCount) {
            evictConnections[evictCount++] = connection;
        // 如果空闲时间比设定的最大空闲时间还大的话,说什么也要关闭连接了
        } else if (idleMillis > maxEvictableIdleTimeMillis) {
            evictConnections[evictCount++] = connection;
        } else if (keepAlive) {
            keepAliveConnections[keepAliveCount++] = connection;
        }
        
        // 计算出要关闭连接的资源数量
        int removeCount = evictCount + keepAliveCount;
        if (removeCount > 0) {
            // connections、poolingCount重新设置值
            System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
            Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
            poolingCount -= removeCount;
        }
        
        // 要关闭连接的资源都在evictConnections数组中
        if (evictCount > 0) {
            for (int i = 0; i < evictCount; ++i) {
                // 遍历数组,一一关闭,过程与JDBC大同小异,不做解释
                DruidConnectionHolder item = evictConnections[i];
                Connection connection = item.getConnection();
                JdbcUtils.close(connection);
                destroyCount.incrementAndGet();
            }
            Arrays.fill(evictConnections, null);
        }
        ......
复制代码

总结

  • 从代码中可以得出结论,无论是获取连接,还是回收连接,还是关闭连接,核心都是DruidDataSource类下成员变量connections的操作,这是数据库连接池的精华所在
  • druid创建数据库连接使用的是异步线程的方式,仅利用多线程的Lock锁、ConditionObject就将调用线程与数据库连接线程、调用线程与调用线程间的关系展现得淋漓尽致,非常值得学习
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享