2025 年 01 月 09 日 18:45

平时跟 RD 排查问题,经常会遇到数据库连接池相关的问题,比如获取不到连接、抛异常、长时间占用无法归还、探活、性能开销等。发现不少同学对连接池仍停留在表层的一知半解,很多配置也是相互复制的,基于此,本文整理了 Druid 连接池[1]的工作原理及推荐实践,希望对大家能有所帮助。

1 前言

一个正常的 SQL 执行流程为:

  1. Connection conn = DriverManager.getConnection();
  2. Statement stmt = conn.createStatement();
  3. ResultSet rs = stmt.executeQuery(sql);
  4. 操作 rs 读取数据;
  5. 关闭 rs、stmt、conn。

示例代码如下:

public static void main(String[] args){
    try{
        Class.forName("com.mysql.cj.jdbc.Driver"); //调用Class.forName()方法加载驱动程序
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/RUNOOB?useSSL=false", "root","*****"); //创建连接
        Statement stmt = conn.createStatement(); //创建Statement对象
 
        String sql = "select * from stu";    //要执行的SQL
        ResultSet rs = stmt.executeQuery(sql);//创建数据对象
        System.out.println("编号"+"\t"+"姓名"+"\t"+"年龄");
        while (rs.next()){
            System.out.print(rs.getInt(1) + "\t");
            System.out.print(rs.getString(2) + "\t");
            System.out.print(rs.getInt(3) + "\t");
            System.out.println();
        }
 
        rs.close();
        stmt.close();
        conn.close();
    }catch(Exception e){
    }
}

但如果每次请求都要 DriverManager.getConnection() 新建连接和关闭连接,操作较重,费时费力,也影响了业务请求。 其实 Connection 对象是可以重复利用的(只要保证 Connection 借出后归单一线程所有,其所创建的 StatementResultSet 在回收前都能关闭即可),这样 Connection 被重新获取后就可以跟新建的一样,从而避免底层 Socket 连接的频繁创建与关闭。数据库连接池便应运而生。

DataSource 定义了一个 getConnection() 的接口,具体实现可以是直接新建,也可以是从连接池里获取。用户使用完 Connection 后,要手动 close(),而这个 close() 也是个逻辑语义。对于 MySQL JDBC 的 ConnectionImpl 来说,close() 是物理关闭;而对于 DruidDruidPooledConnection 来说,close() 就是归还。

2 Druid 简介

当我们有了连接池,应用程序启动时就预先建立多个数据库连接对象,然后将连接对象保存到连接池中。当客户请求到来时,从池中取出一个连接对象为客户服务。当请求完成时,客户程序调用关闭方法,将连接对象放回池中[2]。跟现实生活中的共享单车是不是很像~Image

相比之下,连接池的优点显而易见:

  • 资源复用:因为数据库连接可以复用,避免了频繁创建和释放连接引起的大量性能开销,同时也增加了系统运行环境的平稳性;
  • 提高性能:当业务请求时,因为数据库连接在初始化时已经被创建,可以立即使用,而不需要等待连接的建立,减少了响应时间;
  • 优化资源分配:对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接池的配置,实现某一应用最大可用数据库连接数的限制,避免某一应用独占所有的数据库资源;
  • 连接管理:数据库连接池实现中,可根据预先的占用超时设定,强制回收被占用连接,从而避免了常规数据库连接操作中可能出现的资源泄露。

Druid 连接池内部的数据结构如下(以 minIdle=5,maxActive=10 为例):Image

  • 连接池采用 LRU 栈式置换策略(最近归还的会被最先借出);
  • poolingCount:池中可用的空闲连接;
  • activeCount:已经借出去的连接数。两者之和为所有连接数。此时池里有 7 个空闲连接,poolingCount=7;
  • empty条件变量:连接池有空闲连接时会等待。获取连接时如果无可用空闲连接会触发 signal;
  • notEmpty条件变量:获取连接时如果为空会等待,归还或创建连接时会触发 signal。

3 初始化流程 init()

Image触发时机:首次 getConnection() 时或直接调用 init()

核心流程

  1. 创建 initialSize 个连接;
  2. 启动 LogStatsThreadCreateConnectionThreadDestroyConnectionThread 三个线程。

3.1 LogStatsThread 线程 (Druid-ConnectionPool-Log-)

如果 timeBetweenLogStatsMillis > 0,则每隔 timeBetweenLogStatsMillis 打印一次 stat 日志。

3.2 CreateConnectionThread 线程 (Druid-ConnectionPool-Create-)

Image

后台负责创建连接的线程。监听 empty 条件信号,收到信号后,如果满足条件则创建一个新连接;如果不满足,则忽略此次信号。

3.2.1 创建新连接的条件

// 防止创建超过maxActive数量的连接if (activeCount + poolingCount >= maxActive) { //如果不满足条件,则忽略此次信号并继续await()    empty.await();    continue; //再次收到empty条件信号后,重新回到for (;;)处}
3.2.2 createPhysicalConnection() 创建连接流程
//创建一条连接,并初始化public PhysicalConnectionInfo createPhysicalConnection() throws SQLException {    //此处省略一万行    try {        Connection conn = createPhysicalConnection(url, physicalConnectProperties); //创建一条物理连接        connectedNanos = System.nanoTime();         if (conn == null) {            throw new SQLException("connect error, url " + url + ", driverClass " + this.driverClass);        }         initPhysicalConnection(conn, variables, globalVariables); //初始化连接        initedNanos = System.nanoTime();         validateConnection(conn); //检测一下        validatedNanos = System.nanoTime();         setFailContinuous(false);        setCreateError(null);    } //此处省略一万行     return new PhysicalConnectionInfo(conn, connectStartNanos, connectedNanos, initedNanos, validatedNanos, variables, globalVariables);}

接下来再来看 createPhysicalConnection(url, info) 函数,它就是负责创建一条 java.sql.Connection 连接,如下:

public Connection createPhysicalConnection(String url, Properties info) throws SQLException {    Connection conn;    if (getProxyFilters().size() == 0) {        conn = getDriver().connect(url, info); //创建一条连接    } else {        conn = new FilterChainImpl(this).connection_connect(info);    }     return conn;}

3.2.3 put(holder, createTaskId) 将连接放入连接池

将连接放入连接池尾部,并发送 notEmpty 条件信号。

//将连接放入连接池尾部,并发送notEmpty条件信号private boolean put(DruidConnectionHolder holder, long createTaskId) {    lock.lock();    try {        if (poolingCount >= maxActive) {            return false;        }                 connections[poolingCount] = holder; //放入连接池尾部        incrementPoolingCount(); //可用连接数poolingCount+1         notEmpty.signal(); //发送notEmpty条件信号        notEmptySignalCount++;    } finally {        lock.unlock();    }    return true;}

3.3 DestroyConnectionThread 线程 (Druid-ConnectionPool-Destroy-)

Image

定时扫描连接池进行探测和销毁。DestroyConnectionThread 每隔 timeBetweenEvictionRunsMillis 扫描一次连接池中的空闲连接:

  • 如果物理存活时间超过 phyTimeoutMillis,则直接销毁;
  • 如果 ( keepAlive && 空闲时间 >= keepAliveBetweenTimeMillis),则进行探测;
  • 如果空闲时间 >= minEvictableIdleTimeMillis,则销毁(但要保证留下 minIdle 个);而如果空闲时间超过 maxEvictableIdleTimeMillis 则必须进行销毁;
  • 如果 ( removeAbandoned == true && 连接借出时间 > removeAbandonedTimeoutMillis),则强制关闭其 statement 并归还。
public void run() {    shrink(true, keepAlive); //探测与销毁     if (isRemoveAbandoned()) { //检查连接泄漏        removeAbandoned();    }}

3.3.1 shrink(checkTime, keepAlive) 流程

public void shrink(boolean checkTime, boolean keepAlive) {    try {        final int checkCount = poolingCount - minIdle;        final long currentTimeMillis = System.currentTimeMillis();        for (int i = 0; i < poolingCount; ++i) {            DruidConnectionHolder connection = connections[i];             if (phyTimeoutMillis > 0) { //如果连接的物理存活时间超过限值,将被销毁。                long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;                if (phyConnectTimeMillis > phyTimeoutMillis) {                    evictConnections[evictCount++] = connection;                    continue;                }            }             long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis; //空闲时间              //如果空闲时间过短,直接跳过            if (idleMillis < minEvictableIdleTimeMillis                    && idleMillis < keepAliveBetweenTimeMillis            ) {                break;            }             if (idleMillis >= minEvictableIdleTimeMillis) { //如果连接空闲时间超过minEvictableIdleTimeMillis                if (checkTime && i < checkCount) { //留尾部的minIdle个连接先不销毁                    evictConnections[evictCount++] = connection;                    continue;                } else if (idleMillis > maxEvictableIdleTimeMillis) { //尾部的minIdle个连接如果连接空闲时间>maxEvictableIdleTimeMillis,也会被销毁                    evictConnections[evictCount++] = connection;                    continue;                }            }             if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) { //如果连接空闲时间未超过minEvictableIdleTimeMillis,但超过了keepAliveBetweenTimeMillis就要进行探活                keepAliveConnections[keepAliveCount++] = connection;            }        }     } finally {        lock.unlock();    }     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);    }     if (keepAliveCount > 0) { //如果有需要探测的,则进行探测。探活的,则放回可用池;探不活的,直接关闭,并通知创建。        // keep order        for (int i = keepAliveCount - 1; i >= 0; --i) {            //此处省略一万行        }    }     if (needFill) {        lock.lock();        try {            int fillCount = minIdle - (activeCount + poolingCount + createTaskCount); //需要补充创建的连接个数            for (int i = 0; i < fillCount; ++i) {                emptySignal(); //给CreateConnectionThread发送empty条件信号来创建连接            }        } finally {            lock.unlock();        }    }}

3.3.2 removeAbandoned() 连接泄漏检测

连接泄露检查,打开 removeAbandoned 功能,连接从连接池借出后,长时间不归还,将触发强制关闭其 staement 并归还。回收周期随 timeBetweenEvictionRunsMillis 进行,如果连接借出时间起超过 removeAbandonedTimeout,则强制关闭其 staement 并归还。对性能会有一些影响,建议怀疑存在泄漏之后再打开,不建议在生产环境中使用。

//强制归还泄漏(借出后长时间未归还)的连接public int removeAbandoned() {    long currrentNanos = System.nanoTime();    List<DruidPooledConnection> abandonedList = new ArrayList<DruidPooledConnection>();     activeConnectionLock.lock();    try {        Iterator<DruidPooledConnection> iter = activeConnections.keySet().iterator();        for (; iter.hasNext();) {            DruidPooledConnection pooledConnection = iter.next();             if (pooledConnection.isRunning()) {                continue;            }             long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000); //连接借出时间             if (timeMillis >= removeAbandonedTimeoutMillis) {                iter.remove();                abandonedList.add(pooledConnection);            }        }    } finally {        activeConnectionLock.unlock();    }     if (abandonedList.size() > 0) {        for (DruidPooledConnection pooledConnection : abandonedList) {            JdbcUtils.close(pooledConnection); //强制归还        }    }     return removeCount;}

4 获取连接流程 getConnection()

Image

连接池最核心的功能就是连接的获取与回收。我们直接看 getConnectionDirect(),它负责获取一个可用的连接。

public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {    for (;;) { //如果某次获取到的连接无效,一般会丢弃该连接并重新获取。        DruidPooledConnection poolableConnection;        try {            poolableConnection = getConnectionInternal(maxWaitMillis); //获取连接        } catch (GetConnectionTimeoutException ex) {            //......        }         if (testOnBorrow) { //如果testOnBorrow=true,则进行探测。            boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);            if (!validate) {                discardConnection(poolableConnection.holder); //探测失败,则丢弃此连接并重新获取。                continue;            }        } else {            if (testWhileIdle) { //如果testWhileIdle=true且空闲时间>timeBetweenEvictionRunsMillis,则进行探测。                final DruidConnectionHolder holder = poolableConnection.holder;                long idleMillis                    = currentTimeMillis - lastActiveTimeMillis;                 long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;                 if (idleMillis >= timeBetweenEvictionRunsMillis                        || idleMillis < 0 // unexcepted branch                        ) {                    boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);                    if (!validate) {                        discardConnection(poolableConnection.holder); //如果探测失败,则丢弃连接并重新获取。                         continue;                    }                }            }        }         //是否打开连接泄露检查。DestroyConnectionThread如果检测到连接借出时间超过removeAbandonedTimeout,则强制归还连接到连接池中。        //对性能会有一些影响,建议怀疑存在泄漏之后再打开,不建议在生产环境中使用。        if (removeAbandoned) {            StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); //保存发起方的线程栈            poolableConnection.connectStackTrace = stackTrace;            poolableConnection.setConnectedTimeNano(); //重置借出时间             activeConnectionLock.lock();            try {                activeConnections.put(poolableConnection, PRESENT); //放进activeConnections            } finally {                activeConnectionLock.unlock();            }        }         return poolableConnection;    }}

4.1 getConnectionInternal() 获取一个连接

getConnectionInternal() 负责从连接池获取一个连接(但该连接并不保证可用),并包装成 DruidPooledConnection

private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {    final long nanos = TimeUnit.MILLISECONDS.toNanos(maxWait);    final int maxWaitThreadCount = this.maxWaitThreadCount;    DruidConnectionHolder holder;     for (boolean createDirect = false;;) {        try {            //......             if (maxWaitThreadCount > 0                    && notEmptyWaitThreadCount >= maxWaitThreadCount) { //如果等待获取连接的线程数超过maxWaitThreadCount,则抛出异常                throw new SQLException("maxWaitThreadCount " + maxWaitThreadCount + ", current wait Thread count "                        + lock.getQueueLength());            }              //从可用连接池里获取连接,如果没有则阻塞等待。            if (maxWait > 0) {                holder = pollLast(nanos);            } else {                holder = takeLast();            }             //......        } //......         break;    }     //......    holder.incrementUseCount(); //连接的使用次数+1     DruidPooledConnection poolalbeConnection = new DruidPooledConnection(holder); ////包装成一个DruidPooledConnection对象    return poolalbeConnection;}

4.2 takeLast() 阻塞等待尾部连接

//阻塞等待尾部连接DruidConnectionHolder takeLast() throws InterruptedException, SQLException {    try {        while (poolingCount == 0) {  //如果没有可用连接,就发送个empty条件信号给CreateConnectionThread,并等待notEmpty条件信号            emptySignal(); // send signal to CreateThread create connection             notEmptyWaitThreadCount++;            try {                notEmpty.await(); // signal by recycle or creator            } finally {                notEmptyWaitThreadCount--;            }             //......        }    } catch (InterruptedException ie) {        //......    }      //移除尾部连接    decrementPoolingCount();    DruidConnectionHolder last = connections[poolingCount];    connections[poolingCount] = null;     return last;}

至此,向请求线程返回一个可用的连接 DruidPooledConnection。

5 执行&异常处理

Image

如下为 Mybatis 执行 SQL 的核心函数(SqlSessionTemplate$SqlSessionInterceptor.invoke()):

private class SqlSessionInterceptor implements InvocationHandler {     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        //......        try {            Object result = method.invoke(sqlSession, args); //下调DruidPooledPreparedStatement.execute()执行SQL            unwrapped = result;        } catch (Throwable var11) {            unwrapped = ExceptionUtil.unwrapThrowable(var11);            //......            throw (Throwable)unwrapped; //如果发生异常,继续上抛        } finally {            if (sqlSession != null) {                SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); //关闭连接,对应Druid是归还。            }         }         return unwrapped;    }}
  • method.invoke() 会通过 Druid 获取连接,并调用 DruidPooledPreparedStatement.execute()
  • 执行结束后,close 连接,此时会触发 Druid 的连接归还;
  • 执行中如果发生异常,继续向上抛。

5.1 DruidPooledPreparedStatement.execute()

此时,进入 Druid 连接中 statement 的 execute(),如果发生异常进入 checkException()

public boolean execute() throws SQLException {    conn.beforeExecute();    try {        return stmt.execute(); //执行SQL    } catch (Throwable t) {        errorCheck(t);         throw checkException(t); //如果发生异常,调用DruidDataSource.handleConnectionException()对连接进行处理,并继续上抛    } finally {        conn.afterExecute();    }}

5.1.1 DruidDataSource.handleConnectionException()

public void handleConnectionException(DruidPooledConnection pooledConnection, Throwable t, String sql) throws SQLException {    //......    if (t instanceof SQLException) {        SQLException sqlEx = (SQLException) t;         // exceptionSorter.isExceptionFatal        if (exceptionSorter != null && exceptionSorter.isExceptionFatal(sqlEx)) { //判断是否是致命异常            handleFatalError(pooledConnection, sqlEx, sql); //如果是致命异常,则销毁        }         throw sqlEx;    } else {        throw new SQLException("Error", t);    }}
//对致命异常进行处理protected final void handleFatalError(DruidPooledConnection conn, SQLException error, String sql ) throws SQLException {    final DruidConnectionHolder holder = conn.holder;    //......     if (requireDiscard) {        if (holder != null && holder.statementTrace != null) {            try {                for (Statement stmt : holder.statementTrace) { //关闭连接内的所有的statement                    JdbcUtils.close(stmt);                }            } finally {            }        }         emptySignalCalled = this.discardConnection(holder); //销毁    }    LOG.error("{conn-" + (holder != null ? holder.getConnectionId() : "null") + "} discard", error);     //......}

6 回收连接流程 recycle()

Image

使用 DruidPooledConnection 连接进行 SQL 操作后,会调用 DruidPooledConnection.recycle() 进行回收操作。

public void recycle() throws SQLException {    DruidConnectionHolder holder = this.holder;    DruidAbstractDataSource dataSource = holder.getDataSource();    dataSource.recycle(this);     //......}

6.1 DruidDataSource.recycle()

/** * 回收连接 * 1)重置连接;2)归还到连接池。 */protected void recycle(DruidPooledConnection pooledConnection) throws SQLException {    final DruidConnectionHolder holder = pooledConnection.holder;    final Connection physicalConnection = holder.conn;     try {         //归还前重置连接         holder.reset();         if (phyMaxUseCount > 0 && holder.useCount >= phyMaxUseCount) { //限制连接的最大使用次数。超过此值,会被直接关闭。            discardConnection(holder);            return;        }         if (testOnReturn) { //如果testOnReturn=true,归还前也检测下            //......        }         final long currentTimeMillis = System.currentTimeMillis();        if (phyTimeoutMillis > 0) { //检测物理存活时间            long phyConnectTimeMillis = currentTimeMillis - holder.connectTimeMillis;            if (phyConnectTimeMillis > phyTimeoutMillis) {                discardConnection(holder);                return;            }        }         lock.lock();        try {            result = putLast(holder, currentTimeMillis); //归还到连接池            recycleCount++;        } finally {            lock.unlock();        }         if (!result) {            JdbcUtils.close(holder.conn);        }    } catch (Throwable e) {        //......    }}

6.2 将连接放入可用连接池尾部,并发送 notEmpty 条件信号

//将连接放入可用连接池尾部,并发送notEmpty条件信号boolean putLast(DruidConnectionHolder e, long lastActiveTimeMillis) {    if (poolingCount >= maxActive || e.discard) {        return false;    }     e.lastActiveTimeMillis = lastActiveTimeMillis; //重置最近活跃时间    connections[poolingCount] = e; //归还到尾部    incrementPoolingCount();     notEmpty.signal();    notEmptySignalCount++;     return true;}

至此,连接已成功回收。

7 总结

7.1 整个连接池的核心操作

  • init() 初始化:1)创建 initialSize 个连接;2)启动 LogStatsThreadCreateConnectionThreadDestroyConnectionThread 三个线程;
  • getConnection() 获取连接:获取后会从连接池移除,Connection 只能归当前线程所用;
  • recycle() 回收连接:放回连接池后,其他线程就可以再次获取该连接重复利用了。

7.2 条件信号协作

  • 获取连接时:如果连接池里没有连接,会发出 empty 条件信号,并等待 notEmpty 条件信号。CreateConnectionThread 收到 empty 信号后,如果满足条件则创建一个新连接,也会发出 notEmpty 条件信号;如果不满足,则忽略此次 empty 信号。
  • 回收连接时:连接放回连接池后,会发出 notEmpty 条件信号。如果有请求在阻塞等待获取连接,此时会被唤醒,从而获取连接。

7.3 几处检测和销毁逻辑

  • 借出时:
  • 如果 testOnBorrow,则探测;
  • 如果 (testWhileIdle = true && 空闲时间 > timeBetweenEvictionRunsMillis) 则进行探测;
  • 执行时:
  • 如果抛出异常且 exceptionSorter 判断是致命异常,就调用 handleFatalError() 进行销毁;
  • 归还时:
  • 如果连接使用次数超过 phyMaxUseCount,则销毁;
  • 如果 testOnReturn=true,则探测;
  • 如果连接建立时间走过 phyTimeoutMillis,则销毁;
  • DestroyConnectionThread 每隔 timeBetweenEvictionRunsMillis 扫描一次连接池中的空闲连接
  • 如果物理存活时间超过 phyTimeoutMillis,则销毁;
  • 如果 ( keepAlive && 空闲时间 >= keepAliveBetweenTimeMillis),则进行探测;
  • 如果空闲时间 >= minEvictableIdleTimeMillis,则销毁(但要保证留下 minIdle 个);而如果空闲时间超过 maxEvictableIdleTimeMillis 则必须销毁;
  • 如果 ( removeAbandoned == true && 连接借出时间 > removeAbandonedTimeoutMillis),则被强制关闭其 statement 并归还。

8 常用&推荐配置

8.1 常用配置

官方完整介绍见:https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8

ImageImageImage

8.2 推荐配置

<bean id="userdataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">    <!-- 基本属性 -->    <property name="name" value="userdataSource" />    <property name="url" value="${userdataSource_url}" />    <property name="driverClassName" value="com.zhuanzhuan.arch.kms.jdbc.mysql.Driver" />           <!-- 配置初始化大小、最小、最大 -->    <property name="initialSize" value="1" />    <property name="minIdle" value="3" />    <property name="maxActive" value="20" />      <!-- 获取连接的等待超时时间,单位是毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 -->    <property name="maxWait" value="60000" />     <!-- 探测的SQL -->    <property name="validationQuery" value="SELECT 1" />    <!-- 获取连接时,执行validationQuery检测连接是否有效 -->    <property name="testOnBorrow" value="false" />    <!-- 获取连接时,如果空闲时间超过timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效 -->    <property name="testWhileIdle" value="true" />    <!-- 归还连接时,执行validationQuery检测连接是否有效 -->    <property name="testOnReturn" value="false" />      <!-- 定期检测的时间间隔,单位是毫秒 -->    <property name="timeBetweenEvictionRunsMillis" value="60000" />    <!-- 定期检测时,如果空闲时间超过此值则进行销毁(但要保证留下minIdle个连接),单位是毫秒 -->    <property name="minEvictableIdleTimeMillis" value="300000" /></bean>

此配置说明:

  • 线程池刚启动时会创建 1 个(initialSize)连接,随着程序的运行,池不忙时也会保持最少 3 个(minIdle)空闲连接,但总连接数(包括空闲和在用)不超过 20 个(maxActive);
  • 获取连接时
  • 如果连接池没有空闲连接,最长等待 60 秒(maxWait);
  • 【主动】如果获取到的连接空闲时间大于 60 秒(timeBetweenEvictionRunsMillis),则执行 validationQuery 检测连接是否还有效(有效则使用,无效则销毁);
  • 执行 SQL 时
  • 【被动】如果发生致命异常(默认 exceptionSorter=MySqlExceptionSorter,如 CommunicationsException),则销毁该连接;
  • DestroyConnectionThread 每隔 60 秒(timeBetweenEvictionRunsMillis)扫描一次连接池中的空闲连接:
  • 【主动】如果空闲时间超过 300 秒(minEvictableIdleTimeMillis),则销毁(但要保证留下 minIdle=3 个);而如果空闲时间超过 7 小时(maxEvictableIdleTimeMillis 默认为 7 小时)则必须销毁。

9 监控

Druid 通过 SPI 开放了很多扩展点,我们架构部基于此封装了监控组件,直接上报到 Prometheus。效果如下:Image

Image


关于作者

杜云杰,高级架构师,转转架构部负责人,转转技术委员会执行主席,腾讯云 MVP。负责服务治理、MQ、云平台、APM、IM、分布式调用链路追踪、监控系统、配置中心、分布式任务调度平台、分布式 ID 生成器、分布式锁等基础组件。微信号:waterystone,欢迎建设性交流。

道阻且长,拥抱变化;而困而知,且勉且行。

参考资料

[1]

Druid: https://github.com/alibaba/druid

[2]

聊聊数据库连接池 Druid : https://www.cnblogs.com/makemylife/p/17889584.html

想了解更多转转公司的业务实践,欢迎点击关注下方公众号: