浅谈Tomcat数据源连接池

目录

为什么需要JDBC连接池

[Tomcat JDBC Pool 相关参数](#Tomcat JDBC Pool 相关参数)

[1. 基本配置](#1. 基本配置)

[2. 连接池大小控制](#2. 连接池大小控制)

[3. 连接验证与测试](#3. 连接验证与测试)

[4. 空闲连接回收](#4. 空闲连接回收)

[5. 连接泄漏与超时](#5. 连接泄漏与超时)

[Tomcat JDBC Pool 源码分析(tomcat 8.5.3)](#Tomcat JDBC Pool 源码分析(tomcat 8.5.3))

DataSourceFactory

DataSource

ConnectionPool

PoolCleaner


对于JAVA开发者来说,JDBC肯定都比较熟悉,它其实是Java提供了一套用于数据库操作的接口API,Java程序员只需要面向这套接口编程即可,不同的数据库厂商需要针对这套接口提供不同的实现。

为什么需要JDBC连接池

使用传统的jdbc做开发时,一方面频,繁的进行数据库连接操作将占用很多的系统资源,另一方面,每次拿到connection使用完都要主动去断开,那如果程序出现异常导致链接没有关闭,就会发生内存会泄露,因此可以发现使用jdbc开发时在连接管理上是存在风险的。另外,用传统的连接方法无法控制程序中创建的连接数量,如果连接过多也会导致内存泄漏。

那JDBC连接池的作用,就是可以避免频繁创建连接,帮我们做连接的生命周期管理,以及在请求较大时控制应用内连接的数量防止出现崩溃。

目前java生态里主流的JDBC连接池有HikariCP,Druid等,还有本文写的Tomcat JDBC Pool。

Tomcat JDBC Pool 相关参数

下面列举了一些基本参数和连接管理方面检测机制相关的参数

1. 基本配置

  • driverClassName
    数据库驱动类名(如 com.mysql.jdbc.Driver)。
  • url
    数据库连接 URL(如 jdbc:mysql://localhost:3306/mydb)。
  • username
    数据库用户名。
  • password
    数据库密码。

2. 连接池大小控制

  • initialSize
    连接池初始化时创建的连接数(默认 0)。
  • maxActive
    最大活跃连接数(默认 100)。
  • maxIdle
    最大空闲连接数(默认与 maxActive 相同)。
  • minIdle
    最小空闲连接数(默认 0)。
  • maxWait
    获取连接的最大等待时间(毫秒,默认 30000,超时抛异常)。

3. 连接验证与测试

  • testOnBorrow
    从池中获取连接时是否验证有效性(默认 false)。
  • testOnReturn
    归还连接时是否验证有效性(默认 false)。
  • testWhileIdle
    空闲时是否定期验证连接(默认 false)。
  • validationQuery
    用于验证连接的 SQL 查询(如 SELECT 1)。
  • validationQueryTimeout
    验证查询的超时时间(秒,默认 -1 表示无限制)。

4. 空闲连接回收

  • timeBetweenEvictionRunsMillis
    空闲连接检查线程的运行间隔(毫秒,默认 5000)。
  • minEvictableIdleTimeMillis
    连接被回收前的最小空闲时间(默认 60000)。
  • numTestsPerEvictionRun
    每次检查回收的空闲连接数(默认使用所有空闲连接)。

5. 连接泄漏与超时

  • removeAbandoned
    是否自动回收泄露的连接(默认 false)。
  • removeAbandonedTimeout
    连接被标记为泄露的时间阈值(秒,默认 60)。
  • logAbandoned
    是否记录泄露连接的堆栈信息(默认 false)。

Tomcat JDBC Pool 源码分析(tomcat 8.5.3)

下面简单分析下tomcat连接池是如何创建,又是如何实现连接检测的。

DataSourceFactory

DataSourceFactory是Tomcat JDBC Pool 中用来创建DataSource的工厂类,这个类在org.apache.tomcat.jdbc.pool包下。

下面是类中与连接池创建相关的部分代码片段:

java 复制代码
public DataSource createDataSource(Properties properties) throws Exception {
    return createDataSource(properties,null,false);
}

public DataSource createDataSource(Properties properties,Context context, boolean XA) throws Exception {
	PoolConfiguration poolProperties = DataSourceFactory.parsePoolProperties(properties);
	if (poolProperties.getDataSourceJNDI()!=null && poolProperties.getDataSource()==null) {
		performJNDILookup(context, poolProperties);
	}
	org.apache.tomcat.jdbc.pool.DataSource dataSource = XA? new org.apache.tomcat.jdbc.pool.XADataSource(poolProperties) : new org.apache.tomcat.jdbc.pool.DataSource(poolProperties);
	//创建连接池
	dataSource.createPool();
	return dataSource;
}

可以看到 DataSourceFactory.createDataSource方法返回一个DataSource实例,DataSource初始化后调用了 createPool 方法来初始化连接池相关的组件。

DataSource

DataSource是内置了数据源连接池 ConnectionPool 的对象,基于JDBC标准对外提供了一些方法。

Tomcat JDBC Pool 中支持两种DataSource类型,一种是普通DataSource,另一种是XADataSourced。

ConnectionPool

ConnectionPool 就是连接池最核心的资源对象,但是他不会直接对外暴露,而是被包装在上面说的DataSource中,开发过程中通常先获取到的是DataSource对象实例。

了解了DataSource 和 ConnectionPool 后,继续来看DataSourceFactory.createDataSource方法中的createPool方法逻辑,这个方法在DataSource或XADataSourced共同的父类DataSourceProxy,打开DataSourceProxy类可以进一步看具体的方法内容:

java 复制代码
public ConnectionPool createPool() throws SQLException {
    if (pool != null) {
        return pool;
    } else {
        return pCreatePool();
    }
}

/**
 * Sets up the connection pool, by creating a pooling driver.
 */
private synchronized ConnectionPool pCreatePool() throws SQLException {
    if (pool != null) {
        return pool;
    } else {
        pool = new ConnectionPool(poolProperties);
        return pool;
    }
}
public ConnectionPool(PoolConfiguration prop) throws SQLException {
	init(prop);
}

createPool 方法最终后调用到 init 方法:

java 复制代码
protected void init(PoolConfiguration properties) throws SQLException {
    poolProperties = properties;

    //make sure the pool is properly configured
    checkPoolConfiguration(properties);

    //make space for 10 extra in case we flow over a bit
    busy = new LinkedBlockingQueue<>();
    //busy = new FairBlockingQueue<PooledConnection>();
    //make space for 10 extra in case we flow over a bit
    if (properties.isFairQueue()) {
        idle = new FairBlockingQueue<>();
        //idle = new MultiLockFairBlockingQueue<PooledConnection>();
        //idle = new LinkedTransferQueue<PooledConnection>();
        //idle = new ArrayBlockingQueue<PooledConnection>(properties.getMaxActive(),false);
    } else {
        idle = new LinkedBlockingQueue<>();
    }

    initializePoolCleaner(properties);

    //create JMX MBean
    if (this.getPoolProperties().isJmxEnabled()) createMBean();

    //Parse and create an initial set of interceptors. Letting them know the pool has started.
    //These interceptors will not get any connection.
    PoolProperties.InterceptorDefinition[] proxies = getPoolProperties().getJdbcInterceptorsAsArray();
    for (int i=0; i<proxies.length; i++) {
        try {
            if (log.isDebugEnabled()) {
                log.debug("Creating interceptor instance of class:"+proxies[i].getInterceptorClass());
            }
            JdbcInterceptor interceptor = proxies[i].getInterceptorClass().getConstructor().newInstance();
            interceptor.setProperties(proxies[i].getProperties());
            interceptor.poolStarted(this);
        }catch (Exception x) {
            log.error("Unable to inform interceptor of pool start.",x);
            if (jmxPool!=null) jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.NOTIFY_INIT, getStackTrace(x));
            close(true);
            SQLException ex = new SQLException();
            ex.initCause(x);
            throw ex;
        }
    }

    //initialize the pool with its initial set of members
    PooledConnection[] initialPool = new PooledConnection[poolProperties.getInitialSize()];
    try {
        for (int i = 0; i < initialPool.length; i++) {
            initialPool[i] = this.borrowConnection(0, null, null); //don't wait, should be no contention
        } //for

    } catch (SQLException x) {
        log.error("Unable to create initial connections of pool.", x);
        if (!poolProperties.isIgnoreExceptionOnPreLoad()) {
            if (jmxPool!=null) jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.NOTIFY_INIT, getStackTrace(x));
            close(true);
            throw x;
        }
    } finally {
        //return the members as idle to the pool
        for (int i = 0; i < initialPool.length; i++) {
            if (initialPool[i] != null) {
                try {this.returnConnection(initialPool[i]);}catch(Exception x){/*NOOP*/}
            } //end if
        } //for
    } //catch

    closed = false;
}

这个方法中执行了 initializePoolCleaner 这个方法

java 复制代码
 public void initializePoolCleaner(PoolConfiguration properties) {
        //if the evictor thread is supposed to run, start it now
        if (properties.isPoolSweeperEnabled()) {
            poolCleaner = new PoolCleaner(this, properties.getTimeBetweenEvictionRunsMillis());
            poolCleaner.start();
        } //end if
    }

initializePoolCleaner 方法的逻辑是初始化一个PoolCleaner,然后启动它。

注意初始化这个构造器,传入了一个关键参数:

复制代码
betweenEvictionRunsMillis

这个参数用来指定对于空闲链接检测的任务多久执行一次,默认值是5000ms,默认值实在 org.apache.tomcat.jdbc.pool.PoolProperties 类中定义的固定值,如果外部设置了会覆盖。

PoolCleaner

PoolCleaner 是 ConnectionPool 中的一个内部静态类。这个类继承TimerTask ,实例化后其实是一个线程任务,Tomcat JDBC Pool 就是通过启动这个定时任务来实现连接池中的各种检测功能。

run方法中就是检测任务的具体逻辑,包括:

1.对最小连接数的维护,数量少于最小连接数则新建补充,数量多了则销毁多余的。

2.对空闲链接的检测,如果 testWhileIdle 参数设置为true,那在任务每次执行时会获取一定数量的链接,判断是否可用,如果不可用就销毁。

java 复制代码
 protected static class PoolCleaner extends TimerTask {
        protected WeakReference<ConnectionPool> pool;
        protected long sleepTime;

        PoolCleaner(ConnectionPool pool, long sleepTime) {
            this.pool = new WeakReference<>(pool);
            this.sleepTime = sleepTime;
            if (sleepTime <= 0) {
                log.warn("Database connection pool evicter thread interval is set to 0, defaulting to 30 seconds");
                this.sleepTime = 1000 * 30;
            } else if (sleepTime < 1000) {
                log.warn("Database connection pool evicter thread interval is set to lower than 1 second.");
            }
        }

        @Override
        public void run() {
            ConnectionPool pool = this.pool.get();
            if (pool == null) {
                stopRunning();
            } else if (!pool.isClosed()) {
                try {
                    if (pool.getPoolProperties().isRemoveAbandoned()
                            || pool.getPoolProperties().getSuspectTimeout() > 0)
                        pool.checkAbandoned();
                    if (pool.getPoolProperties().getMinIdle() < pool.idle
                            .size())
                        pool.checkIdle();
                    if (pool.getPoolProperties().isTestWhileIdle())
                        pool.testAllIdle();
                } catch (Exception x) {
                    log.error("", x);
                }
            }
        }

        public void start() {
            registerCleaner(this);
        }

        public void stopRunning() {
            unregisterCleaner(this);
        }
    }
相关推荐
Full Stack Developme6 小时前
java.nio 包详解
java·python·nio
零千叶6 小时前
【面试】Java JVM 调优面试手册
java·开发语言·jvm
代码充电宝7 小时前
LeetCode 算法题【简单】290. 单词规律
java·算法·leetcode·职场和发展·哈希表
li3714908907 小时前
nginx报400bad request 请求头过大异常处理
java·运维·nginx
摇滚侠7 小时前
Spring Boot 项目, idea 控制台日志设置彩色
java·spring boot·intellij-idea
曹牧7 小时前
oracle:NOT IN
数据库·oracle
Aevget7 小时前
「Java EE开发指南」用MyEclipse开发的EJB开发工具(二)
java·ide·java-ee·eclipse·myeclipse
黄昏晓x7 小时前
C++----多态
java·jvm·c++
Brookty8 小时前
【算法】前缀和
java·学习·算法·前缀和·动态规划
少许极端8 小时前
算法奇妙屋(七)-字符串操作
java·开发语言·数据结构·算法·字符串操作