聊聊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,欢迎建设性交流。

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

相关推荐
渡我白衣10 分钟前
【MySQL基础】(2):数据库基础概念
数据库·人工智能·深度学习·神经网络·mysql·机器学习·自然语言处理
怣5025 分钟前
MySQL WHERE子句完全指南:精准过滤数据的艺术
数据库·mysql
Fleshy数模10 小时前
CentOS7 安装配置 MySQL5.7 完整教程(本地虚拟机学习版)
linux·mysql·centos
az44yao11 小时前
mysql 创建事件 每天17点执行一个存储过程
mysql
秦老师Q12 小时前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
橘子1313 小时前
MySQL用户管理(十三)
数据库·mysql
Dxy123931021613 小时前
MySQL如何加唯一索引
android·数据库·mysql
我真的是大笨蛋13 小时前
深度解析InnoDB如何保障Buffer与磁盘数据一致性
java·数据库·sql·mysql·性能优化
怣5013 小时前
MySQL数据检索入门:从零开始学SELECT查询
数据库·mysql
人道领域14 小时前
javaWeb从入门到进阶(SpringBoot事务管理及AOP)
java·数据库·mysql