浅谈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);
        }
    }
相关推荐
mghio5 小时前
Dubbo 中的集群容错
java·微服务·dubbo
咖啡教室10 小时前
java日常开发笔记和开发问题记录
java
咖啡教室10 小时前
java练习项目记录笔记
java
鱼樱前端11 小时前
maven的基础安装和使用--mac/window版本
java·后端
RainbowSea11 小时前
6. RabbitMQ 死信队列的详细操作编写
java·消息队列·rabbitmq
RainbowSea12 小时前
5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
java·消息队列·rabbitmq
李少兄13 小时前
Unirest:优雅的Java HTTP客户端库
java·开发语言·http
此木|西贝13 小时前
【设计模式】原型模式
java·设计模式·原型模式
可乐加.糖14 小时前
一篇关于Netty相关的梳理总结
java·后端·网络协议·netty·信息与通信