在启动完成之后,连接池紧接着会进行一个initialize的操作,这一节主要介绍这个initialize所完成的操作。具体涉及到如下几个参数:
< idle-timeout-minutes >:一个连接的最大空闲超时时间,即在连接被关闭之前,连接可以空闲的最长时间,超过这个时间连接就会被关闭。这个参数设置为0则禁用这个功能,文档上说默认值为15分钟,我看的jboss4.2.3的源代码中默认值为30分钟。
< background-validation >:在jboss4.0.5版本中,增加了一个后台连接验证的功能,用于减少RDBMS系统的负载。当使用这个功能的时候,jboss将使用一个独立的线程(ConnectionValidator)去验证当前池中的连接。这个参数必需在设置为true时才能生效,默认设置为false。
< background-validation-minutes >:ConnectionValidator线程被唤醒的定时间隔。默认设置为10分钟。注意:为谨慎起见,设置这个值稍大些,或者小于idle-timeout-minutes。
< background-validation-millis >:从jboss5.0版本开始,代替了background-validation-minutes参数。参数background-validation-minutes不再被支持。同时background-validation这个参数也被废弃。只要配置了background-validation-millis > 0,则启用后台验证。更多内容查看:https://jira.jboss.org/browse/JBAS-4088。
连接池的初始化方法如下:
/** * Initialize the pool */ protected void initialize() { /*将一个连接池对象注册到IdleRemover线程中,表示这个连接池使用IdleRemover来进行管理。 IdleRemover线程是空闲连接清理线程,被唤醒的周期是poolParams.idleTimeout/2。 即配置的idle-timeout-minutes参数/2。 默认idle-timeout-minutes为30分钟,所以清理线程是15分钟运行一次。 */ if (poolParams.idleTimeout != 0) IdleRemover.registerPool(this, poolParams.idleTimeout); /*将一个连接池对象注册到ConnectionValidator线程中,表示这个连接池使用 ConnectionValidator来进行管理。IdleRemover线程是一个验证连接池的线程, 被唤醒的周期是poolParams.backgroundValidation/2。 即配置的background-validation-millis 参数/2。 默认background-validation-millis为10分钟,所以清理线程是5分钟运行一次。 */ if (poolParams.backgroundValidation) { log.debug("Registering for background validation at interval " + poolParams.backgroundInterval); ConnectionValidator.registerPool(this, poolParams.backgroundInterval); } }
IdleRemover和 ConnectionValidator两个线程的处理方式是一致的。定时调度的处理方式也是完全一致的。
区别在于,定时任务的启动IdleRemover是15分钟清理一次空闲的连接,而ConnectionValidator是5分钟进行一次连接验证,后面会给出主要的两个方法。
因为两者调度方式一致,这里以IdleRemover类为例,来看看这两个线程的具体调度算法:
....... private static final IdleRemover remover = new IdleRemover(); //注册一个连接池清理对象,即将一个连接池清理对象加入到pools中,并传入一个清理的时间参数 public static void registerPool(InternalManagedConnectionPool mcp, long interval) { remover.internalRegisterPool(mcp, interval); } //反注册一个连接池清理对象,并且设置interval = Long.MAX_VALUE public static void unregisterPool(InternalManagedConnectionPool mcp) { remover.internalUnregisterPool(mcp); } .......
IdleRemover线程的启动:
private IdleRemover () { AccessController.doPrivileged(new PrivilegedAction() { public Object run() { Runnable runnable = new IdleRemoverRunnable(); Thread removerThread = new Thread(runnable, "IdleRemover"); removerThread.setDaemon(true); removerThread.start(); return null; } }); } 线程的run方法如下: /** * Idle Remover background thread */ private class IdleRemoverRunnable implements Runnable { public void run() { //更改上下文ClassLoader. setupContextClassLoader(); //这是一个线程安全的操作。 synchronized (pools) { while (true) { try { /* * interval在这个类中的初始值是一个无限大的值:interval = Long.MAX_VALUE。 即线程开始运行时,即一直wait在这里。 当执行internalRegisterPool方法,即第一次被唤醒时, interval即被设置为传入的interval/2, 如果idle-time-out设置为30分钟,这个interval的值即被设为15分钟。 即每次wait15分钟,表示15分钟线程被唤醒清理一次idle的连接。 */ pools.wait(interval); log.debug("run: IdleRemover notifying pools, interval: " + interval); /* * 这里的pools和第二节中讲到的pools一致,是一个临时队列,区别是这个临时队列 不会进行清理,只有调用internalUnregisterPool方法才会从队列中清理出去。 *遍历pool中的连接池对象,对所有的连接池对象,执行removeTimeout,后面会详细介绍。 */ for (Iterator i = pools.iterator(); i.hasNext(); ) ((InternalManagedConnectionPool)i.next()).removeTimedOut(); //设置next值,这个next表示pools下一次需要清理的时间点。 next = System.currentTimeMillis() + interval; if (next < 0) next = Long.MAX_VALUE; } catch (InterruptedException ie) { log.info("run: IdleRemover has been interrupted, returning"); return; } catch (RuntimeException e) { log.warn("run: IdleRemover ignored unexpected runtime exception", e); } catch (Error e) { log.warn("run: IdleRemover ignored unexpected error", e); } } } } }
注册连接池的方法如下:
private void internalRegisterPool(InternalManagedConnectionPool mcp, long interval) { log.debug("internalRegisterPool: registering pool with interval " + interval + " old interval: " + this.interval); synchronized (pools) { //往pool里面增加需要清理的连接池对象,即注册连接池对象。 pools.add(mcp); /*这里有两个条件: * 1.interval>1,即idle-time-out设置至少为2分钟,pools才会执行nofity()。 * 2.interval/2 < LONG.MAX_VALUE,防止idle-time-out设置过大。 */ if (interval > 1 && interval/2 < this.interval) { //设置为interval为interval/2,即清理线程被唤醒的间隔时间。 this.interval = interval/2; //本连接池下一次可能清理的时间。 long maybeNext = System.currentTimeMillis() + this.interval; /*如果next即wait线程的下一次唤醒的时间>maybeNext的时间。 即立即唤醒清理线程,进行第一次清理。 这样的目的是为了让这个连接池的注册能够立即生效,而不被旧的interval影响。 */ if (next > maybeNext && maybeNext > 0) { next = maybeNext; log.debug("internalRegisterPool: about to notify thread: old next: " + next + ", new next: " + maybeNext); pools.notify(); } } } }
反注册方法,即从pools队列中去除注册的连接池,表示这个连接池不需要进行清理。
private void internalUnregisterPool(InternalManagedConnectionPool mcp) { synchronized (pools) { pools.remove(mcp); if (pools.size() == 0) { log.debug("internalUnregisterPool: setting interval to Long.MAX_VALUE"); interval = Long.MAX_VALUE; } } }
以上是IdleRemover.registerPool和 ConnectionValidator.registerPool.registerPool两个线程的调度方式,下面来看一下这两个方法执行的具体操作。
IdleRemover对空闲连接的清理:
public void removeTimedOut() { //合建一个清理队列 ArrayList destroy = null; long timeout = System.currentTimeMillis() - poolParams.idleTimeout; while (true) { synchronized (cls) { // 如果连接池中没有连接,则直接返回。 if (cls.size() == 0) break; /* * 获取cls中的第一个连接,即头部的连接。 * 后面一节中会讲到,在getconnection时,都是从cls的尾部获取。 * 所以,cls头部的连接,肯定是最近最少被使用的。 */ ConnectionListener cl = (ConnectionListener) cls.get(0); //判断是否超时,return lastUse < timeout if (cl.isTimedOut(timeout)) { //销毁连接计数 connectionCounter.incTimedOut(); // 销毁这个连接,并加入到销毁队列 cls.remove(0); if (destroy == null) destroy = new ArrayList(); destroy.add(cl); } else { //它们是按照时间顺序插入的, 如果这个连接没有超时,肯定没有其它的连接超时。 break; } } } // 有需要销毁的连接,将这些连接进行销毁,并对销毁的连接数进行计数。 if (destroy != null) { for (int i = 0; i < destroy.size(); ++i) { ConnectionListener cl = (ConnectionListener) destroy.get(i); if (trace) log.trace("Destroying timedout connection " + cl); doDestroy(cl); } // 销毁完空闲的连接后,判断连接池没有shutdown,并且最小值大于0。 //进行将连接池填充到最小值的操作。 if (shutdown.get() == false && poolParams.minSize > 0) PoolFiller.fillPool(this); } }
关于fillPool方法可以参考第二节:http://www.dbafree.net/?p=300
关于IdleRemover线程的具体执行过程如下:
ConnectionValidator线程对于连接的验证:
public void validateConnections() throws Exception { if (trace) log.trace("Attempting to validate connections for pool " + this); //获取信号量,若不能获取,表时当前的连接都在被使用。直接结束validate。 if (permits.attempt(poolParams.blockingTimeout)) { boolean destroyed = false; try { while (true) { ConnectionListener cl = null; synchronized (cls) { if (cls.size() == 0) { break; } //对连接池中的每个连接进行check,将removeForFrequencyCheck方法见下面。 cl = removeForFrequencyCheck(); } if (cl == null) { break; } try { Set candidateSet = Collections.singleton(cl.getManagedConnection()); //当前时间-上一次check时间>=后台的validate时间的连接,进行validating操作。 if (mcf instanceof ValidatingManagedConnectionFactory) { ValidatingManagedConnectionFactory vcf = (ValidatingManagedConnectionFactory) mcf; candidateSet = vcf.getInvalidConnections(candidateSet); if (candidateSet != null && candidateSet.size() > 0) { if (cl.getState() != ConnectionListener.DESTROY) { doDestroy(cl); destroyed = true; } } } else { log.warn("warning: background validation was specified with a non compliant ManagedConnectionFactory interface."); } } finally { if(!destroyed) { synchronized (cls) { returnForFrequencyCheck(cl); } } } } } finally { permits.release(); //destory之后,也进行一个fillPool的操作,这个操作可以参考第二节的内容 if (destroyed && shutdown.get() == false && poolParams.minSize > 0) { PoolFiller.fillPool(this); } } } }
这个validate操作,由前台的参数控制,可以传入自定义的SQL来验证。也可以直接调用jdbc的ping database操作,如调用ping database方法,通过jdbc驱动中可以找到,oracle执行的是select * from dual来验证,不一一列举。见下面的截图vaildate的方法:
removeForFrequencyCheck方法如下:
private ConnectionListener removeForFrequencyCheck() { log.debug("Checking for connection within frequency"); ConnectionListener cl = null; for (Iterator iter = cls.iterator(); iter.hasNext();) { cl = (ConnectionListener) iter.next(); long lastCheck = cl.getLastValidatedTime(); //返回 当前时间 - 上一次check时间 >= 设置的validate时间的第一个连接。 //即表示这个连接没有在backgroundInterval区间内进行check。 if ((System.currentTimeMillis() - lastCheck) >= poolParams.backgroundInterval) { cls.remove(cl); break; } else { cl = null; } } return cl; }
连接池的shutdown
public void shutdown() { //设置shutdown标志位为true shutdown.set(true); //清除IdleRemover线程和ConnectionValidator线程的初始化 IdleRemover.unregisterPool(this); ConnectionValidator.unRegisterPool(this); //destroy所有checkout队列和连接临听队列中的连接 flush(); }
flush()函数清理所有的连接:包括checkd out队列(已经被使用)中的连接及空闲的队列的连接。这里将空闲的队列的连接监听进行直接销毁,而checkd out队列的连接设置为DESTROY状态,并没有进行销毁。这是一个安全的操作,我们在下一节的returnConnection方法中可以看到对于DESTROY状态连接的清理。
public void flush() { //生成一个destroy队列 ArrayList destroy = null; synchronized (cls) { if (trace) log.trace("Flushing pool checkedOut=" + checkedOut + " inPool=" + cls); // 标记checkd out的连接为清理的状态。 for (Iterator i = checkedOut.iterator(); i.hasNext();) { ConnectionListener cl = (ConnectionListener) i.next(); if (trace) log .trace("Flush marking checked out connection for destruction " + cl); cl.setState(ConnectionListener.DESTROY); } // 销毁空闲的,需要清理的连接监听器,并加入到destroy队列中。 while (cls.size() > 0) { ConnectionListener cl = (ConnectionListener) cls.remove(0); if (destroy == null) destroy = new ArrayList(); destroy.add(cl); } } //销毁destory队列中的所有连接临听。 if (destroy != null) { for (int i = 0; i < destroy.size(); ++i) { ConnectionListener cl = (ConnectionListener) destroy.get(i); if (trace) log.trace("Destroying flushed connection " + cl); doDestroy(cl); } // 如果shutdown==false,即连接池没有shutdown,则再执行一个fillPool的操作 //将连接数填充到min值。 if (shutdown.get() == false && poolParams.minSize > 0) PoolFiller.fillPool(this); } }
关于shutdown操作,流程图如下:
总结:
idle-timeout-minutes参数:后台定时清理过度空闲的连接,从而节省数据库的连接资源,相应线程的wait时间为idle-timeout-minutes/2。
background-validation-millis参数:后台定时验证连接是否有效,对于oracle,内部执行一个select * from dual;的方法来进行验证,相应线程的wait时间为background-validation-millis/2,JBOSS 4的版本中,默认不启用这个验证。
JBOSS会各自启动一个线程来为这两个参数工作,线程内部的调度机制完全一致。IdleRemover线程被唤醒(即每隔多少时间执行)的区间是idle-timeout-minutes参数/2,ConnectionValidator线程被唤醒(即每隔多少时间执行)的区间是background-validation-millis/2。
在销毁空闲的连接(IdleRemover)和无效的连接(ConnectionValidator)后,都会执行一个prefill的操作,将连接池中的连接数填充到min值,所以,对于连接池min需要合理的进行设置,如果min设置过大,JBOSS会将连接不断的进行销毁->创建->销毁->创建…(idle线程对空闲连接销毁,销毁后小于min值,然后马上又创建,新创建的连接处于空闲状态,于是又被销毁…)
总之,我们需要合理的设置连接池min值,对于某些系统来说,数据库的连接资源是很昂贵的。
前段日子在公司的核心系统上优化的一个连接池,主要也是对于min值的优化:一个生产库的JBOSS连接池调整优化及分析。对于连接数的调优,后面会专门整理一篇文章。
1