聊聊Druid连接池的内部原理及推荐配置

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

1 前言

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

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

示例代码如下:

java 复制代码
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简介

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

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

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

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

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

3 初始化流程init()

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

核心流程

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

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

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

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

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

3.2.1 创建新连接的条件

java 复制代码
// 防止创建超过maxActive数量的连接
if (activeCount + poolingCount >= maxActive) { //如果不满足条件,则忽略此次信号并继续await()
    empty.await();
    continue; //再次收到empty条件信号后,重新回到for (;;)处
}
3.2.2 createPhysicalConnection()创建连接流程
java 复制代码
//创建一条连接,并初始化
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连接,如下:

java 复制代码
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条件信号。

java 复制代码
//将连接放入连接池尾部,并发送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-)

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

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

3.3.1 shrink(checkTime, keepAlive)流程

java 复制代码
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并归还。对性能会有一些影响,建议怀疑存在泄漏之后再打开,不建议在生产环境中使用。

java 复制代码
//强制归还泄漏(借出后长时间未归还)的连接
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()

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

java 复制代码
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

java 复制代码
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()阻塞等待尾部连接

java 复制代码
//阻塞等待尾部连接
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 执行&异常处理

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

java 复制代码
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()

java 复制代码
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()

java 复制代码
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);
    }
}
java 复制代码
//对致命异常进行处理
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()

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

java 复制代码
public void recycle() throws SQLException {
    DruidConnectionHolder holder = this.holder;
    DruidAbstractDataSource dataSource = holder.getDataSource();
    dataSource.recycle(this);
 
    //......
}

6.1 DruidDataSource.recycle()

java 复制代码
/**
 * 回收连接
 * 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条件信号

java 复制代码
//将连接放入可用连接池尾部,并发送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 常用配置

官方完整介绍见:github.com/alibaba/dru...

8.2 推荐配置

xml 复制代码
<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。效果如下:


关于作者

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

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

相关推荐
acegi135798 小时前
MySQL - 子查询和相关子查询详解
数据库·mysql
weixin_438335409 小时前
【更新中】Mysql问题分析
数据库·mysql
CodeChampion12 小时前
69.基于SpringBoot + Vue实现的前后端分离-家乡特色推荐系统(项目 + 论文PPT)
java·vue.js·spring boot·mysql·elementui·node.js·mybatis
大雄野比12 小时前
UOS系统mysql服务安装
android·mysql·adb
m0_7482398313 小时前
【MySQL】【已解决】Windows安装MySQL8.0时的报错解决方案
数据库·windows·mysql
兮动人14 小时前
【mysql】流程控制
数据库·mysql
爱吃土豆的程序员14 小时前
flowable mysql 表名大小写问题
linux·windows·mysql·flowable
redemption_214 小时前
超市管理系统(javaweb+mysql+redis)
java·mysql
程序员谷美14 小时前
Mysql 性能优化:索引条件下推(ICP)
数据库·mysql·索引