SlideShare a Scribd company logo
1 of 67
Download to read offline
JOBSS 连接池原理及调优
                   支付宝-樊振华

邮箱: sxfzh1987@gmail.com
旺旺:周仓
QQ: 164038407
Blog: http://www.dbafree.net
内容列表

原理篇:

 连接池的启动及 prefill 参数

 连接池的初始化及关闭

 连接的获取及返还和异常连接销毁


调优篇:

 如何防止连接风暴

 PreparedStatementCache 参数的作用及原理

 合理的设置 PreparedStatementCache

 inlist 查询优化

 合理设置连接数的 min 值和 max 值

 合理的设置 fetchsize


调优总结:

 调优可能带来的好处


附录:

 连接池默认参数及获取连接池中相关的统计信息
原理篇-连接池的启动及 prefill 参数

     jboss 连接池的启动,主要就是一些对象的初始化及一个 prefill 的过程。
     prefill 参数:在 ds.xml 中有一个参数,叫 prefill,这个值可以设置为 true|false,默认为
false,这个参数在 JBOSS4.0.5 版本以上才能被支持。这个参数的设置,决定了连接池在启
动时是否会初始化 min 连接数(最小连接数)。如果设置为 true,连接池在启动的时候会进行
min 值连接数的初始化,但是在应用同时启动时可能导致连接风暴;如果设置为 false,则在
第一次 getconnection 时,才启动创建 min 连接数。
     本节主要研究了 JBOSS 连接池在启动时究竟做了些什么操作,带着好奇心,我们查看
JBOSS 源码,其实并不难,可以看到在连接池的启动过程中主要涉及到两个类,
InternalManagedConnectionPool 和 PoolFiller。
     PoolFiller 类:在 jboss 启动时即开始 fillerThread 线程的 wait,当执行 fillPool 操作时
被唤醒,fillPool 操作主要的作用是将 JBOSS 的连接池填充到 min 值。当连接池填充到 min
值之后,线程继续 wait。
     InternalManagedConnectionPool:这个类是 JOBSS 连接池管理的核心类,后面几
节主要围绕这个类来展开。
     InternalManagedConnectionPool 类的构造方法和连接池的配置参数紧密相关,如下:
protected InternalManagedConnectionPool(ManagedConnectionFactory mcf,
                     ConnectionListenerFactory clf,
           Subject subject, ConnectionRequestInfo cri,
            PoolParams poolParams, Logger log)
  {
         //mcf 是连接管工厂
      this.mcf = mcf;
      //连接监听工厂
      this.clf = clf;
      //默认的 subject
      defaultSubject = subject;
      //连接请求信息
      defaultCri = cri;
      //连接池参数,主要方法见下面的函数,见附录。
      this.poolParams = poolParams;
      this.maxSize = this.poolParams.maxSize;

     this.log = log;
     this.trace = log.isTraceEnabled();
     //可以使用的连接事件监听器初始化。
     cls = new ArrayList(this.maxSize);
     /*创建一个对应连接事件监听器的信号集,用于连接的获取。
     每次获取连接或者创建连接之前,都需要获取信号量,
     当没有可用的信号量时,表示连接已经到达 max 值。
     */
     permits = new FIFOSemaphore(this.maxSize);
     /*
判断 jboss 配置文件中的 prefill 设置,默认为 false。
      如果设置为 true,则将本连接池加入到一个临时 pool(LinkedList)的最后,
      加入的方式是串行的(线程安全)。
      */
      if(poolParams.prefill)
      {
      //fillPool 主要执行了一个 fillToMin 的方法,即将连接池中的连接,填充到 min 值。
           PoolFiller.fillPool(this);

      }
  }



      这就是 InternalManagedConnectionPool 的实例化,  很简单,最后一步判断如果参数 prefill
设置为 flase,则没有其它事了,整个过程结束。否则,执行一个 fillPool 的操作。
      先来看一下与 PoolFiller 类,     PoolFiller 在构造函数中即完成 fillerThread 线程的启动,  线
程启动后由于 pools 是空的,所以本线程一直处于 wait 状态,当执行 PoolFiller.fillPool(this);
操作时,pools 中首先会增加了一个连接池对象(见 internalFillPool 函数)                   ,然后幻醒
fillerThread 线程,fillerThread 线程被唤醒后,因为此时 pools 已经不为 null 了,线程开始初
始化 pools 队列中的所有的连接池对象              (在并发情况下,    可能有多个连接池对象需要初始化,
但是连接初始化的过程,是一个线程安全的操作)                       ,线程唤醒后主要的操作是将连接池的连
接数填充至 min 值,填充由 fillToMin 函数来执行。
      当 pools 中所有的连接池对象都填充到 min 值时,此时 pools 也被清空了,fillerThread
线程继续进行 wait,直到下一次 fillPool 时被唤醒。在连接池启动时只会被 fillPool 一次,其
它的 fillPool 操作还会在三种情况下发生:
      1.后面的章节会讲到在 removeTimedOut(idle 超时清理时,还会调会 fillPool 操作-即
清理 Idle 连接后,发现小于 min 值,又需要进行 fillPool 操作)。
      2.在 valitionconnection 后,紧接着执行 fillToMin。(同上)
      3.当 prefill 设置为 false, 即连接池实例化时没有被 fill 到 min 值,   在第一次 getconnection
时,触发这个 fillPool 操作。
具体如下图所示:




    相应代码如下:

public class PoolFiller implements Runnable
{
   private final LinkedList pools = new LinkedList();

   private final Thread fillerThread;

   private static final PoolFiller filler = new PoolFiller();

   public static void fillPool(InternalManagedConnectionPool mcp)
   {
      filler.internalFillPool(mcp);
   }

   public PoolFiller ()
   {
      fillerThread = new Thread(this, "JCA PoolFiller");
      fillerThread.start();
   }

   public void run()
   {
      ClassLoader myClassLoader = getClass().getClassLoader();
      Thread.currentThread().setContextClassLoader(myClassLoader);
      //keep going unless interrupted
      while (true)
      {
           try
{
              InternalManagedConnectionPool mcp = null;
              //keep iterating through pools till empty, exception escapes.
              while (true)
              {

                  synchronized (pools)
                  {
                     //取出需要处理的连接池,即需要 fillToMin 的连接池
                       mcp = (InternalManagedConnectionPool)pools.removeFirst();
                  }
                  //如果没有需要处理的连接池了,则跳出 while 循环,进行线程的 wait。
                  if (mcp == null)
                       break;
                  //将连接池 mcp 填充至 min 值。
                  mcp.fillToMin();
              }
        }
        catch (Exception e)
        {
        }

        try
        {
              synchronized (pools)
              {
                /*如果没有需要处理的连接池了,则跳出 while 循环,
                进行线程的 wait,等待下一次执行 internalFillPool 函数唤醒 filltoMin
                */
                 while(pools.isEmpty())
                 {
                     pools.wait();

                  }
              }
        }
        catch (InterruptedException ie)
        {
            return;
        }
    }
}

private void internalFillPool(InternalManagedConnectionPool mcp)
{
         synchronized (pools)
         {
              /*
              将连接池对象加入到 pools 临时队列里,
              这里的 pools 只是一个临时队列,用于进行 fillToMin 的操作。
              */
              pools.addLast(mcp);
           //notify()会触发器 run
              pools.notify();
         }
     }
}



fillToMin 函数如下:

    public void fillToMin()
      {
          while (true)
          {
              /*
                获取一个信号量 - 防止在连接池快满时,产生竞争
              当所有的连接都被 checkd out(即全部被占用)时,避免不需要的 fill 检查。
              */
              try
              {
                     //获取信号量,超时时间为 jboss 数据源配置文件的 time out
                   if (permits.attempt(poolParams.blockingTimeout))
                   {
                        try
                        {
                            //判断连接池是否已经 shutdown?如果已经 shutdown,则直接返回。
                            if (shutdown.get())
                                 return;

                  // 判断连接池中的连接是否已经达到 min 值,如果已经达到,则直接
返回。
                  if (getMinSize() - connectionCounter.getGuaranteedCount() <= 0)
                       return;

                  // 创建一个连接去填充连接池。每次创建一个,因为这个是死循环。
                  try
                  {
ConnectionListener cl =
                           createConnectionEventListener(defaultSubject, defaultCri);
                         synchronized (cls)
                         {
                              if (trace)
                                   log.trace("Filling pool cl=" + cl);
                              cls.add(cl);
                         }
                     }
                     catch (ResourceException re)
                     {
                         log.warn("Unable to fill pool ", re);
                         return;
                     }
                 }
                 finally
                 {
                       //释放信号量
                     permits.release();
                 }
              }
          }
          catch (InterruptedException ignored)
          {
              log.trace("Interrupted while requesting permit in fillToMin");
          }
      }
  }




附:
  ds.xml 参数的解释:
  < prefill > - 这个参数决定是否填充连接池到最小连接数。         如果连接池不支持这个功能,
将在日志文件中看到一个警告,默认设置为 false。
  < blocking-timeout-millis > - 当所有的连接都被占用(即被使用)时,为了从连接池中等
待一个可用的连接而消耗的时间。默认值设置为 5 秒。
  仔细研究源码,可以获得更加细节的细节的知识,本块知识点只是介绍连接池的启动,
过程很简单。
原理篇-连接池的初始化及关闭


      前一节已经讲了 jboss 连接池的启动过程及 prefill 参数配置。
      在启动完成之后,连接池紧接着会进行一个 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 连接池调整优化及分析。对于连接数的调优,后面会专门整理一篇文章。
原理篇-连接的获取及返还和异常连接销毁

    前面二节已经介绍了 JBOSS 的启动及初始化。在初始化的时候,会加载 IdleRemove 线
程和 validateConnections 线程。IdleRemove 线程默认 15 分钟启动一次用于空闲连接的清
理;validateConnections 线程默认 10 分钟启动一次,用于连接的定时校验,及时清理无效的
连接。
    本节介绍 JBOSS 的两个最重要的方法:getconnection(获取连接)和 returnConnection(释
放连接)    。最后,补充介绍一下 JBOSS 中对于异常连接是如何销毁的。
当应用需要进行业务处理时,首先会执行一个 getConnection 的操作,用于从连接池中获取
连接,   当业务处理完成后,         需要把连接放回到连接池中,     执行一个 returnConnection 的操作。
    下面先看一下 getConnection 的源码:


//getConnection 方法返回的值是一个连接监听对象 ConnectionListener
public ConnectionListener getConnection(Subject subject, ConnectionRequestInfo cri)
                 throws ResourceException
    {

       subject = (subject == null) ? defaultSubject : subject;
       //获取连接信息
       cri = (cri == null) ? defaultCri : cri;
       //打印 startWait,即当前时间,精确到毫秒
       long startWait = System.currentTimeMillis();
       try
       {
          /*等待最多 xx 毫秒获取一个信号量(permit)                      ,即 permit 操作。
           * permit 操作用于获取当前可用的信号量,即是否有可以使用的信号量。
             因此,因为在创建连接池的时候,我们也创建了一个 max 值的信号集,
             所以,对于连接池中的连接数未达到 max 值的时候,肯定有可以使用的信号量。
               除非,所有的连接都已经在使用状态,并且连接数已经达到 max 值,
               这时,才有可能出现没有信号量,并出现超时的情况。
           */
           if (permits.attempt(poolParams.blockingTimeout))
           {
             //计算本次获取连接的阻塞时间=当前时间-开始获取信号量的时间
                long poolBlockTime = System.currentTimeMillis() - startWait ;
                //累加阻塞时间。
                connectionCounter.updateBlockTime(poolBlockTime);
                //我们有一个权限去获取一个连接,判断是否在连接池中已经有一个可用的连
接?
              ConnectionListener cl = null;
              do
              {
                 //线程安全,即从连接池中获取连接是一个串行操作
                   synchronized (cls)
{
          //判断连接池是否已经被 shutdown,如果被 shutdown,
          //则抛出异常"The pool has been shutdown",并释放信号量
            if (shutdown.get())
            {
                 permits.release();
                 throw new ResourceException("The pool has been shutdown");
            }
            //如果可用的连接事件监听的 arraylist 大于 0,                 则从 arraylist 的尾部取一
个连接
           if (cls.size() > 0)
           {
                cl = (ConnectionListener) cls.remove(cls.size() - 1);
                //将 arraylist 中获取的连接加到到 checkdout 的一个 hash 结构中.
                checkedOut.add(cl);
                //计算已经使用的连接数
                int size = (int) (maxSize - permits.permits());

               //更新当前已经使用的最大连接数
               if (size > maxUsedConnections)
                    maxUsedConnections = size;
           }
      }
      //如果已经从连接事件监听数组中获取到了连接
      if (cl != null)
      {
        //我们从一个 pool 取一个 ManagedConnection,并检查它是否符合要求?
           try
           {
               Object matchedMC = mcf.matchManagedConnections
               (Collections.singleton(cl.getManagedConnection())
                   ,subject, cri);
               if (matchedMC != null)
               {
                   if (trace)
                     log.trace("supplying ManagedConnection from pool: " + cl);
                   //通知 connection listener 是否它拥有权限
                   cl.grantPermit(true);
                   //返回连接,结束
                   return cl;
               }

               /*
                * 匹配不成功,并且没有异常信息抛出。
* 在检查的时候,要么我们匹配错误,要么连接已经死亡
               我们需要去辨别这些场景,但是现在,不管怎么样,我们都销毁连
接。
               */
             log.warn("Destroying connection that could not be
             successfully matched: " + cl);
             synchronized (cls)
             {
               //从 checkout 的 hashset 中删除已经获取的连接。
                  checkedOut.remove(cl);
             }
             //销毁连接
             doDestroy(cl);
             cl = null;

          }
          //不管发生任何事,都销毁连接
          catch (Throwable t)
          {
              log.warn("Throwable while trying to match ManagedConnection,
                      destroying connection: " + cl, t);
              synchronized (cls)
              {
                  checkedOut.remove(cl);
              }
              doDestroy(cl);
              cl = null;

          }
          //如果发生意外,我们应该决定是否应该继续尝试去建立连接,
          //由 jboss 配置文件中的的 useFastFail 参数来决定,默认为 false。
          //这个 useFastFail 设置为 true,则立刻跳出 get connection,并报错。
          if(poolParams.useFastFail)
          {
              log.trace("Fast failing for connection attempt.
             No more attempts will be made to acquire connection from pool
              and a new connection will be created immeadiately");
              break;
          }

        }
     }
     //当连接监听队列>0,即还有可用的连接监听器
     while (cls.size() > 0);//end of do loop
//OK, 我们不能够找到一个可以使用的连接,则新建一个
 try
 {
      //创建一个新的连接。这里不需要判断是否已经到达 max 连接数
    //因为前面已经获取了信号量,所以肯定可以创建连接。
      cl = createConnectionEventListener(subject, cri);
      synchronized (cls)
      { //将创建的连接加入到 checkout 数组中。
          checkedOut.add(cl);
          int size = (int) (maxSize - permits.permits());
        //更新当前已经使用的最大连接数
          if (size > maxUsedConnections)
               maxUsedConnections = size;
      }

    //如果连接池还没有启动,则初始化连接池,并设置值 started 为 true。
    //这里连接池可能被启用多次(因为非线程安全),但是这里没有危害。
    if (started == false)
    {

        started = true;
        if (poolParams.minSize > 0)
             PoolFiller.fillPool(this);
    }
    if (trace)
      log.trace("supplying new ManagedConnection: " + cl);
    //通知 connection listener 是否它拥有权限
    cl.grantPermit(true);
    return cl;
 }
 catch (Throwable t)
 {
log.warn("Throwable while attempting to get a new connection: " + cl, t);
     //return permit and rethrow
     synchronized (cls)
     {
          checkedOut.remove(cl);
     }
     permits.release();
     JBossResourceException.rethrowAsResourceException(
   "Unexpected throwable while trying to create a connection: " + cl, t);
     throw new UnreachableStatementException();
 }
}
       //这里的 else 操作,不能获取信号量,则抛出异常,报错连接池超时。
       else
       {
           // we timed out
           throw new ResourceException("No ManagedConnections available
              within configured blocking timeout ( "
                  + poolParams.blockingTimeout + " [ms] )");
       }

    }
    catch (InterruptedException ie)
    {
        long end = System.currentTimeMillis() - startWait;
      connectionCounter.updateBlockTime(end);
        throw new ResourceException("Interrupted while requesting permit!
        Waited " + end + " ms");
    }
}
执行过程流程图如下:
关于 getConnetion 的几点说明
    1.blockingTimeout 是一个 jboss 的参数:
    <blocking-timeout-millis >5000</blocking-timeout-millis >,它是一个获取信号量的超
    时时间,更确切的说,是从连接池中获取一个连接的超时时间。如果超过 5000ms,不
    能够获取到信号量(连接),则 jboss 会抛出异常: “can not get connection,No
    ManagedConnections available within configured blocking timeout [xx] ms”。当然,
    只要当前正在使用的连接数没有到达 MAX 值,这个信号量一定能够被获取到。因为信
    号量一共有 MAX 值个,如果连接池中当前的连接不够用时,在获取信号量之后,新创
    建一个连接即可。建议可以设置成可以设成 500ms 左右,不需要设计过大,当应用中
    存在多个数据源时,可以防止因 DB 异常线程池带来的阻塞。如果网络环境不好的话,
    可以设置的更高一点。
    2.连接池内部就是一个连接监听队列,每次都从队列的尾部获取连接。而 IdleRemove
线程,则是从这个队列头部开始进行清理。
    3.连接池的获取,销毁,创建,这三个操作都是线程安全的操作。
    4.业务在使用连接的过程中,会一直占有这个信号量,在 returnConnection 或者发生
异常时释放信号量。能够获取信号量,则意味着肯定可以获取到连接。第二节中,我们讲到
JBOSS 在启动时会初始化一个信号量数组,长度为连接池的 max 参数。

     当业务系统使用完连接后,需要把连接放回到连接池中它的主要见下图,源代码如下:
public void returnConnection(ConnectionListener cl, boolean kill) {
     synchronized (cls) {
        /*
                   * 判断连接是否已经被 DESTROYED?
                   * 可能有其它的线程如 background-validation 及 shuwdown
                   * 标记这个连接临听器为 DESTORYED 状态。
                   *
                   */
        if (cl.getState() == ConnectionListener.DESTROYED) {
           if (trace)
              log
              .trace("ManagedConnection is being returned after it was destroyed"
                         + cl);
           //释放信号量,并直接返回
           if (cl.hasPermit()) {
              // release semaphore
              cl.grantPermit(false);
              permits.release();
           }

             return;
         }
     }

     if (trace)
log.trace("putting ManagedConnection back into pool kill=" + kill
         + " cl=" + cl);
try {
   //前台应用强制清理连接。
   cl.getManagedConnection().cleanup();
} catch (ResourceException re) {
   log.warn("ResourceException cleaning up ManagedConnection: " + cl,
         re);
   //清理失败,抛出异常,清理失败。
   kill = true;
}

synchronized (cls) {
  // 连接监听的状态为 DESTROY 或者 DESTROYED,则设置 kill 为 true
  if (cl.getState() == ConnectionListener.DESTROY
        || cl.getState() == ConnectionListener.DESTROYED)
     kill = true;
  //checkedOut 队列中移除连接监听器。
  checkedOut.remove(cl);

  //如果 kill==false,    并且连接数>=最大连接 max 值,               说明异常发生,     再次设置 kill=true
  if (kill == false && cls.size() >= poolParams.maxSize) {
     log
.warn("Destroying returned connection, maximum pool size exceeded "
                + cl);
     kill = true;
  }

  //kill 连接
  if (kill) {
     // Adrian Brock: A resource adapter can asynchronously notify us
     // that
     // a connection error occurred.
     // This could happen while the connection is not checked out.
     // e.g. JMS can do this via an ExceptionListener on the
     // connection.
     // I have twice had to reinstate this line of code, PLEASE DO
     // NOT REMOVE IT!
     cls.remove(cl);
  }
  //如果 kill==false
  else {
     cl.used();
     //这个连接监听不属于连接监听队列,则加入。
if (cls.contains(cl) == false)
               cls.add(cl);
            else
               log.warn("Attempt to return connection twice (ignored): "
                     + cl, new Throwable("STACKTRACE"));
        }

        if (cl.hasPermit()) {
           //释放信号量
           cl.grantPermit(false);
           permits.release();
        }
    }

    if (kill) {
       if (trace)
          log.trace("Destroying returned connection " + cl);
       //销毁连接。
       doDestroy(cl);
    }
}
执行过程流程图如下:




ReturnConnetion 总结:
   1.释放连接也是一个线程安全的操作。
   2.在连接 return 时,有可能已经是 destory 的状态(前面第三节中讲到的 SHUTDOWN
   操作,会对连接打上 DESTORY 的标记),这时,直接进行 remove 即可。
   3.释放的连接若不属于连接监听队列(连接池),即加入到连接监听队列中(即连接池
中)。
   4.释放连接需要释放信号量。
5.在释放过程中,出现任何异常,则将连接从连接池中移除,并进行强制销毁。



JBOSS 对于异常连接的处理:
   默认情况下,JBOSS 不会对无效的连接进行销毁。
   如果我们需要对异常列表中的连接进行销毁,则需要在连接池的 ds.xml 中添加以下配
置:


< exception-sorter-class-name>
     org.jboss.resource.adapter.jdbc.vendor.OracleExceptionSorter
     < /exception-sorter-class-name>


  这个是 ORACLE 的异常列表,可以查看这个类里面定义了 ORACLE 的异常列表,当连
接抛出这个列表中的错误时,即会进行销毁,如不能销毁,异常连接会一直存在连接池中。
ORACLE 异常列表如下:

 public boolean isExceptionFatal(final SQLException e)
    {
                // I can't remember if the errors are negative or positive.
          final int error_code = Math.abs( e.getErrorCode() );

          if( ( error_code == 28 )          //session has been killed
            || ( error_code == 600 )        //Internal oracle error
            || ( error_code == 1012 )       //not logged on
            || ( error_code == 1014 )       //Oracle shutdown in progress
            || ( error_code == 1033 )       //Oracle initialization or shutdown in progress
            || ( error_code == 1034 )       //Oracle not available
            || ( error_code == 1035 )         //ORACLE only available to users with RESTRICTED
SESSION privilege
            || ( error_code == 1089 )          //immediate shutdown in progress - no operations are
permitted
            || ( error_code == 1090 )       //shutdown in progress - connection is not permitted
            || ( error_code == 1092 )       //ORACLE instance terminated. Disconnection forced
            || ( error_code == 1094 )        //ALTER DATABASE CLOSE in progress. Connections
not permitted
            || ( error_code == 2396 )       //exceeded maximum idle time, please connect again
            || ( error_code == 3106 )       //fatal two-task communication protocol error
            || ( error_code == 3111 )       //break received on communication channel
            || ( error_code == 3113 )       //end-of-file on communication channel
            || ( error_code == 3114 )       //not connected to ORACLE
            || ( error_code >= 12100 && error_code = 21000 ) &&
                    ( (error_text.indexOf("SOCKET") > -1)              //for control socket error
                    || (error_text.indexOf("CONNECTION HAS ALREADY BEEN CLOSED") >
-1)
|| (error_text.indexOf("BROKEN PIPE") > -1) ) )
        {
             return true;
        }

        return false;
    }



    类似的,我们也可以找到一个 MYSQL 的异常列表,
org.jboss.resource.adapter.jdbc.vendor.OracleExceptionSorter:

  public boolean isExceptionFatal(SQLException e)
   {
       if (e.getSQLState() != null)
       { // per Mark Matthews at MySQL
            if (e.getSQLState().startsWith("08"))
            {
                 return true;
            }
       }
       switch (e.getErrorCode())
       {
          // Communications Errors
          case 1040: // ER_CON_COUNT_ERROR
          case 1042: // ER_BAD_HOST_ERROR
          case 1043: // ER_HANDSHAKE_ERROR
          case 1047: // ER_UNKNOWN_COM_ERROR
          case 1081: // ER_IPSOCK_ERROR
          case 1129: // ER_HOST_IS_BLOCKED
          case 1130: // ER_HOST_NOT_PRIVILEGED

        // Authentication Errors
        case 1045: // ER_ACCESS_DENIED_ERROR

        // Resource errors
        case 1004: // ER_CANT_CREATE_FILE
        case 1005: // ER_CANT_CREATE_TABLE
        case 1015: // ER_CANT_LOCK
        case 1021: // ER_DISK_FULL
        case 1041: // ER_OUT_OF_RESOURCES

        // Out-of-memory errors
        case 1037: // ER_OUTOFMEMORY
case 1038: // ER_OUT_OF_SORTMEMORY

              return true;
        }

        return false;
   }



       还有各种数据库的异常列表,都可以在自行配置,都定义在这个包下:
org.jboss.resource.adapter.jdbc.vendor。
调优篇-如何防止连接风暴

     引言:为什么 prefill=false 会在启动的时候碰到连接风暴,本节结合 JBOSS 源代码的
来分析这种情况产生的根本原因,最后给出几种方案来解决这种连接数突然飙升的问题。
(prefill 这个参数在 jboss4.0.5 版本以后才能够被支持)
     连接风暴,听起来这个词很时髦,那么到底什么是连接风暴?在百度和 google 上搜索
了下,    都没有找到相关的解释。         所谓连接风暴,我们也可以称之为访问风暴,这是在使用 jboss
默认参数 prefill=false 的情况下,大规模应用集群很容易碰到的问题。先来描述一个场景:
     在项目发布的过程中,我们需要重启应用,当应用启动的时候,经常会碰到各应用服务
器的连接数异常飙升。假设连接数的设置为:min 值 3,max 值 10。正常的业务使用连接数在
5 个左右,当重启应用时,各应用连接数可能会飙升到 10 个,瞬间甚至还有可能部分应用
会报取不到连接。启动完成后接下来的时间内,连接开始慢慢返回到业务的正常值。这种场
景,就是碰到了所谓的连接风暴。
知道了连接风暴的场景,那么它到底有什么危害呢,简单来说产要有以下几点(这些都是实
际碰到的情况)       :
     1. 在多个应用系统同时启动时,系统大量占用数据库连接资源,可能导致数据库连接
数耗尽。
     2. 数据库创建连接的能力是有限的,并且是非常耗时和消耗 CPU 等资源的,突然大量
请求落到数据库上,极端情况下可能导致数据库异常 crash。
     3. 对于应用系统来说,多一个连接也就多占用一点资源。在启动的时候,连接会填充
到 max 值,并有可能导致瞬间业务请求失败。
为什么我们的应用系统会在启动时会产生连接风暴。              前面第二节我们已经介绍了 jboss 在启
动的时候,有一个参数 < prefill >,这个参数决定了启动时是否初始化数据库的连接,默认
设置为 false,即启动时不初始化 min 值连接数。min 值的初始化在业务请求调用的第一次
getConnection 时进行。
下面来看一下,getConnection 的基本流程:(详细细节参考:
http://www.dbafree.net/?p=378)
假设 min 值:9,max 值:20,我们结合上图来分析一下应用启动时的场景。因为数据库创建
一个连接需要耗费的时间在 80-100ms。在数据库启动的时候,80ms 之内,如果发生了并发
的 10 个 getConnection 操作(假设这 10 个 getConnection 都不能被复用),则会怎么样呢?一
共会创建多少个连接呢?
   在 80ms 之内,第一个并发 getConnection 会执行 filltoMin(创建 9 个连接)  ,其它的 9
个并发请求会各自独立的创建一个连接,所以一共会创建 9+9=18 个连接。这个过程的流程
图如下:




  在实际执行 filltoMin 操作需要 9*80ms=720ms 左右,即在 80ms,160ms,240ms 的时候,
会分别创建 1 个连接,创建的连接,马上就可以被使用。所以,连接风暴会在最初的 80ms
内非常明显。
  当这 18 个连接被创建后,我们前面在章节 JBOSS 连接池的初始化及关闭中已经提
到,连接池初始化的时候会把当前连接池注册到 IdleRemove 线程中,对于空闲 30 分钟以上
的连接会进行定时清理,这个定时任务每隔 15 分钟运行一次。所以定时任务执行时,会清
理空闲时间在 30 分钟-45 分钟的空闲连接。因此,在最多 45 分钟之后,连接又可以回落到
正常的业务值。
  了解了连接风暴的原理后,我们可以思考一下解决方案。这是目前总结的三种方案:
    1. 设置 jboss 连接池参数 prefill=true,即在启动时将连接数填充到 min 值。
    2. 不设置 prefill,在启动完成,业务正常运行,应用先执行一个 SQL(如 oracle
  可以执行:select * from dual),将连接数填充到 min 值。
    3. 选择在业务低峰时重启系统。
以上三种方案中,第一种是最简单的,也不需要任何的修改。第二种方案有点麻烦,实
现的方式和第一种方案本质上一样,进行连接预加载。我碰到过有的系统实现了这种方案,
并且预加载了 PreparedStatementCache。当然,我觉得意义并不是很大,不知道为什么会有
人选择这种方式来实现,只能猜想也许是程序员并不知道第一种方式的存在。第三种方案,
不用说了,治标不治本。
  总结三种方案,我们可以通过简单的设置 prefill=true 来解决连接风暴的问题。
调优篇-PreparedStatementCache 参数的作用及原理

     JBOSS 连接池配置文件中有个参数,叫做 PreparedStatementCache,这个值怎么设置
呢,对于某些数据库来讲,这个值的设置会产生比较大的性能影响。

     先来看两个使用 JAVA 语言来查询数据库例子:

例 1:

String sql = "select * from table_name where id = ";
Statement stmt = conn.createStatement();
rset = stmt.executeQuery(sql+"1");



例 2:

String v_id = 'xxxxx';
String v_sql = 'select name from table_a where id = ? ';   //嵌入绑定变量
stmt = con.prepareStatement(v_sql);
stmt.setString(1, v_id ); //为绑定变量赋值
stmt.executeQuery();


例 1 和例 2 有什么区别呢?
先来看看 JBOSS 源码中对于这两个方法的解释:
createStatement()方法:
    创建一个 Statement 对象,用于发送 SQL 语句到数据库。没有使用绑定变量的 SQL 语
句一般使用 Statement 来执行。如果一个相同的 SQL 语句被执行多次,则使用
PreparedStatement 是一个更好的方式。
PreparedStatement prepareStatement(String sql)方法:
    创建一个 PreparedStatement 对象,用于发送使用绑定变量的 SQL 语句到数据库中。
SQL 语句能够被预编译,并且存储到一个 PreparedStatement 对象中。这个对象,可以在多
次执行这个 SQL 语句的块景中被高效的使用(使用绑定变量的 SQL 至少可以省去数据库的
硬解析)。
    因为 SQL 可以被预编译,所以这种方式用于使用绑定变量的 SQL。如果驱动程序支持
绑定变量,方法 prepareStatement 将会发送一个 SQL 语句到数据库,以执行预编译(即数
据库中的解析)。也有一些驱动不支持预编译,在这种情况下,在 PreparedStatement 执行
之后,语句才会被发送到数据库,这对于用户没有直接的作用。
这是源码中对于这两个方法的解释。

     简单来说,这是数据库执行 SQL 的两种方式。第一种方式,直接发送 SQL 语句到数
据库执行。第二种方式在使用上较第一个方式会复杂一点,首先发送 SQL 到数据库,进行
解析,  然后将有关这个 SQL 的信息存储到一个 PreparedStatement,这个 PreparedStatement
可以被同样的 SQL 语句反复使用。
PreparedStatement 是 JDBC 里面提供的对象,而 JBOSS 里面引入了一个
PreparedStatementCache,PreparedStatementCache 即用于保存与数据库交互的
prepareStatement 对象。 PreparedStatementCache 使用了一个本地缓存的 LRU 链表来减少
SQL 的预编译,减少 SQL 的预编译,意味着可以减少一次网络的交互和数据库的解析(有
可能在 session cursor cache hits 中命中,也可能是 share pool 中命中),这对应用的 DAO
响应延时是很大的提升。

下面是 JBOSS 源代码中对于 prepareStatementCache 的实现。

BaseWrapperManagedConnection 类是 JBOSS 连接管理最基本的一个抽象类,有两个类继
续自这个抽象类,分别为:
1.LocalManagedConnection 本地事务的连接管理。(一般我们都使用这个类)
2.XAManagedConnection 分布式事务的连接管理
BaseWrapperManagedConnection 类的构造函数中, PreparedStatementCache 的初始化。
                                        有
(即在连接被创建的时候,即初始化 PreparedStatementCache),初始化代码如下:

if (psCacheSize > 0)
           psCache = new PreparedStatementCache(psCacheSize);



    而 PreparedStatementCache 这个类继承自 LRUCachePolicy 类,如下:

public class PreparedStatementCache extends LRUCachePolicy
   public PreparedStatementCache(int max)
   {
         //max 值即为 JBOSS 连接池配置文件定义的 psCacheSize
        super(2, max);
        create();
   }

  其中 super 如下:
  //根据指定的最小值和最大值,创建 LRU cache 管理方案。
  public LRUCachePolicy(int min, int max)
    {
       if (min < 2 || min > max)
       {throw new IllegalArgumentException("Illegal cache capacities");}
       m_minCapacity = min;
       m_maxCapacity = max;
    }

 public void create()
   {
       m_map = new HashMap();
       m_list = createList();
       m_list.m_maxCapacity = m_maxCapacity;
       m_list.m_minCapacity = m_minCapacity;
m_list.m_capacity = m_maxCapacity;
   }



       PreparedStatementCache 对象其实是一个是一个 LRU List,即它使用了一个双向链表
来存储 PreparedStatement 的值,这个 LRU 链表的数据结构如下:

public class LRUList
   {
        /** The maximum capacity of the cache list */
        public int m_maxCapacity;
        /** The minimum capacity of the cache list */
        public int m_minCapacity;
        /** The current capacity of the cache list */
        public int m_capacity;
        /** The number of cached objects */
        public int m_count;
        /** The head of the double linked list */
        public LRUCacheEntry m_head;
        /** The tail of the double linked list */
        public LRUCacheEntry m_tail;
        /** The cache misses happened */
        public int m_cacheMiss;
        /**
          * Creates a new double queued list.
          */
        protected LRUList()
        {
             m_head = null;
             m_tail = null;
             m_count = 0;
        }
    在 BaseWrapperManagedConnection 被实例化的时候,即连接创建的时候,会初始化
一个最小值为 2,最大值为 max 的一个 LRU 双向链表(max 值在 jboss 连接池中通过<
prepared-statement-cache-size> 50< /prepared-statement-cache-size>参数来设置)。
BaseWrapperManagedConnection 方法的 prepareStatement 方法,源代码如下:

  PreparedStatement prepareStatement(String sql, int resultSetType,
              int resultSetConcurrency) throws SQLException
   {
      if (psCache != null)
      {
         //实例化 KEY 类。
           PreparedStatementCache.Key key = new PreparedStatementCache.Key(sql,
PreparedStatementCache.Key.PREPARED_STATEMENT,
               resultSetType, resultSetConcurrency);
        /*
        根据 KEY 从 LRU 链表中获取相应的 SQL,即 CachedPreparedStatement,
        它包 SQL 语句及一些其它的环境参数。
        在 get key 时进行判断,若有值,则将这个 KEY 对象移动到 LRU 链表头,LRU 链表的
热头.
        */
        CachedPreparedStatement cachedps =
                      (CachedPreparedStatement) psCache.get(key);
        if (cachedps != null)
        {
        /*判断是否可以使用。如果没有其它人使用,则可以使用这个 KEY。
        否则进行下一步判断:是否自动提交模式,是否共享 cached prepared statements。
        */
             if (canUse(cachedps))
                  cachedps.inUse();
             else
                   /*
                   如果不能使用,则临时创建一个 PreparedStatement 对象,
                   这个 PreparedStatement 不会被插入到 psCache。
                   */
                  return doPrepareStatement(sql, resultSetType, resultSetConcurrency);
        }
        else
        {
                   //若没有找到相应的 KEY, 创建一个 PreparedStatement 对象。
             PreparedStatement ps =
                      doPrepareStatement(sql, resultSetType, resultSetConcurrency);
             //再创建一个 CachedPreparedStatement
             cachedps = wrappedConnectionFactory.createCachedPreparedStatement(ps);
             /*
             把新的 SQL 语句插入到 psCache 中。这个新的 SQL 语句被放到 LRU 链表的
头部,
           同时,如果 LRU 链表在放置时满了,则清理最后的一个 SQL。
           */
           psCache.insert(key, cachedps);
        }
        return cachedps;
      }
      /*
      如果 psCache 为 null,则说明没用启用 psCache,或者 psCache 为空,
      则直接创建一个 PreparedStatement 对象进行查询。即不使用 psCache。
      */
else
          return doPrepareStatement(sql, resultSetType, resultSetConcurrency);
  }



    BaseWrapperManagedConnection 是一个连接管理的抽象类,对于每一个数据库的连接,
都有独立的 PreparedStatementCache。
    在 ORACLE 数据库中,        使用 PreparedStatementCache 能够显著的提高系统的性能, 前提
是 SQL 都使用了绑定变量,因为对于 ORACLE 数据库而言,在 PreparedStatementCache 中
存在的 SQL,不需要 open cursor,可以减少一次网络的交互,并能够绕过数据库的解析,即
所 有的 SQL 语 句, 不需 要解 析即 可以 直接 执行。 但是 PreparedStatementCache 中 的
PreparedStatement 对象会一直存在,并占用较多的内存。
    在 MYSQL 数据库中,因为没有绑定变量这个概念,MYSQL 本身在执行所有的 SQL
之前,都需要进行解析,因此在 MYSQL 中这个值不会对性能有影响,这个值可以不设置,
但是我觉得设置了这个值,可以避免在客户端频繁的创建 PreparedStatement,也有一定的好
处(这个没有经过测试,总的来说,意义并没有 ORACLE 那么大)                         。
    另外,PreparedStatementCache 也不是设置越大越好,毕竟,PreparedStatementCache 是
会占用 JVM 内存的。之前出现过一个核心系统因为在增加了连接数(拆分数据源)后,这
个参数设置没有修改,导致 JVM 内存被撑爆的情况。                          (JVM 内存占用情况=连接总数
*PreparedStatementCache 设置大小*每个 PreparedStatement 占用的平均内存)   。
调优篇-合理的设置 PSCACHE

   在前面第一节的 JBOSS 连接池 1-PreparedStatementCache 参数的作用及原理中已经解
释了 PreparedStatementCache(以下简称 PSCache)的作用及其工作的原理。这一节将根据
实际工作中碰到的问题,谈一下如何进行 PSCache 的调优。
对于 PSCache 设置过大或过小的影响:
为什么我们要合理的设置 PSCache 的值,这个值对于应用系统有怎样的影响呢?当然,之
所以要把这个问题提出来,肯定是有原因的,在这个参数的设置问题上,我们是吃过亏的。
所谓合理的设置,即这个值既不能设置太大,也不能设置太小。PSCache 设置过小,可能导
致 PSCache 命中率降低,最直观的影响,就是应用访问数据库延时增加,具体分析一下,
主要有以下几方面的影响:
   1.对于没有在 PSCache 中的 SQL,每次需要在应用程序中新建 PS 对象。                   (PS 对象的大小
和具体的 SQL 有关,       一般可以认为在几 K~1MB 之间,               具体大小需要以 JVM 的内存 DUMP
为准。后面会详细介绍。           )
   2.新建 PreparedStatement,需要进行一次网络的交互,这个开销非常大的。                     (主要由于
交换机的延时及实际距离产生的代价,                   一般可以认为这一次网络的交互在 0.3~0.5ms 之间。          )
   3.新建 PreparedStatement,需要数据库的一次解析操作,                而解析操作是非常消耗数据库资
源的,一般一次解析的时间,我们可以认为在 0.05ms 左右。
   以上就是 PSCache 不能命中时主要的代价。实际经验中,我们碰到过因 PSCache 太小
的情况下,     应用访问增加了 0.6~0.8ms 的延时,            这个影响是很大的。       并且,PSCache 的增大,
可以消除 ORACLE 的等待事件:”SQL*Net more data from client”,这个等待事件在某生产库
上比较厉害,调整 PSCache 之后,由于该等待事件的下降,应用 DAO 的平均响应时间实际
提升达到了将近 1ms。关于该等待事件的详情,参
考:http://blogs.warwick.ac.uk/java/entry/wait_class_network/
   显然,PSCache 设置太小对性能影响很大,但是设置的过大,也会产生问题,PSCache
设置过大有可能耗尽应用服务器的内存,导致应用内存被撑爆。

那么如何来设置 PSCache 这个值呢,首先我们需要知道它占用的内存大小。

连接池内存耗费的计算:
  我们知道,一个连接池中有多个数据库连接,每一个连接都有单独的 PSCache,并且,
连接池中占空间最大的就是 PSCache(占用 95%以上的空间大小)。因此,JVM 中连接池
的内存开销可以大致计算如下:

  JVM 内存占用大小=(连接池 1 中的连接数*PSCache 大小*平均每个 PS 的内
存占用)+  (连接池 2 中的连接数*PSCache 大小*平均每个 PS 的内存占用)+(连
接池 3 中的连接数*PSCache 大小*每个 PS 的内存占用)

假设一个应用有 3 个连接池,每个连接池有 10 个连接,每个 PreparedStatement 平均大小
为 200K。当 PSCache 分别设置为 30 和 100 时,它们的内存占用情况分别如下:

   当 PSCache 为 30 时:内存大小=3*10*200K*30=175.78MB
   当 PSCache 为 100 时:内存大小=3*10*200K*100=585.95MB
   两者 PSCache 相差了 400 多 MB,和内存大小有关系的 4 个参数,分别是:连接池个
数,连接池中的连接数,PSCache 的大小,PS 大小。
连接池个数:    这个和应用架构有关系,       如果不能省略数据库的访问或者通过接口调用来
访问,这一块的开销必不可少。
   连接池中的连接数:      这个由业务的处理时间及业务量决定。        我们只能合理的设置连接池
的 min 值为 max 值。
   PSCache 大小:这个参数可以由我们来控制,和 PS 的大小关系很大,需要合理设置
这个参数。
   PS 大小: 由具体访问的 SQL 语句决定,      这个值决定了 PSCache 大小的设置。因此,JVM
内存空间的计算最关键的问题变成了如何计算 PreparedStatement 的大小。
如何计算 PreparedStatement(PS)的大小:
   进行连接池内存计算的关键值就是 PreparedStatement 的大小。我们可以对正在运
行的 JBOSS 容器,作一个 JVM 内存的 DUMP(方法:使用 jmap 来产生一个 DUMP
文件) ,然后使用 Memory Analyzer (MAT)工具来查看 JVM 内存里面的具体内容。下
面是我自已测试的一个 DUMP 结果,如下:
测试用例中执行了一个 SQL 为:

Select * from test10000 where col_a=:1



Test10000 的表结构如下:

ZHOUCANG/ZHOUCANG@TEST>create table test10000

ZHOUCANG/ZHOUCANG@TEST>(col_a varchar2(4000) ,

ZHOUCANG/ZHOUCANG@TEST> col_b varchar2(4000), col_c varchar2(2000));

Table created.
使用 OCI 方式连接数据库:
用 OCI 方式连接数据库,可以看到调用的是 JDBC 的 T2CPreparedStatement 方法,里
面有一个 CHAR[100030]的数组,占用了 200KB 左右的内存,这个就是用于保存数据库查
询结果的 char 数组。这里的 fetchsize 设置为 10(jdbc 默认值)。同样使用 OCI 方式,我
们将 fetchsize 设置为 100,则获取到的 char 数据长度也增大了 10 倍,为 CHAR[1000300],
大小在 2MB,如下图:




可见 PreparedStatement 占用内存的大小,和 fetchsize 有很大的关系。同样的,char 数组
保存了查询的结果集,       所以,  查询的字段的总长度决定了也 PreparedStatement 对象的大小。
有了 fetchsize 和查询字段总长度,我们就可以计算出 PS 内存占用。如 SQL:Select* from
test10000,选取的字段长度为 10000,内存占用为 200KB,fetchsize 为 10。则可以得出
这个比例为:
200K/10(fetchsize)/10000(字段总长)=2

为什么这个比值是 2 呢?

    怀颖这中间会有一层字符集的转换。    10000
                            (字节)    长度的字段, fetchsize 为 10 时,
                                             当
需要保存的占用的 JVM 内存的 char 数组长度为:  10000 字节*10=100KB, char[100030],
                                               即
而本地客户端的编码为 GBK,所以,char[100030]的字符数组转化为字节数,大约会占用
200KB 的内存空间。

使用 thin 的方式连接数据库:
这里使用了 thin 方式来连接数据库,     fetchsize 设置为 100,保存的数据结构同样是 CHAR
数据,THIN 方式与 OCI 方式没有区别,并且 PreparedStatement 内存占用也基本一致。唯
一的区别是,thin 方式调用的是 JDBC 的 T4CPreparedStatement 方法,而 OCI 方式调用的
是 T2CPreparedStatement。具体这两个方法的区别,有兴趣的同学可以研究下 JDBC 的驱
动。

关于 char 数组的补充:
    以上测试基于 OJDBC10.2.3 版本进行测试,           在早期的 JDBC 驱动中,   我们可以看到 select
返 回 结 果 集 是 以 一 个 Object 数 组 的 形 式 来 保 存 , 如 fetchsize 为 10 , 这 个 数 组 为
Object[10],fetchsize 为 100,则数组为 Object[100]。并且 Object 数组中的元素又是以字段的
个数来分别保存 string 字符串的,即有多少个查询字段,就会有多少个 string 字符串。对于
这种存储方式,我的理解是,减少了 JVM 内存的碎片,可以减轻 GC 的压力。
真实场景中计算 PreparedStatement 对象:
    根据 select 字段的长度,我们就可以计算出每个 PreparedStatement 在内存中的大小。当
然,这里需要两个值,fetchsize(默认为 10),select 字段长度的总和。有了这两个值之后,就
可以精确的计算 PreparedStatement 的大小了。如,以下为某系统的 DUMP 统计结果,供参
考:
字段长度       PS 大小 (字节)    PS 大小/字段长度       说明

14151      1495269.376   105.6652799      因为 PS 对象中除了 char 数
                                          组,还有其它的变量,所以当
14151      1495269.376   105.6652799
                                          字段长度较小时  (即 char 数据
4412       479232        108.6201269      较小)
4412       479232        108.6201269      “PS 大小/字段长度”,这个比重
                                          就增大了。
4412       479232        108.6201269
3736       404172.8      108.1832976
3000       319488        106.496
2851       319488        112.0617327

3000       317440        105.8133333
2391       259072        108.3529904

2282       246784        108.1437336

1576       178176        113.0558376
1576       178176        113.0558376

725        84992         117.2303448

438        52224         119.2328767

401        50688         126.40399
使用图表关系来查看如下,可以发现这些点可以构成一个一元二次方程,偏差在 99.99%以
内:
计算内存和字段总长的比例:
    1495269.376 字节/fetchsize(50)/14151=2(约等于)

  实际这个比值,应该以真实环境的内存 DUMP 为准。知道了如何计算 PreparedStatement
对象的大小后,我们对于应用内存的控制可以做到心中有数了。   当然,如果应用内存不吃紧,
可以适当的加大这个值,以提升 SQL 的效率。

     补充一点:     如果 PSCache 中有 in 的 SQL,    如“select * from t where id in(:1,:2)”和“select
* from t where id in(:1,:2,:3)”,这在 PreparedStatementCache 中是两个不同的对象,因为
in 中条件个数的不同,可能导致 PreparedStatementCache 的命中率急剧下降。这里有一种
方法可以解决,详见后面第三章节的内容。
总结计算连接池占用 JVM 内存的公式及 PSCache 设置参考:
     以单个数据源为例(多个数据源累加即可),假设当前连接数为 a,每个连接的
PreparedStatementCache 为 b,并且“内存/字段总长”=c,字段平均的长度为 d

连接池占用的内存大小=a*b*c*d。
  最后,给出一个 PSCache 设置的参考意见,PSCache 最好能够覆盖 95%的应用 SQL,
然后可以在 95%这个值以上,再上浮 5-10 个。如:

SQL1: select * from text1; 应用调用占比重 45%
SQL2: select * from xxx where; 应用调用占比重 25%
SQL3: select * from xxx ; 应用调用占比重 15%
SQL4: select * from xxx; 应用调用占比重 10%
SQL5: select * from xxx; 应用调用占比重 3%
SQL6: select * from xxx;   应用调用占比重 1.3%
  我们统计,   SQL1,SQL2,SQL3,SQL4 占了 SQL 总访问量的 95%,因此我们可以将 PSCache
设置在 9-14 这个区间。

    以上测试及数据仅供参考,请以真实环境数据为准进行评估。

   记住,一定要给应用预留足够多的内存,即使 PSCache 设置在平时是合理的,但是因
为连接数可能会由于应用异常或者业务冲峰而陡增,按上面的计算公式,还是可能会导致
JVM 内存被撑爆。



附:
OCI 连接数据库
package com.alipay.db;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/*
 * jmap -dump:format=b,file=test.bin 4939
 */

public class Test_Tns {
     public static void main(String[] args) throws InstantiationException,
                IllegalAccessException, ClassNotFoundException {
          String dbUrl = "java:oracle:oci:@xxx";
          Connection conn = null;
          PreparedStatement stmt;
          ResultSet rs = null;
          String sql = "";
          try {
                Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();
                conn = DriverManager.getConnection(dbUrl, "zhoucang", "zhoucang");
                sql = "select * from test10000 where col_a=? ";
                stmt = conn.prepareStatement(sql);
                stmt.setString(1, "test");
                stmt.setFetchSize(10);
                System.out.println(stmt.getFetchSize());
                rs = stmt.executeQuery();
//              System.out.println(stmt.get);
                stmt.getMetaData();
while (rs.next()) {
                    //System.out.print(rs.getString("COL_A"));
               }
               System.out.println(stmt.getQueryTimeout());

               stmt.close();
               conn.close();

          } catch (SQLException e) {
               e.printStackTrace();
          }
     }
}



THIN 连接数据库

package com.alipay.db;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

/*
 * jmap -dump:format=b,file=test.bin 4939
 */

public class Test {
     public static void main(String[] args) throws InstantiationException,
                IllegalAccessException, ClassNotFoundException {

          String dbUrl = "jdbc:oracle:thin:@10.xx.xx.xx:1521:sid";
          Connection conn = null;
          PreparedStatement stmt;
          ResultSet rs = null;
          String sql = "";
          try {
                Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();
                conn = DriverManager.getConnection(dbUrl, "zhoucang", "zhoucang");
                sql = "select * from test10000 where col_a=? ";
                stmt = conn.prepareStatement(sql);
stmt.setString(1, "test");
            stmt.setFetchSize(100);
            System.out.println(stmt.getFetchSize());
            rs=stmt.executeQuery();
            while (rs.next()) {
                 System.out.print(rs.getString("COL_A"));
            }

            stmt.close();
            conn.close();

        } catch (SQLException e) {
             e.printStackTrace();
    }
}
}
调优篇-inlist 查询优化

  在前面一节 JBOSS 连接池调优 2-合理的设置 PreparedStatementCache 中我们已经学
会了使用 Memory Analyzer (MAT)工具来查看 JVM 的内存,并且看到了由于 inlist sql 的存
在,导致系统 PreparedStatementCache 中 SQL 的命中率仅为 40%左右。之前我们已经介
绍了 PreparedStatementCache 的作用,因此,命中率低可能引起的问题也显而易见(不清
楚的同学请参考前一章节的内容)。
  部分同学可能不明白什么是 in list 的 SQL,这里先说明一下,所谓 in list 的 SQL 就是指
使用了 in 来进行查询,绑定变量个数不确认的 SQL,如:

select * from test where id in (:1,:2,:3)
  对于这一类的查询,       由于 in 的查询条件中绑定变量个数的不同,     会导致 SQL 版本变多,
从而导致 PreparedStatementCache 的命中率下降。(因为 JBOSS/oracle 中都是以 SQL 文
本完全一致来匹配 PreparedStatementCache)。

  因为 PreparedStatementCache 在 MYSQL 中没有作用(根本原因是 MYSQL 不支持绑定
变量) 所以 MYSQL 不需要考虑 in list 在 PSCACHE 中的优化,
   ,                                       下面给出 oracle 中两种 in list
sql 的解决方案:

方案 1:使用 oracle 中的 pipelined function(可以认为是一种性能较好的存储过程),实
现如下函数:
create or replace type t_array
      as table of varchar2(4000)
/
--The PIPELINED keyword on line 4 allows this function
-- to work as if it were a table:

create or replace function
   str2varlist(p_string in varchar2)
   return t_array
   PIPELINED
   as
    v_str      VARCHAR2 (4000)             DEFAULT p_string || ',';
    v_n         number(11);
   begin
           LOOP
        v_n                           := INSTR (v_str, ',');
        EXIT WHEN (NVL (v_n, 0) = 0);
        pipe row(LTRIM (RTRIM (SUBSTR (v_str, 1, v_n - 1))));
        v_str                        := SUBSTR (v_str, v_n + 1);
    END LOOP;

     return;
  end;
/



      这个函数的作用是将以逗号分隔的字符串转换成一个 table,如下:

zhoucang@zhoucang>select * from TABLE(str2varlist('123,456,789,012,345'));

COLUMN_VALUE
---------------------------------------------------------------------------
123
456
789
012
345

5 rows selected.



      这样,每次我们的 SQL 执行时,只要传入一个固定长度的字符串即可,如:

select * from test where id in(select * from TABLE(str2varlist(:1)))
或者写成 join 的方式:
select /*+ use_nl(a b) ordered*/ b.* from
TABLE(str2varlist(:1)) a,test b
 where a.column_value = b.id



      这也不失为一种好方法,至少使用起来非常的方便。

方案 2:这是一种看起来很土的方法,同事建议的,真的很土,即固定 in 后面条件的个数,
每次传入不同的值,不足的可以使用一个不存在的值来进行补充。如:
select * from test where id in(:1,:2,:3,:4,:5,:6,:7,:8,:9,:10)

固定 10 个 ID 的查询,不足 10 个以-1 来补充(如果 ID 不为负数)。
  因为 oracle 中的 in 限制最多为 999 个,所以,极端情况下,也只需要固定 999 个 ID
即可。

如何选择方案 1 还是方案 2?
      专门测试了一下,测试过程主要逻辑如下:

      方案 1,创建 pipelined function,执行 20000 次查询:

      sql = " select /*+ use_nl(a b) ordered*/ b.* from
                       TABLE(str2varlist(:1)) a,test b
                           where a.column_value = b.id ";
for (int i = 0; i < 20000; i++) {
                      stmt1.setString(1,"1111,2222,3333");
}
     方案 2,固定绑定变量个数,执行 20000 次查询:

sql = "select * from test where id in(?,?,?,?,?,?,?,?..........) ";
     for (int i = 0; i < 20000; i++) {
                       stmt.setString(1, "1111");
                       stmt.setString(2, "2222");
                       stmt.setString(3, "3333");
                       stmt.setString(4, "-1");
                       stmt.setString(5, "-1");
                       stmt.setString(6, "-1");
                        ……
}


  测试主要以数据库的性能损耗为主要考虑指标(因为应用上带来的好处对我们来说诱惑
力实在太大了,基本上没什么坏处,性价比很高),测试结果如下(给出数据库中单个 SQL
主要的性能参考指标):


说明:                                  应用总           内 存 读          返 回   单     条    单条 SQL
                                     执行时           (块)            记录    SQL        响应时间
                                     间:秒                                CPU 时
                                                                        间(us)

3 个绑定变量,传 3 个 id                     38            9              3     75.84365   75.84365
20 个绑定变量,传 3 个 id                    39            10.06          3     94.48995   94.48995

100 个绑定变量, 传 3 个 id                  43            10.05955       3     112.8007   112.8007
PPLINE,3 个 id                        41            9.01435        3     149.5074   149.5074

100 个绑定变量, 10 个 id
          传                          53            31.05955       10    220.5576   220.55765
                                                                        5
10 个绑定变量,传 10 个 id                   43            30             10    174.7577   174.7577
PPLINE,10 个 id                       51            30.03795       10    358.4637   358.46375
                                                                        5
PPLINE 方案(方案 1):
1. CPU:会增加 CPU 的消耗 1 倍左右,原因是数据库需要对字符串进行解析。
2. IO:对 IO 基本无损耗。
3. 响应时间:数据库响应时间增加一倍。
固定绑定变量个数(方案 2) :
1. CPU:CPU 的消耗,增加幅度在 20%~60%,之间,具体因 in 的个数而定。
2. IO:增加了一个逻辑读,  这一个逻辑读是由于查询-1 时产生的,  主要是由于索引 branch
节点查询引起,由于 root 节点一定需要查询,索引一般为 3-4 层,可以认为,这个逻辑读
开销在 1-3 个左右,基本上可以认为在 1 个左右(这个可以结合 B 树索引结构得知)。并
且这个 branch 节点非常热,IO 开销肯定是逻辑读。
3. 响应时间: 增加幅度在 20%~60%,之间,具体因 in 的个数而定。
显然,选择方案 2 会更加节省数据库的资源。
   从测试结果可以看出,         当传入 3 个 ID 的时候,使用 100 个绑定变量和 20 个绑定变量,
还是存在一定的差异。因此,对于 in 的 SQL 我们可以再进行分级,根据执行频率,评估适
当的多给出几个版本,如:
   Id=:1 :适用于 1 个 ID 的查询
   Id in(20 个绑定变量) :适用于 ID 个数据在 2-20 个之前的查询。
   Id in (999 绑定变量) :适用于 ID 个数在 20 个以上的查询。
调优篇-合理设置连接数的 min 值和 max 值

  前面两节 jboss 连接池的启动及 prefill 参数配置和 JBOSS 连接池的初始化及关闭中,
已经详细解释了 JBOSS 连接池的内部管理机制。本文就实际使用中的一些经验,分享一下
连接池 MIN 值和 MAX 值设置上的一些经验,最后再补充下关于 blocking-timeout-millis 设
置的一些建议。
连接池设置不合理可能导致的后果:
  连接池的 MIN 数和 MAX 数,看似很简单的两个参数,其实对于应用和数据库的影响,
并不是那么的简单。我们先来看看连接数设置不合理可能产生的后果:

   1. 连接池 MIN 设置过小,应用业务量突增,或者启动时可能产生连接风暴。

   2. 连接池 MIN 值设置过大,会造成资源的浪费,主要包括数据库和应用内存,连接数
      的浪费,同时连接池 MIN 值设置过大,也会导致连接被频繁的创建和销毁。这是由
      连接池的工作机制决定的。

   3. 连接池 MAX 值设置过大。在极端情况下,当应用发生异常时,会导致连接数被撑
      到 MAX 值,有可能导致数据库连接数被耗尽,从而导致正常的业务受到影响。当
      连接数被撑到 MAX 值,在获取连接超时的时候,应用的线程池也有可能受到影响,
      这是一系列的连锁反应。

以上是连接池 MIN 值和 MAX 值设置最主要的 3 点影响。其中,影响最大的并且最难配置的
是第 2 点,即连接数的 MIN 值设置。

设置连接池的 min-pool-size:
   一般来说,我们可以按照业务高峰时期的压力来估算连接数。比如,在高峰时期,每秒
有 5000 个并发请求,每个请求的处理时间为 2ms,则每秒总共需要 5000*2ms=10s 的连
接处理时间。因此,连接数的 MIN 值我们可以设置为 10 个(适当上浮 1-2 个),这样就能
基本上解决连接风暴的问题了。当然在业务低峰时期,10 个连接数的设置可能偏大了点,
所以这里对于连接数的 MIN 值设置也做了一个权衡,保守起见,我们需要以高峰时需要的
正常连接数来设置 MIN 值,以避免连接风暴的发生。因为这样做是最保险的,当然,这带
了一些额外的代价,会牺牲掉部分的数据库和应用的资源。
有时候,我们对于业务的估计可能并不准备,如业务刚上线,对于连接数不能很好的评估,
这个时候我们可以对连接池的 MIN 值进行调优,共有两种方式,如下:

通过 ORACLE 监听日志对 min-pool-size 调优:
     对 于 ORACLE 数 据 库 , 我 们 可 以 使 用 数 据 库 的 监 听 日 志 来 进 行 调 优 , 即 通 过
listener.log(数据库监听日志)进行调优。收集以下几个信息:
     1. 根据监听日志,我们可以得到某个应用集群 A 在 2 小时内创建的连接总数,假设统
计一共为 N 个。
     2. 假设 A 集群在 2 小时内的连接数维持在一个值:M。          (后面会对这个 M 进行说明)
     3. A 集群有 S 台应用服务器,假设应用服务器负载均衡。
我们知道,       连接池 IDLE 清理是 15 分钟执行一次的,   每次清理空闲时间超过 30 分钟的连接。
所以,2 小时内,每台应用平均创建的连接数为:N/4S 个。
假设:
1. M=min 连接数,则我们的应用可以下调的 MIN 连接数为 N/4S 个,即设置为 M-N/4S。
(由连接池的原理可以知道,这些新创建的连接完全都是空闲的连接,只是由于配置了 MIN
值较大而产生的,实际上这些连接一直处于空闲状态)
   2.假设 M 值>min 连接数,则需要将 min 值设置为 M。
   3.假设 M<min,这不可能发生。
之前对于某核心系统做的一次调优,        上线后效果很不错,参考 一个生产库的 JBOSS 连接池调
整优化及分析
通过监控 JBOSS 连接池对 min-pool-size 的调优:
   连接池默认参数及获取连接池中相关的统计信息一节中,         我们提到了连接池中有一些方
法可以获取连接数的信息,其中有三个方法:
//获取连接数,内部执行:return created - destroyed;即所有创建的连接数-所有销毁的连接数
   public int getConnectionCount()
   {
      return connectionCounter.getCount();
   }

//创建连接的总数
   public int getConnectionCreatedCount()
   {
       return connectionCounter.getCreatedCount();
   }
     //连接销毁总数
   public int getConnectionDestroyedCount()
   {
       return connectionCounter.getDestroyedCount();
   }



  通过监控这三个参数我们即可进行连接数的调优,具体的调优方式和方法 1 一致,只是
我们获取创建的连接数,通过监控连接池中的状态获取。如下图所示,我们监控了
10:00-12:00 数据库的连接数:
通过上面的图可以知道,我们的连接数 MIN 值设置过大了(上图是对于单台应用的连接
池监控)。因为连接池始终保持在 MIN 值 10 个,说明连接数过剩,而我们的连接池不停的
创建,销毁。由于定时任务是 15 分钟启动 1 次的,所以我们在图上可以看到每隔 15 分钟
说有 1 个连接被销毁。在 30 分钟平均有 2 个连接被销毁。根据同样的 ORACLE 监听日志的
调优算法:

我们的连接数可以被设置为 M-N/4S,即 10-8/4=8 个。
  这是二种连接池 MIN 调整的方案。其实原理是完全一样的,只是获取数据的两种方式
不同,对于有条件的应用,建议监控 JBOSS 的连接池进行调优。这样,连接池的一切状态,
都会尽在掌握之中,调优也会变的更加简单高效。

设置连接的 max-pool-size:
  其实我们在调整连接池 MIN 值的时候,已经是按业务的高峰时期进行调整。所以 MAX
值调整的意义其实并不大,我们只要将 MAX 值设置在一个安全的阀值即可。保证不超过数
据库的连接数,同时又保证在业务偶尔分布不均匀时,应用也能够获取连接,防止连接池取
不到的情况。

    我们经常会看到应用程序报错:“No ManagedConnections available within
configured blocking timeout xx [ms]”,这其实是由于连接池达到最大值了,没有可用
的连接的时候产生的。(参考:JBOSS 连接池 4-从连接池中获取连接及返还连接)。产生
这个问题,      最根本的原因往往并不是连接数不够用,          我们应该首先看看数据库的响应和应用
DAO 的响应时间。有时候因为 SQL 走不上索引,或者数据库响应较慢,会增加业务占用连
接的时间(即处理时间),这种情况下,加大 MAX 连接数可以从一定程序上缓解问题,但
是不能解决根本的问题。
blocking-timeout-millis 的设置:
    另外一点建议是,将 blocking-timeout-millis 参数设置的尽量小一点,这个参数是应用
getconnection 时的超时时间,只在连接数达到 MAX 值时才会起作用,因为连接数没到达
MAX 值,这个获取连接是一个很快的操作,内部仅仅是执行一个获取信号量的操作。为了
尽量的减小在获取不到连接而等待超时对应用线程池的阻塞,我们需要将
blocking-timeout-millis 参数设置为一个较小的值即可,  这样,当连接池中无可用的连接时,
由于超时时间减小了,             可以避免由于线程池共享造成对其它正常业务的影响,   特别是一个应
用连接多个数据源时,这个值的设置尤其重要。

总结:
  实际我们对于连接池的调优,需要从连接池的原理入手,只有理解了原理,才能够更好
的将连接池控制在我们所想要的一个状态。因为连接池的不合理设置所产生的问题,我们在
上面吃的亏很多。就因为一个小小的连接池出了问题,现在看来实在有点得不偿失。

    真实的场景中,特别是基于数据库的水平拆分,或者读写分离的场景中,当一个应用集
群需要连接多个数据源时,我们尤其需要考虑连接池的这几个参数:
prepared-statement-cache-size,min-pool-size, max-pool-size, blocking-timeout-millis。这
几参数设置的利害关系,需要仔细的考虑。另外,我们还需要重点考虑 2 个点:

 1. 对于物理库-逻辑库中,数据源共享和不共享的取舍。
 2. prepared-statement-cache-size,min-pool-size 值的设置,数据源个数,fetchsize
    的设置 直接影响到 JVM 内存的使用。
调优篇-合理的设置 fetchsize

    在前面的几节中,我们经常会提到一个词“Fetchsize”,并且在《JBOSS 连接池调优 2-
合理的设置 PreparedStatementCache》一节中研究了 fetchsize 对应用内存的影响。网上关
于 fetchsize 的文章不是很多,很难对这个参数有一个全面的了解,确实,如果从开发的角
度来理解 fetchsize,确实是存在一些困难的。那么到时什么是 fetchsize?fetchsize 对内存
和性能到底有什么影响呢?我在前段日子作了不少测试和研究,                  下面将自己的研究成果和大
家分享一下。
什么是 fetchsize?

Oracle 中的 fetchsize:
     先来简单解释一下,当我们执行一个 SQL 查询语句的时候,需要在客户端和服务器端
都打开一个游标,并且分别申请一块内存空间,作为存放查询的数据的一个缓冲区。这块内
存区,存放多少条数据就由 fetchsize 来决定,同时每次网络包会传送 fetchsize 条记录到客
户端。应该很容易理解,如果 fetchsize 设置为 20,当我们从服务器端查询数据往客户端传
送时,   每次可以传送 20 条数据, 但是两端分别需要 20 条数据的内存空闲来保存这些数据。
fetchsize 决定了每批次可以传输的记录条数,但同时,也决定了内存的大小。这块内存,
在 oracle 服务器端是动态分配的(大家可以想想为什么)。而在客户端(JBOSS),PS 对
象会存在一个缓冲中(LRU 链表),也就是说,这块内存是事先配好的,应用端内存的分配
在 conn.prepareStatement(sql)或都 conn.CreateStatement(sql)的时候完成。

   在 java 程序中,我们会执行以下代码:

//打开游标,执行查询,但是并不获取任何的数据,网络上没有数据的传输。
rs = stmt.executeQuery();
//获取具体的数据,网络一般每次传输 fetchsize 条数据。
while (rs.next()){
}
MYSQL 中的 fetchsize:
     MYSQL 的 preparestament 基本上不占用内存,为什么呢?因为 MYSQL 并不需要象
oracle 那样的一块内存来保存结果集缓冲区,为什么不需要缓冲区,其中根本的原因是由
MYSQL 的通讯方式决定的。

  MYSQL 客户端/服务器协议是半双工的,即 MYSQL 只能在给定的时间,发送或接受数
据,但不能同时发送和接收。所以,MYSQL 在数据查询结果集传送的时候,需要一次性将
数据全部传送到客户端,在客户数据接收完之后,释放相关的锁等资源。因为这种半双工的
通讯方式,所以 MYSQL 不需要客户端的游标,但是客户端 API 通过把结果取到内存中,可
以模拟游标的操作。所以,我们可以在 JAVA 程序中,可以象 ORACLE 那样来实现 MYSQL
的访问。

如何设置 fetchsize?
  Fetchsize 可以在任何一层进行设置 ,ORACLE JDBC 驱动默认的 FETCHSIZE 为 10。
一般为了方便,我们会在数据源层面上来设置 fetchsize。
语句级别的设置:
     我们可以在 jdbc 中调用 Preparedstatement .setFetchSize()的进行设置:

stmt = conn.prepareStatement(sql);
stmt.setFetchSize(50);


     也可以在 Ibatis, hibernate 等框架上直接针对某个语句进行设置:

< select id="getAllProduct"> select * from employee < /select>


数据源中的全局设置:
  JBOSS 连接中设置:

< connection-property name="defaultRowPrefetch">50


Fetchsize 的核心源码:

     可以在 JDBC 驱动类 Oracle.jdbc.driver.OracleStatment 中找到这个方法,
setPrefetchInternal 方法中传入的默认值为 0,伪代码如下:

void setPrefetchInternal(int paramInt){
            if (paramInt < 0) {
                    DatabaseError.throwSqlException(68, "setFetchSize");
               }
               //获取连接池中的 DefaultRowPrefetch 属性
           else if (paramInt == 0) {
                    paramInt = this.connection.getDefaultRowPrefetch();
            }
         if (paramInt == this.defaultRowPrefetch)
                  return;
              this.defaultRowPrefetch = paramInt;

        if ((this.currentResultSet == null) || (this.currentResultSet.closed)) {
                this.rowPrefetchChanged = true;
          }
}
Fetchsize 对性能影响的测试:
    空查询结果集的测试:
     查询的表一共有 300 条记录,测试中查询的结果集为空,执行的是全表扫描。

SQL> select count(*) from test10000;

   COUNT(*)
----------
Jboss连接池原理及调优
Jboss连接池原理及调优
Jboss连接池原理及调优
Jboss连接池原理及调优
Jboss连接池原理及调优
Jboss连接池原理及调优

More Related Content

What's hot

Hash map导致cpu100% 的分析
Hash map导致cpu100% 的分析Hash map导致cpu100% 的分析
Hash map导致cpu100% 的分析wang hongjiang
 
线程与并发
线程与并发线程与并发
线程与并发Tony Deng
 
Java华为面试题
Java华为面试题Java华为面试题
Java华为面试题yiditushe
 
OpenResty 项目模块化最佳实践
OpenResty 项目模块化最佳实践OpenResty 项目模块化最佳实践
OpenResty 项目模块化最佳实践Orangle Liu
 
Exodus重构和向apollo迁移
Exodus重构和向apollo迁移Exodus重构和向apollo迁移
Exodus重构和向apollo迁移wang hongjiang
 
深入剖析Concurrent hashmap中的同步机制(下)
深入剖析Concurrent hashmap中的同步机制(下)深入剖析Concurrent hashmap中的同步机制(下)
深入剖析Concurrent hashmap中的同步机制(下)wang hongjiang
 
Shell,信号量以及java进程的退出
Shell,信号量以及java进程的退出Shell,信号量以及java进程的退出
Shell,信号量以及java进程的退出wang hongjiang
 
Effective linux.1.(commandline)
Effective linux.1.(commandline)Effective linux.1.(commandline)
Effective linux.1.(commandline)wang hongjiang
 
test
testtest
testxieyq
 
The comet technology on Jetty
The comet technology on Jetty The comet technology on Jetty
The comet technology on Jetty wavefly
 
OpenEJB - 另一個選擇
OpenEJB - 另一個選擇OpenEJB - 另一個選擇
OpenEJB - 另一個選擇Justin Lin
 
使用Lua提高开发效率
使用Lua提高开发效率使用Lua提高开发效率
使用Lua提高开发效率gowell
 
深入淺出 Web 容器 - Tomcat 原始碼分析
深入淺出 Web 容器  - Tomcat 原始碼分析深入淺出 Web 容器  - Tomcat 原始碼分析
深入淺出 Web 容器 - Tomcat 原始碼分析Justin Lin
 
Reactive X 响应式编程
Reactive X 响应式编程Reactive X 响应式编程
Reactive X 响应式编程Jun Liu
 
Jetty(version 8)核心架构解析
Jetty(version 8)核心架构解析Jetty(version 8)核心架构解析
Jetty(version 8)核心架构解析wavefly
 
Mysql展示功能与源码对应
Mysql展示功能与源码对应Mysql展示功能与源码对应
Mysql展示功能与源码对应zhaolinjnu
 
Effective linux.2.(tools)
Effective linux.2.(tools)Effective linux.2.(tools)
Effective linux.2.(tools)wang hongjiang
 
Cassandra运维之道(office2003)
Cassandra运维之道(office2003)Cassandra运维之道(office2003)
Cassandra运维之道(office2003)haiyuan ning
 

What's hot (20)

Hash map导致cpu100% 的分析
Hash map导致cpu100% 的分析Hash map导致cpu100% 的分析
Hash map导致cpu100% 的分析
 
Aswan&hump
Aswan&humpAswan&hump
Aswan&hump
 
Exodus2 大局观
Exodus2 大局观Exodus2 大局观
Exodus2 大局观
 
线程与并发
线程与并发线程与并发
线程与并发
 
Java华为面试题
Java华为面试题Java华为面试题
Java华为面试题
 
OpenResty 项目模块化最佳实践
OpenResty 项目模块化最佳实践OpenResty 项目模块化最佳实践
OpenResty 项目模块化最佳实践
 
Exodus重构和向apollo迁移
Exodus重构和向apollo迁移Exodus重构和向apollo迁移
Exodus重构和向apollo迁移
 
深入剖析Concurrent hashmap中的同步机制(下)
深入剖析Concurrent hashmap中的同步机制(下)深入剖析Concurrent hashmap中的同步机制(下)
深入剖析Concurrent hashmap中的同步机制(下)
 
Shell,信号量以及java进程的退出
Shell,信号量以及java进程的退出Shell,信号量以及java进程的退出
Shell,信号量以及java进程的退出
 
Effective linux.1.(commandline)
Effective linux.1.(commandline)Effective linux.1.(commandline)
Effective linux.1.(commandline)
 
test
testtest
test
 
The comet technology on Jetty
The comet technology on Jetty The comet technology on Jetty
The comet technology on Jetty
 
OpenEJB - 另一個選擇
OpenEJB - 另一個選擇OpenEJB - 另一個選擇
OpenEJB - 另一個選擇
 
使用Lua提高开发效率
使用Lua提高开发效率使用Lua提高开发效率
使用Lua提高开发效率
 
深入淺出 Web 容器 - Tomcat 原始碼分析
深入淺出 Web 容器  - Tomcat 原始碼分析深入淺出 Web 容器  - Tomcat 原始碼分析
深入淺出 Web 容器 - Tomcat 原始碼分析
 
Reactive X 响应式编程
Reactive X 响应式编程Reactive X 响应式编程
Reactive X 响应式编程
 
Jetty(version 8)核心架构解析
Jetty(version 8)核心架构解析Jetty(version 8)核心架构解析
Jetty(version 8)核心架构解析
 
Mysql展示功能与源码对应
Mysql展示功能与源码对应Mysql展示功能与源码对应
Mysql展示功能与源码对应
 
Effective linux.2.(tools)
Effective linux.2.(tools)Effective linux.2.(tools)
Effective linux.2.(tools)
 
Cassandra运维之道(office2003)
Cassandra运维之道(office2003)Cassandra运维之道(office2003)
Cassandra运维之道(office2003)
 

Recently uploaded

日本姫路独协大学毕业证制作/修士学位记多少钱/哪里可以购买假美国圣何塞州立大学成绩单
日本姫路独协大学毕业证制作/修士学位记多少钱/哪里可以购买假美国圣何塞州立大学成绩单日本姫路独协大学毕业证制作/修士学位记多少钱/哪里可以购买假美国圣何塞州立大学成绩单
日本姫路独协大学毕业证制作/修士学位记多少钱/哪里可以购买假美国圣何塞州立大学成绩单kathrynalvarez364
 
澳洲圣母大学毕业证制作/加拿大硕士学历代办/购买一个假的中央警察大学硕士学位证书
澳洲圣母大学毕业证制作/加拿大硕士学历代办/购买一个假的中央警察大学硕士学位证书澳洲圣母大学毕业证制作/加拿大硕士学历代办/购买一个假的中央警察大学硕士学位证书
澳洲圣母大学毕业证制作/加拿大硕士学历代办/购买一个假的中央警察大学硕士学位证书kathrynalvarez364
 
布莱德福德大学毕业证制作/英国本科学历如何认证/购买一个假的香港中文大学专业进修学院硕士学位证书
布莱德福德大学毕业证制作/英国本科学历如何认证/购买一个假的香港中文大学专业进修学院硕士学位证书布莱德福德大学毕业证制作/英国本科学历如何认证/购买一个假的香港中文大学专业进修学院硕士学位证书
布莱德福德大学毕业证制作/英国本科学历如何认证/购买一个假的香港中文大学专业进修学院硕士学位证书kathrynalvarez364
 
哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制
哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制
哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制jakepaige317
 
EDUC6506_ClassPresentation_TC330277 (1).pptx
EDUC6506_ClassPresentation_TC330277 (1).pptxEDUC6506_ClassPresentation_TC330277 (1).pptx
EDUC6506_ClassPresentation_TC330277 (1).pptxmekosin001123
 
EDUC6506(001)_ClassPresentation_2_TC330277 (1).pptx
EDUC6506(001)_ClassPresentation_2_TC330277 (1).pptxEDUC6506(001)_ClassPresentation_2_TC330277 (1).pptx
EDUC6506(001)_ClassPresentation_2_TC330277 (1).pptxmekosin001123
 
日本九州齿科大学毕业证制作🚩定制本科卒业证书🚩哪里可以购买假美国西南基督复临安息日会大学成绩单
日本九州齿科大学毕业证制作🚩定制本科卒业证书🚩哪里可以购买假美国西南基督复临安息日会大学成绩单日本九州齿科大学毕业证制作🚩定制本科卒业证书🚩哪里可以购买假美国西南基督复临安息日会大学成绩单
日本九州齿科大学毕业证制作🚩定制本科卒业证书🚩哪里可以购买假美国西南基督复临安息日会大学成绩单jakepaige317
 
中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,
中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,
中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,Xin Yun Teo
 
1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...
1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...
1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...黑客 接单【TG/微信qoqoqdqd】
 
educ6506presentationtc3302771-240427173057-06a46de5.pptx
educ6506presentationtc3302771-240427173057-06a46de5.pptxeduc6506presentationtc3302771-240427173057-06a46de5.pptx
educ6506presentationtc3302771-240427173057-06a46de5.pptxmekosin001123
 

Recently uploaded (10)

日本姫路独协大学毕业证制作/修士学位记多少钱/哪里可以购买假美国圣何塞州立大学成绩单
日本姫路独协大学毕业证制作/修士学位记多少钱/哪里可以购买假美国圣何塞州立大学成绩单日本姫路独协大学毕业证制作/修士学位记多少钱/哪里可以购买假美国圣何塞州立大学成绩单
日本姫路独协大学毕业证制作/修士学位记多少钱/哪里可以购买假美国圣何塞州立大学成绩单
 
澳洲圣母大学毕业证制作/加拿大硕士学历代办/购买一个假的中央警察大学硕士学位证书
澳洲圣母大学毕业证制作/加拿大硕士学历代办/购买一个假的中央警察大学硕士学位证书澳洲圣母大学毕业证制作/加拿大硕士学历代办/购买一个假的中央警察大学硕士学位证书
澳洲圣母大学毕业证制作/加拿大硕士学历代办/购买一个假的中央警察大学硕士学位证书
 
布莱德福德大学毕业证制作/英国本科学历如何认证/购买一个假的香港中文大学专业进修学院硕士学位证书
布莱德福德大学毕业证制作/英国本科学历如何认证/购买一个假的香港中文大学专业进修学院硕士学位证书布莱德福德大学毕业证制作/英国本科学历如何认证/购买一个假的香港中文大学专业进修学院硕士学位证书
布莱德福德大学毕业证制作/英国本科学历如何认证/购买一个假的香港中文大学专业进修学院硕士学位证书
 
哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制
哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制
哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制
 
EDUC6506_ClassPresentation_TC330277 (1).pptx
EDUC6506_ClassPresentation_TC330277 (1).pptxEDUC6506_ClassPresentation_TC330277 (1).pptx
EDUC6506_ClassPresentation_TC330277 (1).pptx
 
EDUC6506(001)_ClassPresentation_2_TC330277 (1).pptx
EDUC6506(001)_ClassPresentation_2_TC330277 (1).pptxEDUC6506(001)_ClassPresentation_2_TC330277 (1).pptx
EDUC6506(001)_ClassPresentation_2_TC330277 (1).pptx
 
日本九州齿科大学毕业证制作🚩定制本科卒业证书🚩哪里可以购买假美国西南基督复临安息日会大学成绩单
日本九州齿科大学毕业证制作🚩定制本科卒业证书🚩哪里可以购买假美国西南基督复临安息日会大学成绩单日本九州齿科大学毕业证制作🚩定制本科卒业证书🚩哪里可以购买假美国西南基督复临安息日会大学成绩单
日本九州齿科大学毕业证制作🚩定制本科卒业证书🚩哪里可以购买假美国西南基督复临安息日会大学成绩单
 
中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,
中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,
中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,
 
1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...
1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...
1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...
 
educ6506presentationtc3302771-240427173057-06a46de5.pptx
educ6506presentationtc3302771-240427173057-06a46de5.pptxeduc6506presentationtc3302771-240427173057-06a46de5.pptx
educ6506presentationtc3302771-240427173057-06a46de5.pptx
 

Jboss连接池原理及调优

  • 1. JOBSS 连接池原理及调优 支付宝-樊振华 邮箱: sxfzh1987@gmail.com 旺旺:周仓 QQ: 164038407 Blog: http://www.dbafree.net
  • 2. 内容列表 原理篇: 连接池的启动及 prefill 参数 连接池的初始化及关闭 连接的获取及返还和异常连接销毁 调优篇: 如何防止连接风暴 PreparedStatementCache 参数的作用及原理 合理的设置 PreparedStatementCache inlist 查询优化 合理设置连接数的 min 值和 max 值 合理的设置 fetchsize 调优总结: 调优可能带来的好处 附录: 连接池默认参数及获取连接池中相关的统计信息
  • 3. 原理篇-连接池的启动及 prefill 参数 jboss 连接池的启动,主要就是一些对象的初始化及一个 prefill 的过程。 prefill 参数:在 ds.xml 中有一个参数,叫 prefill,这个值可以设置为 true|false,默认为 false,这个参数在 JBOSS4.0.5 版本以上才能被支持。这个参数的设置,决定了连接池在启 动时是否会初始化 min 连接数(最小连接数)。如果设置为 true,连接池在启动的时候会进行 min 值连接数的初始化,但是在应用同时启动时可能导致连接风暴;如果设置为 false,则在 第一次 getconnection 时,才启动创建 min 连接数。 本节主要研究了 JBOSS 连接池在启动时究竟做了些什么操作,带着好奇心,我们查看 JBOSS 源码,其实并不难,可以看到在连接池的启动过程中主要涉及到两个类, InternalManagedConnectionPool 和 PoolFiller。 PoolFiller 类:在 jboss 启动时即开始 fillerThread 线程的 wait,当执行 fillPool 操作时 被唤醒,fillPool 操作主要的作用是将 JBOSS 的连接池填充到 min 值。当连接池填充到 min 值之后,线程继续 wait。 InternalManagedConnectionPool:这个类是 JOBSS 连接池管理的核心类,后面几 节主要围绕这个类来展开。 InternalManagedConnectionPool 类的构造方法和连接池的配置参数紧密相关,如下: protected InternalManagedConnectionPool(ManagedConnectionFactory mcf, ConnectionListenerFactory clf, Subject subject, ConnectionRequestInfo cri, PoolParams poolParams, Logger log) { //mcf 是连接管工厂 this.mcf = mcf; //连接监听工厂 this.clf = clf; //默认的 subject defaultSubject = subject; //连接请求信息 defaultCri = cri; //连接池参数,主要方法见下面的函数,见附录。 this.poolParams = poolParams; this.maxSize = this.poolParams.maxSize; this.log = log; this.trace = log.isTraceEnabled(); //可以使用的连接事件监听器初始化。 cls = new ArrayList(this.maxSize); /*创建一个对应连接事件监听器的信号集,用于连接的获取。 每次获取连接或者创建连接之前,都需要获取信号量, 当没有可用的信号量时,表示连接已经到达 max 值。 */ permits = new FIFOSemaphore(this.maxSize); /*
  • 4. 判断 jboss 配置文件中的 prefill 设置,默认为 false。 如果设置为 true,则将本连接池加入到一个临时 pool(LinkedList)的最后, 加入的方式是串行的(线程安全)。 */ if(poolParams.prefill) { //fillPool 主要执行了一个 fillToMin 的方法,即将连接池中的连接,填充到 min 值。 PoolFiller.fillPool(this); } } 这就是 InternalManagedConnectionPool 的实例化, 很简单,最后一步判断如果参数 prefill 设置为 flase,则没有其它事了,整个过程结束。否则,执行一个 fillPool 的操作。 先来看一下与 PoolFiller 类, PoolFiller 在构造函数中即完成 fillerThread 线程的启动, 线 程启动后由于 pools 是空的,所以本线程一直处于 wait 状态,当执行 PoolFiller.fillPool(this); 操作时,pools 中首先会增加了一个连接池对象(见 internalFillPool 函数) ,然后幻醒 fillerThread 线程,fillerThread 线程被唤醒后,因为此时 pools 已经不为 null 了,线程开始初 始化 pools 队列中的所有的连接池对象 (在并发情况下, 可能有多个连接池对象需要初始化, 但是连接初始化的过程,是一个线程安全的操作) ,线程唤醒后主要的操作是将连接池的连 接数填充至 min 值,填充由 fillToMin 函数来执行。 当 pools 中所有的连接池对象都填充到 min 值时,此时 pools 也被清空了,fillerThread 线程继续进行 wait,直到下一次 fillPool 时被唤醒。在连接池启动时只会被 fillPool 一次,其 它的 fillPool 操作还会在三种情况下发生: 1.后面的章节会讲到在 removeTimedOut(idle 超时清理时,还会调会 fillPool 操作-即 清理 Idle 连接后,发现小于 min 值,又需要进行 fillPool 操作)。 2.在 valitionconnection 后,紧接着执行 fillToMin。(同上) 3.当 prefill 设置为 false, 即连接池实例化时没有被 fill 到 min 值, 在第一次 getconnection 时,触发这个 fillPool 操作。
  • 5. 具体如下图所示: 相应代码如下: public class PoolFiller implements Runnable { private final LinkedList pools = new LinkedList(); private final Thread fillerThread; private static final PoolFiller filler = new PoolFiller(); public static void fillPool(InternalManagedConnectionPool mcp) { filler.internalFillPool(mcp); } public PoolFiller () { fillerThread = new Thread(this, "JCA PoolFiller"); fillerThread.start(); } public void run() { ClassLoader myClassLoader = getClass().getClassLoader(); Thread.currentThread().setContextClassLoader(myClassLoader); //keep going unless interrupted while (true) { try
  • 6. { InternalManagedConnectionPool mcp = null; //keep iterating through pools till empty, exception escapes. while (true) { synchronized (pools) { //取出需要处理的连接池,即需要 fillToMin 的连接池 mcp = (InternalManagedConnectionPool)pools.removeFirst(); } //如果没有需要处理的连接池了,则跳出 while 循环,进行线程的 wait。 if (mcp == null) break; //将连接池 mcp 填充至 min 值。 mcp.fillToMin(); } } catch (Exception e) { } try { synchronized (pools) { /*如果没有需要处理的连接池了,则跳出 while 循环, 进行线程的 wait,等待下一次执行 internalFillPool 函数唤醒 filltoMin */ while(pools.isEmpty()) { pools.wait(); } } } catch (InterruptedException ie) { return; } } } private void internalFillPool(InternalManagedConnectionPool mcp)
  • 7. { synchronized (pools) { /* 将连接池对象加入到 pools 临时队列里, 这里的 pools 只是一个临时队列,用于进行 fillToMin 的操作。 */ pools.addLast(mcp); //notify()会触发器 run pools.notify(); } } } fillToMin 函数如下: public void fillToMin() { while (true) { /* 获取一个信号量 - 防止在连接池快满时,产生竞争 当所有的连接都被 checkd out(即全部被占用)时,避免不需要的 fill 检查。 */ try { //获取信号量,超时时间为 jboss 数据源配置文件的 time out if (permits.attempt(poolParams.blockingTimeout)) { try { //判断连接池是否已经 shutdown?如果已经 shutdown,则直接返回。 if (shutdown.get()) return; // 判断连接池中的连接是否已经达到 min 值,如果已经达到,则直接 返回。 if (getMinSize() - connectionCounter.getGuaranteedCount() <= 0) return; // 创建一个连接去填充连接池。每次创建一个,因为这个是死循环。 try {
  • 8. ConnectionListener cl = createConnectionEventListener(defaultSubject, defaultCri); synchronized (cls) { if (trace) log.trace("Filling pool cl=" + cl); cls.add(cl); } } catch (ResourceException re) { log.warn("Unable to fill pool ", re); return; } } finally { //释放信号量 permits.release(); } } } catch (InterruptedException ignored) { log.trace("Interrupted while requesting permit in fillToMin"); } } } 附: ds.xml 参数的解释: < prefill > - 这个参数决定是否填充连接池到最小连接数。 如果连接池不支持这个功能, 将在日志文件中看到一个警告,默认设置为 false。 < blocking-timeout-millis > - 当所有的连接都被占用(即被使用)时,为了从连接池中等 待一个可用的连接而消耗的时间。默认值设置为 5 秒。 仔细研究源码,可以获得更加细节的细节的知识,本块知识点只是介绍连接池的启动, 过程很简单。
  • 9. 原理篇-连接池的初始化及关闭 前一节已经讲了 jboss 连接池的启动过程及 prefill 参数配置。 在启动完成之后,连接池紧接着会进行一个 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) {
  • 10. 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");
  • 11. 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(); )
  • 12. ((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;
  • 13. //本连接池下一次可能清理的时间。 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() {
  • 14. //合建一个清理队列 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)
  • 15. 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。
  • 16. 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)
  • 17. { 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); } } }
  • 18. } 这个 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; }
  • 19. 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);
  • 20. 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); } }
  • 21. 关于 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。
  • 22. 在销毁空闲的连接(IdleRemover)和无效的连接(ConnectionValidator)后,都会执行一个 prefill 的操作,将连接池中的连接数填充到 min 值,所以,对于连接池 min 需要合理的进行 设置,如果 min 设置过大,JBOSS 会将连接不断的进行销毁->创建->销毁->创建…(idle 线程对空闲连接销毁,销毁后小于 min 值,然后马上又创建, 新创建的连接处于空闲状态, 于是又被销毁…) 总之,我们需要合理的设置连接池 min 值,对于某些系统来说,数据库的连接资源是 很昂贵的。 前段日子在公司的核心系统上优化的一个连接池,主要也是对于 min 值的优化:一个 生产库的 JBOSS 连接池调整优化及分析。对于连接数的调优,后面会专门整理一篇文章。
  • 23. 原理篇-连接的获取及返还和异常连接销毁 前面二节已经介绍了 JBOSS 的启动及初始化。在初始化的时候,会加载 IdleRemove 线 程和 validateConnections 线程。IdleRemove 线程默认 15 分钟启动一次用于空闲连接的清 理;validateConnections 线程默认 10 分钟启动一次,用于连接的定时校验,及时清理无效的 连接。 本节介绍 JBOSS 的两个最重要的方法:getconnection(获取连接)和 returnConnection(释 放连接) 。最后,补充介绍一下 JBOSS 中对于异常连接是如何销毁的。 当应用需要进行业务处理时,首先会执行一个 getConnection 的操作,用于从连接池中获取 连接, 当业务处理完成后, 需要把连接放回到连接池中, 执行一个 returnConnection 的操作。 下面先看一下 getConnection 的源码: //getConnection 方法返回的值是一个连接监听对象 ConnectionListener public ConnectionListener getConnection(Subject subject, ConnectionRequestInfo cri) throws ResourceException { subject = (subject == null) ? defaultSubject : subject; //获取连接信息 cri = (cri == null) ? defaultCri : cri; //打印 startWait,即当前时间,精确到毫秒 long startWait = System.currentTimeMillis(); try { /*等待最多 xx 毫秒获取一个信号量(permit) ,即 permit 操作。 * permit 操作用于获取当前可用的信号量,即是否有可以使用的信号量。 因此,因为在创建连接池的时候,我们也创建了一个 max 值的信号集, 所以,对于连接池中的连接数未达到 max 值的时候,肯定有可以使用的信号量。 除非,所有的连接都已经在使用状态,并且连接数已经达到 max 值, 这时,才有可能出现没有信号量,并出现超时的情况。 */ if (permits.attempt(poolParams.blockingTimeout)) { //计算本次获取连接的阻塞时间=当前时间-开始获取信号量的时间 long poolBlockTime = System.currentTimeMillis() - startWait ; //累加阻塞时间。 connectionCounter.updateBlockTime(poolBlockTime); //我们有一个权限去获取一个连接,判断是否在连接池中已经有一个可用的连 接? ConnectionListener cl = null; do { //线程安全,即从连接池中获取连接是一个串行操作 synchronized (cls)
  • 24. { //判断连接池是否已经被 shutdown,如果被 shutdown, //则抛出异常"The pool has been shutdown",并释放信号量 if (shutdown.get()) { permits.release(); throw new ResourceException("The pool has been shutdown"); } //如果可用的连接事件监听的 arraylist 大于 0, 则从 arraylist 的尾部取一 个连接 if (cls.size() > 0) { cl = (ConnectionListener) cls.remove(cls.size() - 1); //将 arraylist 中获取的连接加到到 checkdout 的一个 hash 结构中. checkedOut.add(cl); //计算已经使用的连接数 int size = (int) (maxSize - permits.permits()); //更新当前已经使用的最大连接数 if (size > maxUsedConnections) maxUsedConnections = size; } } //如果已经从连接事件监听数组中获取到了连接 if (cl != null) { //我们从一个 pool 取一个 ManagedConnection,并检查它是否符合要求? try { Object matchedMC = mcf.matchManagedConnections (Collections.singleton(cl.getManagedConnection()) ,subject, cri); if (matchedMC != null) { if (trace) log.trace("supplying ManagedConnection from pool: " + cl); //通知 connection listener 是否它拥有权限 cl.grantPermit(true); //返回连接,结束 return cl; } /* * 匹配不成功,并且没有异常信息抛出。
  • 25. * 在检查的时候,要么我们匹配错误,要么连接已经死亡 我们需要去辨别这些场景,但是现在,不管怎么样,我们都销毁连 接。 */ log.warn("Destroying connection that could not be successfully matched: " + cl); synchronized (cls) { //从 checkout 的 hashset 中删除已经获取的连接。 checkedOut.remove(cl); } //销毁连接 doDestroy(cl); cl = null; } //不管发生任何事,都销毁连接 catch (Throwable t) { log.warn("Throwable while trying to match ManagedConnection, destroying connection: " + cl, t); synchronized (cls) { checkedOut.remove(cl); } doDestroy(cl); cl = null; } //如果发生意外,我们应该决定是否应该继续尝试去建立连接, //由 jboss 配置文件中的的 useFastFail 参数来决定,默认为 false。 //这个 useFastFail 设置为 true,则立刻跳出 get connection,并报错。 if(poolParams.useFastFail) { log.trace("Fast failing for connection attempt. No more attempts will be made to acquire connection from pool and a new connection will be created immeadiately"); break; } } } //当连接监听队列>0,即还有可用的连接监听器 while (cls.size() > 0);//end of do loop
  • 26. //OK, 我们不能够找到一个可以使用的连接,则新建一个 try { //创建一个新的连接。这里不需要判断是否已经到达 max 连接数 //因为前面已经获取了信号量,所以肯定可以创建连接。 cl = createConnectionEventListener(subject, cri); synchronized (cls) { //将创建的连接加入到 checkout 数组中。 checkedOut.add(cl); int size = (int) (maxSize - permits.permits()); //更新当前已经使用的最大连接数 if (size > maxUsedConnections) maxUsedConnections = size; } //如果连接池还没有启动,则初始化连接池,并设置值 started 为 true。 //这里连接池可能被启用多次(因为非线程安全),但是这里没有危害。 if (started == false) { started = true; if (poolParams.minSize > 0) PoolFiller.fillPool(this); } if (trace) log.trace("supplying new ManagedConnection: " + cl); //通知 connection listener 是否它拥有权限 cl.grantPermit(true); return cl; } catch (Throwable t) { log.warn("Throwable while attempting to get a new connection: " + cl, t); //return permit and rethrow synchronized (cls) { checkedOut.remove(cl); } permits.release(); JBossResourceException.rethrowAsResourceException( "Unexpected throwable while trying to create a connection: " + cl, t); throw new UnreachableStatementException(); }
  • 27. } //这里的 else 操作,不能获取信号量,则抛出异常,报错连接池超时。 else { // we timed out throw new ResourceException("No ManagedConnections available within configured blocking timeout ( " + poolParams.blockingTimeout + " [ms] )"); } } catch (InterruptedException ie) { long end = System.currentTimeMillis() - startWait; connectionCounter.updateBlockTime(end); throw new ResourceException("Interrupted while requesting permit! Waited " + end + " ms"); } }
  • 29. 关于 getConnetion 的几点说明 1.blockingTimeout 是一个 jboss 的参数: <blocking-timeout-millis >5000</blocking-timeout-millis >,它是一个获取信号量的超 时时间,更确切的说,是从连接池中获取一个连接的超时时间。如果超过 5000ms,不 能够获取到信号量(连接),则 jboss 会抛出异常: “can not get connection,No ManagedConnections available within configured blocking timeout [xx] ms”。当然, 只要当前正在使用的连接数没有到达 MAX 值,这个信号量一定能够被获取到。因为信 号量一共有 MAX 值个,如果连接池中当前的连接不够用时,在获取信号量之后,新创 建一个连接即可。建议可以设置成可以设成 500ms 左右,不需要设计过大,当应用中 存在多个数据源时,可以防止因 DB 异常线程池带来的阻塞。如果网络环境不好的话, 可以设置的更高一点。 2.连接池内部就是一个连接监听队列,每次都从队列的尾部获取连接。而 IdleRemove 线程,则是从这个队列头部开始进行清理。 3.连接池的获取,销毁,创建,这三个操作都是线程安全的操作。 4.业务在使用连接的过程中,会一直占有这个信号量,在 returnConnection 或者发生 异常时释放信号量。能够获取信号量,则意味着肯定可以获取到连接。第二节中,我们讲到 JBOSS 在启动时会初始化一个信号量数组,长度为连接池的 max 参数。 当业务系统使用完连接后,需要把连接放回到连接池中它的主要见下图,源代码如下: public void returnConnection(ConnectionListener cl, boolean kill) { synchronized (cls) { /* * 判断连接是否已经被 DESTROYED? * 可能有其它的线程如 background-validation 及 shuwdown * 标记这个连接临听器为 DESTORYED 状态。 * */ if (cl.getState() == ConnectionListener.DESTROYED) { if (trace) log .trace("ManagedConnection is being returned after it was destroyed" + cl); //释放信号量,并直接返回 if (cl.hasPermit()) { // release semaphore cl.grantPermit(false); permits.release(); } return; } } if (trace)
  • 30. log.trace("putting ManagedConnection back into pool kill=" + kill + " cl=" + cl); try { //前台应用强制清理连接。 cl.getManagedConnection().cleanup(); } catch (ResourceException re) { log.warn("ResourceException cleaning up ManagedConnection: " + cl, re); //清理失败,抛出异常,清理失败。 kill = true; } synchronized (cls) { // 连接监听的状态为 DESTROY 或者 DESTROYED,则设置 kill 为 true if (cl.getState() == ConnectionListener.DESTROY || cl.getState() == ConnectionListener.DESTROYED) kill = true; //checkedOut 队列中移除连接监听器。 checkedOut.remove(cl); //如果 kill==false, 并且连接数>=最大连接 max 值, 说明异常发生, 再次设置 kill=true if (kill == false && cls.size() >= poolParams.maxSize) { log .warn("Destroying returned connection, maximum pool size exceeded " + cl); kill = true; } //kill 连接 if (kill) { // Adrian Brock: A resource adapter can asynchronously notify us // that // a connection error occurred. // This could happen while the connection is not checked out. // e.g. JMS can do this via an ExceptionListener on the // connection. // I have twice had to reinstate this line of code, PLEASE DO // NOT REMOVE IT! cls.remove(cl); } //如果 kill==false else { cl.used(); //这个连接监听不属于连接监听队列,则加入。
  • 31. if (cls.contains(cl) == false) cls.add(cl); else log.warn("Attempt to return connection twice (ignored): " + cl, new Throwable("STACKTRACE")); } if (cl.hasPermit()) { //释放信号量 cl.grantPermit(false); permits.release(); } } if (kill) { if (trace) log.trace("Destroying returned connection " + cl); //销毁连接。 doDestroy(cl); } }
  • 32. 执行过程流程图如下: ReturnConnetion 总结: 1.释放连接也是一个线程安全的操作。 2.在连接 return 时,有可能已经是 destory 的状态(前面第三节中讲到的 SHUTDOWN 操作,会对连接打上 DESTORY 的标记),这时,直接进行 remove 即可。 3.释放的连接若不属于连接监听队列(连接池),即加入到连接监听队列中(即连接池 中)。 4.释放连接需要释放信号量。
  • 33. 5.在释放过程中,出现任何异常,则将连接从连接池中移除,并进行强制销毁。 JBOSS 对于异常连接的处理: 默认情况下,JBOSS 不会对无效的连接进行销毁。 如果我们需要对异常列表中的连接进行销毁,则需要在连接池的 ds.xml 中添加以下配 置: < exception-sorter-class-name> org.jboss.resource.adapter.jdbc.vendor.OracleExceptionSorter < /exception-sorter-class-name> 这个是 ORACLE 的异常列表,可以查看这个类里面定义了 ORACLE 的异常列表,当连 接抛出这个列表中的错误时,即会进行销毁,如不能销毁,异常连接会一直存在连接池中。 ORACLE 异常列表如下: public boolean isExceptionFatal(final SQLException e) { // I can't remember if the errors are negative or positive. final int error_code = Math.abs( e.getErrorCode() ); if( ( error_code == 28 ) //session has been killed || ( error_code == 600 ) //Internal oracle error || ( error_code == 1012 ) //not logged on || ( error_code == 1014 ) //Oracle shutdown in progress || ( error_code == 1033 ) //Oracle initialization or shutdown in progress || ( error_code == 1034 ) //Oracle not available || ( error_code == 1035 ) //ORACLE only available to users with RESTRICTED SESSION privilege || ( error_code == 1089 ) //immediate shutdown in progress - no operations are permitted || ( error_code == 1090 ) //shutdown in progress - connection is not permitted || ( error_code == 1092 ) //ORACLE instance terminated. Disconnection forced || ( error_code == 1094 ) //ALTER DATABASE CLOSE in progress. Connections not permitted || ( error_code == 2396 ) //exceeded maximum idle time, please connect again || ( error_code == 3106 ) //fatal two-task communication protocol error || ( error_code == 3111 ) //break received on communication channel || ( error_code == 3113 ) //end-of-file on communication channel || ( error_code == 3114 ) //not connected to ORACLE || ( error_code >= 12100 && error_code = 21000 ) && ( (error_text.indexOf("SOCKET") > -1) //for control socket error || (error_text.indexOf("CONNECTION HAS ALREADY BEEN CLOSED") > -1)
  • 34. || (error_text.indexOf("BROKEN PIPE") > -1) ) ) { return true; } return false; } 类似的,我们也可以找到一个 MYSQL 的异常列表, org.jboss.resource.adapter.jdbc.vendor.OracleExceptionSorter: public boolean isExceptionFatal(SQLException e) { if (e.getSQLState() != null) { // per Mark Matthews at MySQL if (e.getSQLState().startsWith("08")) { return true; } } switch (e.getErrorCode()) { // Communications Errors case 1040: // ER_CON_COUNT_ERROR case 1042: // ER_BAD_HOST_ERROR case 1043: // ER_HANDSHAKE_ERROR case 1047: // ER_UNKNOWN_COM_ERROR case 1081: // ER_IPSOCK_ERROR case 1129: // ER_HOST_IS_BLOCKED case 1130: // ER_HOST_NOT_PRIVILEGED // Authentication Errors case 1045: // ER_ACCESS_DENIED_ERROR // Resource errors case 1004: // ER_CANT_CREATE_FILE case 1005: // ER_CANT_CREATE_TABLE case 1015: // ER_CANT_LOCK case 1021: // ER_DISK_FULL case 1041: // ER_OUT_OF_RESOURCES // Out-of-memory errors case 1037: // ER_OUTOFMEMORY
  • 35. case 1038: // ER_OUT_OF_SORTMEMORY return true; } return false; } 还有各种数据库的异常列表,都可以在自行配置,都定义在这个包下: org.jboss.resource.adapter.jdbc.vendor。
  • 36. 调优篇-如何防止连接风暴 引言:为什么 prefill=false 会在启动的时候碰到连接风暴,本节结合 JBOSS 源代码的 来分析这种情况产生的根本原因,最后给出几种方案来解决这种连接数突然飙升的问题。 (prefill 这个参数在 jboss4.0.5 版本以后才能够被支持) 连接风暴,听起来这个词很时髦,那么到底什么是连接风暴?在百度和 google 上搜索 了下, 都没有找到相关的解释。 所谓连接风暴,我们也可以称之为访问风暴,这是在使用 jboss 默认参数 prefill=false 的情况下,大规模应用集群很容易碰到的问题。先来描述一个场景: 在项目发布的过程中,我们需要重启应用,当应用启动的时候,经常会碰到各应用服务 器的连接数异常飙升。假设连接数的设置为:min 值 3,max 值 10。正常的业务使用连接数在 5 个左右,当重启应用时,各应用连接数可能会飙升到 10 个,瞬间甚至还有可能部分应用 会报取不到连接。启动完成后接下来的时间内,连接开始慢慢返回到业务的正常值。这种场 景,就是碰到了所谓的连接风暴。 知道了连接风暴的场景,那么它到底有什么危害呢,简单来说产要有以下几点(这些都是实 际碰到的情况) : 1. 在多个应用系统同时启动时,系统大量占用数据库连接资源,可能导致数据库连接 数耗尽。 2. 数据库创建连接的能力是有限的,并且是非常耗时和消耗 CPU 等资源的,突然大量 请求落到数据库上,极端情况下可能导致数据库异常 crash。 3. 对于应用系统来说,多一个连接也就多占用一点资源。在启动的时候,连接会填充 到 max 值,并有可能导致瞬间业务请求失败。 为什么我们的应用系统会在启动时会产生连接风暴。 前面第二节我们已经介绍了 jboss 在启 动的时候,有一个参数 < prefill >,这个参数决定了启动时是否初始化数据库的连接,默认 设置为 false,即启动时不初始化 min 值连接数。min 值的初始化在业务请求调用的第一次 getConnection 时进行。 下面来看一下,getConnection 的基本流程:(详细细节参考: http://www.dbafree.net/?p=378)
  • 37. 假设 min 值:9,max 值:20,我们结合上图来分析一下应用启动时的场景。因为数据库创建 一个连接需要耗费的时间在 80-100ms。在数据库启动的时候,80ms 之内,如果发生了并发 的 10 个 getConnection 操作(假设这 10 个 getConnection 都不能被复用),则会怎么样呢?一 共会创建多少个连接呢? 在 80ms 之内,第一个并发 getConnection 会执行 filltoMin(创建 9 个连接) ,其它的 9 个并发请求会各自独立的创建一个连接,所以一共会创建 9+9=18 个连接。这个过程的流程 图如下: 在实际执行 filltoMin 操作需要 9*80ms=720ms 左右,即在 80ms,160ms,240ms 的时候, 会分别创建 1 个连接,创建的连接,马上就可以被使用。所以,连接风暴会在最初的 80ms 内非常明显。 当这 18 个连接被创建后,我们前面在章节 JBOSS 连接池的初始化及关闭中已经提 到,连接池初始化的时候会把当前连接池注册到 IdleRemove 线程中,对于空闲 30 分钟以上 的连接会进行定时清理,这个定时任务每隔 15 分钟运行一次。所以定时任务执行时,会清 理空闲时间在 30 分钟-45 分钟的空闲连接。因此,在最多 45 分钟之后,连接又可以回落到 正常的业务值。 了解了连接风暴的原理后,我们可以思考一下解决方案。这是目前总结的三种方案: 1. 设置 jboss 连接池参数 prefill=true,即在启动时将连接数填充到 min 值。 2. 不设置 prefill,在启动完成,业务正常运行,应用先执行一个 SQL(如 oracle 可以执行:select * from dual),将连接数填充到 min 值。 3. 选择在业务低峰时重启系统。
  • 39. 调优篇-PreparedStatementCache 参数的作用及原理 JBOSS 连接池配置文件中有个参数,叫做 PreparedStatementCache,这个值怎么设置 呢,对于某些数据库来讲,这个值的设置会产生比较大的性能影响。 先来看两个使用 JAVA 语言来查询数据库例子: 例 1: String sql = "select * from table_name where id = "; Statement stmt = conn.createStatement(); rset = stmt.executeQuery(sql+"1"); 例 2: String v_id = 'xxxxx'; String v_sql = 'select name from table_a where id = ? '; //嵌入绑定变量 stmt = con.prepareStatement(v_sql); stmt.setString(1, v_id ); //为绑定变量赋值 stmt.executeQuery(); 例 1 和例 2 有什么区别呢? 先来看看 JBOSS 源码中对于这两个方法的解释: createStatement()方法: 创建一个 Statement 对象,用于发送 SQL 语句到数据库。没有使用绑定变量的 SQL 语 句一般使用 Statement 来执行。如果一个相同的 SQL 语句被执行多次,则使用 PreparedStatement 是一个更好的方式。 PreparedStatement prepareStatement(String sql)方法: 创建一个 PreparedStatement 对象,用于发送使用绑定变量的 SQL 语句到数据库中。 SQL 语句能够被预编译,并且存储到一个 PreparedStatement 对象中。这个对象,可以在多 次执行这个 SQL 语句的块景中被高效的使用(使用绑定变量的 SQL 至少可以省去数据库的 硬解析)。 因为 SQL 可以被预编译,所以这种方式用于使用绑定变量的 SQL。如果驱动程序支持 绑定变量,方法 prepareStatement 将会发送一个 SQL 语句到数据库,以执行预编译(即数 据库中的解析)。也有一些驱动不支持预编译,在这种情况下,在 PreparedStatement 执行 之后,语句才会被发送到数据库,这对于用户没有直接的作用。 这是源码中对于这两个方法的解释。 简单来说,这是数据库执行 SQL 的两种方式。第一种方式,直接发送 SQL 语句到数 据库执行。第二种方式在使用上较第一个方式会复杂一点,首先发送 SQL 到数据库,进行 解析, 然后将有关这个 SQL 的信息存储到一个 PreparedStatement,这个 PreparedStatement 可以被同样的 SQL 语句反复使用。 PreparedStatement 是 JDBC 里面提供的对象,而 JBOSS 里面引入了一个 PreparedStatementCache,PreparedStatementCache 即用于保存与数据库交互的
  • 40. prepareStatement 对象。 PreparedStatementCache 使用了一个本地缓存的 LRU 链表来减少 SQL 的预编译,减少 SQL 的预编译,意味着可以减少一次网络的交互和数据库的解析(有 可能在 session cursor cache hits 中命中,也可能是 share pool 中命中),这对应用的 DAO 响应延时是很大的提升。 下面是 JBOSS 源代码中对于 prepareStatementCache 的实现。 BaseWrapperManagedConnection 类是 JBOSS 连接管理最基本的一个抽象类,有两个类继 续自这个抽象类,分别为: 1.LocalManagedConnection 本地事务的连接管理。(一般我们都使用这个类) 2.XAManagedConnection 分布式事务的连接管理 BaseWrapperManagedConnection 类的构造函数中, PreparedStatementCache 的初始化。 有 (即在连接被创建的时候,即初始化 PreparedStatementCache),初始化代码如下: if (psCacheSize > 0) psCache = new PreparedStatementCache(psCacheSize); 而 PreparedStatementCache 这个类继承自 LRUCachePolicy 类,如下: public class PreparedStatementCache extends LRUCachePolicy public PreparedStatementCache(int max) { //max 值即为 JBOSS 连接池配置文件定义的 psCacheSize super(2, max); create(); } 其中 super 如下: //根据指定的最小值和最大值,创建 LRU cache 管理方案。 public LRUCachePolicy(int min, int max) { if (min < 2 || min > max) {throw new IllegalArgumentException("Illegal cache capacities");} m_minCapacity = min; m_maxCapacity = max; } public void create() { m_map = new HashMap(); m_list = createList(); m_list.m_maxCapacity = m_maxCapacity; m_list.m_minCapacity = m_minCapacity;
  • 41. m_list.m_capacity = m_maxCapacity; } PreparedStatementCache 对象其实是一个是一个 LRU List,即它使用了一个双向链表 来存储 PreparedStatement 的值,这个 LRU 链表的数据结构如下: public class LRUList { /** The maximum capacity of the cache list */ public int m_maxCapacity; /** The minimum capacity of the cache list */ public int m_minCapacity; /** The current capacity of the cache list */ public int m_capacity; /** The number of cached objects */ public int m_count; /** The head of the double linked list */ public LRUCacheEntry m_head; /** The tail of the double linked list */ public LRUCacheEntry m_tail; /** The cache misses happened */ public int m_cacheMiss; /** * Creates a new double queued list. */ protected LRUList() { m_head = null; m_tail = null; m_count = 0; } 在 BaseWrapperManagedConnection 被实例化的时候,即连接创建的时候,会初始化 一个最小值为 2,最大值为 max 的一个 LRU 双向链表(max 值在 jboss 连接池中通过< prepared-statement-cache-size> 50< /prepared-statement-cache-size>参数来设置)。 BaseWrapperManagedConnection 方法的 prepareStatement 方法,源代码如下: PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { if (psCache != null) { //实例化 KEY 类。 PreparedStatementCache.Key key = new PreparedStatementCache.Key(sql,
  • 42. PreparedStatementCache.Key.PREPARED_STATEMENT, resultSetType, resultSetConcurrency); /* 根据 KEY 从 LRU 链表中获取相应的 SQL,即 CachedPreparedStatement, 它包 SQL 语句及一些其它的环境参数。 在 get key 时进行判断,若有值,则将这个 KEY 对象移动到 LRU 链表头,LRU 链表的 热头. */ CachedPreparedStatement cachedps = (CachedPreparedStatement) psCache.get(key); if (cachedps != null) { /*判断是否可以使用。如果没有其它人使用,则可以使用这个 KEY。 否则进行下一步判断:是否自动提交模式,是否共享 cached prepared statements。 */ if (canUse(cachedps)) cachedps.inUse(); else /* 如果不能使用,则临时创建一个 PreparedStatement 对象, 这个 PreparedStatement 不会被插入到 psCache。 */ return doPrepareStatement(sql, resultSetType, resultSetConcurrency); } else { //若没有找到相应的 KEY, 创建一个 PreparedStatement 对象。 PreparedStatement ps = doPrepareStatement(sql, resultSetType, resultSetConcurrency); //再创建一个 CachedPreparedStatement cachedps = wrappedConnectionFactory.createCachedPreparedStatement(ps); /* 把新的 SQL 语句插入到 psCache 中。这个新的 SQL 语句被放到 LRU 链表的 头部, 同时,如果 LRU 链表在放置时满了,则清理最后的一个 SQL。 */ psCache.insert(key, cachedps); } return cachedps; } /* 如果 psCache 为 null,则说明没用启用 psCache,或者 psCache 为空, 则直接创建一个 PreparedStatement 对象进行查询。即不使用 psCache。 */
  • 43. else return doPrepareStatement(sql, resultSetType, resultSetConcurrency); } BaseWrapperManagedConnection 是一个连接管理的抽象类,对于每一个数据库的连接, 都有独立的 PreparedStatementCache。 在 ORACLE 数据库中, 使用 PreparedStatementCache 能够显著的提高系统的性能, 前提 是 SQL 都使用了绑定变量,因为对于 ORACLE 数据库而言,在 PreparedStatementCache 中 存在的 SQL,不需要 open cursor,可以减少一次网络的交互,并能够绕过数据库的解析,即 所 有的 SQL 语 句, 不需 要解 析即 可以 直接 执行。 但是 PreparedStatementCache 中 的 PreparedStatement 对象会一直存在,并占用较多的内存。 在 MYSQL 数据库中,因为没有绑定变量这个概念,MYSQL 本身在执行所有的 SQL 之前,都需要进行解析,因此在 MYSQL 中这个值不会对性能有影响,这个值可以不设置, 但是我觉得设置了这个值,可以避免在客户端频繁的创建 PreparedStatement,也有一定的好 处(这个没有经过测试,总的来说,意义并没有 ORACLE 那么大) 。 另外,PreparedStatementCache 也不是设置越大越好,毕竟,PreparedStatementCache 是 会占用 JVM 内存的。之前出现过一个核心系统因为在增加了连接数(拆分数据源)后,这 个参数设置没有修改,导致 JVM 内存被撑爆的情况。 (JVM 内存占用情况=连接总数 *PreparedStatementCache 设置大小*每个 PreparedStatement 占用的平均内存) 。
  • 44. 调优篇-合理的设置 PSCACHE 在前面第一节的 JBOSS 连接池 1-PreparedStatementCache 参数的作用及原理中已经解 释了 PreparedStatementCache(以下简称 PSCache)的作用及其工作的原理。这一节将根据 实际工作中碰到的问题,谈一下如何进行 PSCache 的调优。 对于 PSCache 设置过大或过小的影响: 为什么我们要合理的设置 PSCache 的值,这个值对于应用系统有怎样的影响呢?当然,之 所以要把这个问题提出来,肯定是有原因的,在这个参数的设置问题上,我们是吃过亏的。 所谓合理的设置,即这个值既不能设置太大,也不能设置太小。PSCache 设置过小,可能导 致 PSCache 命中率降低,最直观的影响,就是应用访问数据库延时增加,具体分析一下, 主要有以下几方面的影响: 1.对于没有在 PSCache 中的 SQL,每次需要在应用程序中新建 PS 对象。 (PS 对象的大小 和具体的 SQL 有关, 一般可以认为在几 K~1MB 之间, 具体大小需要以 JVM 的内存 DUMP 为准。后面会详细介绍。 ) 2.新建 PreparedStatement,需要进行一次网络的交互,这个开销非常大的。 (主要由于 交换机的延时及实际距离产生的代价, 一般可以认为这一次网络的交互在 0.3~0.5ms 之间。 ) 3.新建 PreparedStatement,需要数据库的一次解析操作, 而解析操作是非常消耗数据库资 源的,一般一次解析的时间,我们可以认为在 0.05ms 左右。 以上就是 PSCache 不能命中时主要的代价。实际经验中,我们碰到过因 PSCache 太小 的情况下, 应用访问增加了 0.6~0.8ms 的延时, 这个影响是很大的。 并且,PSCache 的增大, 可以消除 ORACLE 的等待事件:”SQL*Net more data from client”,这个等待事件在某生产库 上比较厉害,调整 PSCache 之后,由于该等待事件的下降,应用 DAO 的平均响应时间实际 提升达到了将近 1ms。关于该等待事件的详情,参 考:http://blogs.warwick.ac.uk/java/entry/wait_class_network/ 显然,PSCache 设置太小对性能影响很大,但是设置的过大,也会产生问题,PSCache 设置过大有可能耗尽应用服务器的内存,导致应用内存被撑爆。 那么如何来设置 PSCache 这个值呢,首先我们需要知道它占用的内存大小。 连接池内存耗费的计算: 我们知道,一个连接池中有多个数据库连接,每一个连接都有单独的 PSCache,并且, 连接池中占空间最大的就是 PSCache(占用 95%以上的空间大小)。因此,JVM 中连接池 的内存开销可以大致计算如下: JVM 内存占用大小=(连接池 1 中的连接数*PSCache 大小*平均每个 PS 的内 存占用)+ (连接池 2 中的连接数*PSCache 大小*平均每个 PS 的内存占用)+(连 接池 3 中的连接数*PSCache 大小*每个 PS 的内存占用) 假设一个应用有 3 个连接池,每个连接池有 10 个连接,每个 PreparedStatement 平均大小 为 200K。当 PSCache 分别设置为 30 和 100 时,它们的内存占用情况分别如下: 当 PSCache 为 30 时:内存大小=3*10*200K*30=175.78MB 当 PSCache 为 100 时:内存大小=3*10*200K*100=585.95MB 两者 PSCache 相差了 400 多 MB,和内存大小有关系的 4 个参数,分别是:连接池个 数,连接池中的连接数,PSCache 的大小,PS 大小。
  • 45. 连接池个数: 这个和应用架构有关系, 如果不能省略数据库的访问或者通过接口调用来 访问,这一块的开销必不可少。 连接池中的连接数: 这个由业务的处理时间及业务量决定。 我们只能合理的设置连接池 的 min 值为 max 值。 PSCache 大小:这个参数可以由我们来控制,和 PS 的大小关系很大,需要合理设置 这个参数。 PS 大小: 由具体访问的 SQL 语句决定, 这个值决定了 PSCache 大小的设置。因此,JVM 内存空间的计算最关键的问题变成了如何计算 PreparedStatement 的大小。 如何计算 PreparedStatement(PS)的大小: 进行连接池内存计算的关键值就是 PreparedStatement 的大小。我们可以对正在运 行的 JBOSS 容器,作一个 JVM 内存的 DUMP(方法:使用 jmap 来产生一个 DUMP 文件) ,然后使用 Memory Analyzer (MAT)工具来查看 JVM 内存里面的具体内容。下 面是我自已测试的一个 DUMP 结果,如下: 测试用例中执行了一个 SQL 为: Select * from test10000 where col_a=:1 Test10000 的表结构如下: ZHOUCANG/ZHOUCANG@TEST>create table test10000 ZHOUCANG/ZHOUCANG@TEST>(col_a varchar2(4000) , ZHOUCANG/ZHOUCANG@TEST> col_b varchar2(4000), col_c varchar2(2000)); Table created. 使用 OCI 方式连接数据库:
  • 46. 用 OCI 方式连接数据库,可以看到调用的是 JDBC 的 T2CPreparedStatement 方法,里 面有一个 CHAR[100030]的数组,占用了 200KB 左右的内存,这个就是用于保存数据库查 询结果的 char 数组。这里的 fetchsize 设置为 10(jdbc 默认值)。同样使用 OCI 方式,我 们将 fetchsize 设置为 100,则获取到的 char 数据长度也增大了 10 倍,为 CHAR[1000300], 大小在 2MB,如下图: 可见 PreparedStatement 占用内存的大小,和 fetchsize 有很大的关系。同样的,char 数组 保存了查询的结果集, 所以, 查询的字段的总长度决定了也 PreparedStatement 对象的大小。 有了 fetchsize 和查询字段总长度,我们就可以计算出 PS 内存占用。如 SQL:Select* from test10000,选取的字段长度为 10000,内存占用为 200KB,fetchsize 为 10。则可以得出 这个比例为: 200K/10(fetchsize)/10000(字段总长)=2 为什么这个比值是 2 呢? 怀颖这中间会有一层字符集的转换。 10000 (字节) 长度的字段, fetchsize 为 10 时, 当 需要保存的占用的 JVM 内存的 char 数组长度为: 10000 字节*10=100KB, char[100030], 即 而本地客户端的编码为 GBK,所以,char[100030]的字符数组转化为字节数,大约会占用 200KB 的内存空间。 使用 thin 的方式连接数据库:
  • 47. 这里使用了 thin 方式来连接数据库, fetchsize 设置为 100,保存的数据结构同样是 CHAR 数据,THIN 方式与 OCI 方式没有区别,并且 PreparedStatement 内存占用也基本一致。唯 一的区别是,thin 方式调用的是 JDBC 的 T4CPreparedStatement 方法,而 OCI 方式调用的 是 T2CPreparedStatement。具体这两个方法的区别,有兴趣的同学可以研究下 JDBC 的驱 动。 关于 char 数组的补充: 以上测试基于 OJDBC10.2.3 版本进行测试, 在早期的 JDBC 驱动中, 我们可以看到 select 返 回 结 果 集 是 以 一 个 Object 数 组 的 形 式 来 保 存 , 如 fetchsize 为 10 , 这 个 数 组 为 Object[10],fetchsize 为 100,则数组为 Object[100]。并且 Object 数组中的元素又是以字段的 个数来分别保存 string 字符串的,即有多少个查询字段,就会有多少个 string 字符串。对于 这种存储方式,我的理解是,减少了 JVM 内存的碎片,可以减轻 GC 的压力。 真实场景中计算 PreparedStatement 对象: 根据 select 字段的长度,我们就可以计算出每个 PreparedStatement 在内存中的大小。当 然,这里需要两个值,fetchsize(默认为 10),select 字段长度的总和。有了这两个值之后,就 可以精确的计算 PreparedStatement 的大小了。如,以下为某系统的 DUMP 统计结果,供参 考: 字段长度 PS 大小 (字节) PS 大小/字段长度 说明 14151 1495269.376 105.6652799 因为 PS 对象中除了 char 数 组,还有其它的变量,所以当 14151 1495269.376 105.6652799 字段长度较小时 (即 char 数据 4412 479232 108.6201269 较小) 4412 479232 108.6201269 “PS 大小/字段长度”,这个比重 就增大了。 4412 479232 108.6201269 3736 404172.8 108.1832976 3000 319488 106.496 2851 319488 112.0617327 3000 317440 105.8133333 2391 259072 108.3529904 2282 246784 108.1437336 1576 178176 113.0558376 1576 178176 113.0558376 725 84992 117.2303448 438 52224 119.2328767 401 50688 126.40399 使用图表关系来查看如下,可以发现这些点可以构成一个一元二次方程,偏差在 99.99%以 内:
  • 48. 计算内存和字段总长的比例: 1495269.376 字节/fetchsize(50)/14151=2(约等于) 实际这个比值,应该以真实环境的内存 DUMP 为准。知道了如何计算 PreparedStatement 对象的大小后,我们对于应用内存的控制可以做到心中有数了。 当然,如果应用内存不吃紧, 可以适当的加大这个值,以提升 SQL 的效率。 补充一点: 如果 PSCache 中有 in 的 SQL, 如“select * from t where id in(:1,:2)”和“select * from t where id in(:1,:2,:3)”,这在 PreparedStatementCache 中是两个不同的对象,因为 in 中条件个数的不同,可能导致 PreparedStatementCache 的命中率急剧下降。这里有一种 方法可以解决,详见后面第三章节的内容。 总结计算连接池占用 JVM 内存的公式及 PSCache 设置参考: 以单个数据源为例(多个数据源累加即可),假设当前连接数为 a,每个连接的 PreparedStatementCache 为 b,并且“内存/字段总长”=c,字段平均的长度为 d 连接池占用的内存大小=a*b*c*d。 最后,给出一个 PSCache 设置的参考意见,PSCache 最好能够覆盖 95%的应用 SQL, 然后可以在 95%这个值以上,再上浮 5-10 个。如: SQL1: select * from text1; 应用调用占比重 45% SQL2: select * from xxx where; 应用调用占比重 25% SQL3: select * from xxx ; 应用调用占比重 15% SQL4: select * from xxx; 应用调用占比重 10% SQL5: select * from xxx; 应用调用占比重 3%
  • 49. SQL6: select * from xxx; 应用调用占比重 1.3% 我们统计, SQL1,SQL2,SQL3,SQL4 占了 SQL 总访问量的 95%,因此我们可以将 PSCache 设置在 9-14 这个区间。 以上测试及数据仅供参考,请以真实环境数据为准进行评估。 记住,一定要给应用预留足够多的内存,即使 PSCache 设置在平时是合理的,但是因 为连接数可能会由于应用异常或者业务冲峰而陡增,按上面的计算公式,还是可能会导致 JVM 内存被撑爆。 附: OCI 连接数据库 package com.alipay.db; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; /* * jmap -dump:format=b,file=test.bin 4939 */ public class Test_Tns { public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException { String dbUrl = "java:oracle:oci:@xxx"; Connection conn = null; PreparedStatement stmt; ResultSet rs = null; String sql = ""; try { Class.forName("oracle.jdbc.driver.OracleDriver").newInstance(); conn = DriverManager.getConnection(dbUrl, "zhoucang", "zhoucang"); sql = "select * from test10000 where col_a=? "; stmt = conn.prepareStatement(sql); stmt.setString(1, "test"); stmt.setFetchSize(10); System.out.println(stmt.getFetchSize()); rs = stmt.executeQuery(); // System.out.println(stmt.get); stmt.getMetaData();
  • 50. while (rs.next()) { //System.out.print(rs.getString("COL_A")); } System.out.println(stmt.getQueryTimeout()); stmt.close(); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } THIN 连接数据库 package com.alipay.db; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; /* * jmap -dump:format=b,file=test.bin 4939 */ public class Test { public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException { String dbUrl = "jdbc:oracle:thin:@10.xx.xx.xx:1521:sid"; Connection conn = null; PreparedStatement stmt; ResultSet rs = null; String sql = ""; try { Class.forName("oracle.jdbc.driver.OracleDriver").newInstance(); conn = DriverManager.getConnection(dbUrl, "zhoucang", "zhoucang"); sql = "select * from test10000 where col_a=? "; stmt = conn.prepareStatement(sql);
  • 51. stmt.setString(1, "test"); stmt.setFetchSize(100); System.out.println(stmt.getFetchSize()); rs=stmt.executeQuery(); while (rs.next()) { System.out.print(rs.getString("COL_A")); } stmt.close(); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }
  • 52. 调优篇-inlist 查询优化 在前面一节 JBOSS 连接池调优 2-合理的设置 PreparedStatementCache 中我们已经学 会了使用 Memory Analyzer (MAT)工具来查看 JVM 的内存,并且看到了由于 inlist sql 的存 在,导致系统 PreparedStatementCache 中 SQL 的命中率仅为 40%左右。之前我们已经介 绍了 PreparedStatementCache 的作用,因此,命中率低可能引起的问题也显而易见(不清 楚的同学请参考前一章节的内容)。 部分同学可能不明白什么是 in list 的 SQL,这里先说明一下,所谓 in list 的 SQL 就是指 使用了 in 来进行查询,绑定变量个数不确认的 SQL,如: select * from test where id in (:1,:2,:3) 对于这一类的查询, 由于 in 的查询条件中绑定变量个数的不同, 会导致 SQL 版本变多, 从而导致 PreparedStatementCache 的命中率下降。(因为 JBOSS/oracle 中都是以 SQL 文 本完全一致来匹配 PreparedStatementCache)。 因为 PreparedStatementCache 在 MYSQL 中没有作用(根本原因是 MYSQL 不支持绑定 变量) 所以 MYSQL 不需要考虑 in list 在 PSCACHE 中的优化, , 下面给出 oracle 中两种 in list sql 的解决方案: 方案 1:使用 oracle 中的 pipelined function(可以认为是一种性能较好的存储过程),实 现如下函数: create or replace type t_array as table of varchar2(4000) / --The PIPELINED keyword on line 4 allows this function -- to work as if it were a table: create or replace function str2varlist(p_string in varchar2) return t_array PIPELINED as v_str VARCHAR2 (4000) DEFAULT p_string || ','; v_n number(11); begin LOOP v_n := INSTR (v_str, ','); EXIT WHEN (NVL (v_n, 0) = 0); pipe row(LTRIM (RTRIM (SUBSTR (v_str, 1, v_n - 1)))); v_str := SUBSTR (v_str, v_n + 1); END LOOP; return; end;
  • 53. / 这个函数的作用是将以逗号分隔的字符串转换成一个 table,如下: zhoucang@zhoucang>select * from TABLE(str2varlist('123,456,789,012,345')); COLUMN_VALUE --------------------------------------------------------------------------- 123 456 789 012 345 5 rows selected. 这样,每次我们的 SQL 执行时,只要传入一个固定长度的字符串即可,如: select * from test where id in(select * from TABLE(str2varlist(:1))) 或者写成 join 的方式: select /*+ use_nl(a b) ordered*/ b.* from TABLE(str2varlist(:1)) a,test b where a.column_value = b.id 这也不失为一种好方法,至少使用起来非常的方便。 方案 2:这是一种看起来很土的方法,同事建议的,真的很土,即固定 in 后面条件的个数, 每次传入不同的值,不足的可以使用一个不存在的值来进行补充。如: select * from test where id in(:1,:2,:3,:4,:5,:6,:7,:8,:9,:10) 固定 10 个 ID 的查询,不足 10 个以-1 来补充(如果 ID 不为负数)。 因为 oracle 中的 in 限制最多为 999 个,所以,极端情况下,也只需要固定 999 个 ID 即可。 如何选择方案 1 还是方案 2? 专门测试了一下,测试过程主要逻辑如下: 方案 1,创建 pipelined function,执行 20000 次查询: sql = " select /*+ use_nl(a b) ordered*/ b.* from TABLE(str2varlist(:1)) a,test b where a.column_value = b.id ";
  • 54. for (int i = 0; i < 20000; i++) { stmt1.setString(1,"1111,2222,3333"); } 方案 2,固定绑定变量个数,执行 20000 次查询: sql = "select * from test where id in(?,?,?,?,?,?,?,?..........) "; for (int i = 0; i < 20000; i++) { stmt.setString(1, "1111"); stmt.setString(2, "2222"); stmt.setString(3, "3333"); stmt.setString(4, "-1"); stmt.setString(5, "-1"); stmt.setString(6, "-1"); …… } 测试主要以数据库的性能损耗为主要考虑指标(因为应用上带来的好处对我们来说诱惑 力实在太大了,基本上没什么坏处,性价比很高),测试结果如下(给出数据库中单个 SQL 主要的性能参考指标): 说明: 应用总 内 存 读 返 回 单 条 单条 SQL 执行时 (块) 记录 SQL 响应时间 间:秒 CPU 时 间(us) 3 个绑定变量,传 3 个 id 38 9 3 75.84365 75.84365 20 个绑定变量,传 3 个 id 39 10.06 3 94.48995 94.48995 100 个绑定变量, 传 3 个 id 43 10.05955 3 112.8007 112.8007 PPLINE,3 个 id 41 9.01435 3 149.5074 149.5074 100 个绑定变量, 10 个 id 传 53 31.05955 10 220.5576 220.55765 5 10 个绑定变量,传 10 个 id 43 30 10 174.7577 174.7577 PPLINE,10 个 id 51 30.03795 10 358.4637 358.46375 5 PPLINE 方案(方案 1): 1. CPU:会增加 CPU 的消耗 1 倍左右,原因是数据库需要对字符串进行解析。 2. IO:对 IO 基本无损耗。 3. 响应时间:数据库响应时间增加一倍。 固定绑定变量个数(方案 2) : 1. CPU:CPU 的消耗,增加幅度在 20%~60%,之间,具体因 in 的个数而定。 2. IO:增加了一个逻辑读, 这一个逻辑读是由于查询-1 时产生的, 主要是由于索引 branch 节点查询引起,由于 root 节点一定需要查询,索引一般为 3-4 层,可以认为,这个逻辑读 开销在 1-3 个左右,基本上可以认为在 1 个左右(这个可以结合 B 树索引结构得知)。并
  • 55. 且这个 branch 节点非常热,IO 开销肯定是逻辑读。 3. 响应时间: 增加幅度在 20%~60%,之间,具体因 in 的个数而定。 显然,选择方案 2 会更加节省数据库的资源。 从测试结果可以看出, 当传入 3 个 ID 的时候,使用 100 个绑定变量和 20 个绑定变量, 还是存在一定的差异。因此,对于 in 的 SQL 我们可以再进行分级,根据执行频率,评估适 当的多给出几个版本,如: Id=:1 :适用于 1 个 ID 的查询 Id in(20 个绑定变量) :适用于 ID 个数据在 2-20 个之前的查询。 Id in (999 绑定变量) :适用于 ID 个数在 20 个以上的查询。
  • 56. 调优篇-合理设置连接数的 min 值和 max 值 前面两节 jboss 连接池的启动及 prefill 参数配置和 JBOSS 连接池的初始化及关闭中, 已经详细解释了 JBOSS 连接池的内部管理机制。本文就实际使用中的一些经验,分享一下 连接池 MIN 值和 MAX 值设置上的一些经验,最后再补充下关于 blocking-timeout-millis 设 置的一些建议。 连接池设置不合理可能导致的后果: 连接池的 MIN 数和 MAX 数,看似很简单的两个参数,其实对于应用和数据库的影响, 并不是那么的简单。我们先来看看连接数设置不合理可能产生的后果: 1. 连接池 MIN 设置过小,应用业务量突增,或者启动时可能产生连接风暴。 2. 连接池 MIN 值设置过大,会造成资源的浪费,主要包括数据库和应用内存,连接数 的浪费,同时连接池 MIN 值设置过大,也会导致连接被频繁的创建和销毁。这是由 连接池的工作机制决定的。 3. 连接池 MAX 值设置过大。在极端情况下,当应用发生异常时,会导致连接数被撑 到 MAX 值,有可能导致数据库连接数被耗尽,从而导致正常的业务受到影响。当 连接数被撑到 MAX 值,在获取连接超时的时候,应用的线程池也有可能受到影响, 这是一系列的连锁反应。 以上是连接池 MIN 值和 MAX 值设置最主要的 3 点影响。其中,影响最大的并且最难配置的 是第 2 点,即连接数的 MIN 值设置。 设置连接池的 min-pool-size: 一般来说,我们可以按照业务高峰时期的压力来估算连接数。比如,在高峰时期,每秒 有 5000 个并发请求,每个请求的处理时间为 2ms,则每秒总共需要 5000*2ms=10s 的连 接处理时间。因此,连接数的 MIN 值我们可以设置为 10 个(适当上浮 1-2 个),这样就能 基本上解决连接风暴的问题了。当然在业务低峰时期,10 个连接数的设置可能偏大了点, 所以这里对于连接数的 MIN 值设置也做了一个权衡,保守起见,我们需要以高峰时需要的 正常连接数来设置 MIN 值,以避免连接风暴的发生。因为这样做是最保险的,当然,这带 了一些额外的代价,会牺牲掉部分的数据库和应用的资源。 有时候,我们对于业务的估计可能并不准备,如业务刚上线,对于连接数不能很好的评估, 这个时候我们可以对连接池的 MIN 值进行调优,共有两种方式,如下: 通过 ORACLE 监听日志对 min-pool-size 调优: 对 于 ORACLE 数 据 库 , 我 们 可 以 使 用 数 据 库 的 监 听 日 志 来 进 行 调 优 , 即 通 过 listener.log(数据库监听日志)进行调优。收集以下几个信息: 1. 根据监听日志,我们可以得到某个应用集群 A 在 2 小时内创建的连接总数,假设统 计一共为 N 个。 2. 假设 A 集群在 2 小时内的连接数维持在一个值:M。 (后面会对这个 M 进行说明) 3. A 集群有 S 台应用服务器,假设应用服务器负载均衡。 我们知道, 连接池 IDLE 清理是 15 分钟执行一次的, 每次清理空闲时间超过 30 分钟的连接。 所以,2 小时内,每台应用平均创建的连接数为:N/4S 个。 假设:
  • 57. 1. M=min 连接数,则我们的应用可以下调的 MIN 连接数为 N/4S 个,即设置为 M-N/4S。 (由连接池的原理可以知道,这些新创建的连接完全都是空闲的连接,只是由于配置了 MIN 值较大而产生的,实际上这些连接一直处于空闲状态) 2.假设 M 值>min 连接数,则需要将 min 值设置为 M。 3.假设 M<min,这不可能发生。 之前对于某核心系统做的一次调优, 上线后效果很不错,参考 一个生产库的 JBOSS 连接池调 整优化及分析 通过监控 JBOSS 连接池对 min-pool-size 的调优: 连接池默认参数及获取连接池中相关的统计信息一节中, 我们提到了连接池中有一些方 法可以获取连接数的信息,其中有三个方法: //获取连接数,内部执行:return created - destroyed;即所有创建的连接数-所有销毁的连接数 public int getConnectionCount() { return connectionCounter.getCount(); } //创建连接的总数 public int getConnectionCreatedCount() { return connectionCounter.getCreatedCount(); } //连接销毁总数 public int getConnectionDestroyedCount() { return connectionCounter.getDestroyedCount(); } 通过监控这三个参数我们即可进行连接数的调优,具体的调优方式和方法 1 一致,只是 我们获取创建的连接数,通过监控连接池中的状态获取。如下图所示,我们监控了 10:00-12:00 数据库的连接数:
  • 58. 通过上面的图可以知道,我们的连接数 MIN 值设置过大了(上图是对于单台应用的连接 池监控)。因为连接池始终保持在 MIN 值 10 个,说明连接数过剩,而我们的连接池不停的 创建,销毁。由于定时任务是 15 分钟启动 1 次的,所以我们在图上可以看到每隔 15 分钟 说有 1 个连接被销毁。在 30 分钟平均有 2 个连接被销毁。根据同样的 ORACLE 监听日志的 调优算法: 我们的连接数可以被设置为 M-N/4S,即 10-8/4=8 个。 这是二种连接池 MIN 调整的方案。其实原理是完全一样的,只是获取数据的两种方式 不同,对于有条件的应用,建议监控 JBOSS 的连接池进行调优。这样,连接池的一切状态, 都会尽在掌握之中,调优也会变的更加简单高效。 设置连接的 max-pool-size: 其实我们在调整连接池 MIN 值的时候,已经是按业务的高峰时期进行调整。所以 MAX 值调整的意义其实并不大,我们只要将 MAX 值设置在一个安全的阀值即可。保证不超过数 据库的连接数,同时又保证在业务偶尔分布不均匀时,应用也能够获取连接,防止连接池取 不到的情况。 我们经常会看到应用程序报错:“No ManagedConnections available within configured blocking timeout xx [ms]”,这其实是由于连接池达到最大值了,没有可用
  • 59. 的连接的时候产生的。(参考:JBOSS 连接池 4-从连接池中获取连接及返还连接)。产生 这个问题, 最根本的原因往往并不是连接数不够用, 我们应该首先看看数据库的响应和应用 DAO 的响应时间。有时候因为 SQL 走不上索引,或者数据库响应较慢,会增加业务占用连 接的时间(即处理时间),这种情况下,加大 MAX 连接数可以从一定程序上缓解问题,但 是不能解决根本的问题。 blocking-timeout-millis 的设置: 另外一点建议是,将 blocking-timeout-millis 参数设置的尽量小一点,这个参数是应用 getconnection 时的超时时间,只在连接数达到 MAX 值时才会起作用,因为连接数没到达 MAX 值,这个获取连接是一个很快的操作,内部仅仅是执行一个获取信号量的操作。为了 尽量的减小在获取不到连接而等待超时对应用线程池的阻塞,我们需要将 blocking-timeout-millis 参数设置为一个较小的值即可, 这样,当连接池中无可用的连接时, 由于超时时间减小了, 可以避免由于线程池共享造成对其它正常业务的影响, 特别是一个应 用连接多个数据源时,这个值的设置尤其重要。 总结: 实际我们对于连接池的调优,需要从连接池的原理入手,只有理解了原理,才能够更好 的将连接池控制在我们所想要的一个状态。因为连接池的不合理设置所产生的问题,我们在 上面吃的亏很多。就因为一个小小的连接池出了问题,现在看来实在有点得不偿失。 真实的场景中,特别是基于数据库的水平拆分,或者读写分离的场景中,当一个应用集 群需要连接多个数据源时,我们尤其需要考虑连接池的这几个参数: prepared-statement-cache-size,min-pool-size, max-pool-size, blocking-timeout-millis。这 几参数设置的利害关系,需要仔细的考虑。另外,我们还需要重点考虑 2 个点: 1. 对于物理库-逻辑库中,数据源共享和不共享的取舍。 2. prepared-statement-cache-size,min-pool-size 值的设置,数据源个数,fetchsize 的设置 直接影响到 JVM 内存的使用。
  • 60. 调优篇-合理的设置 fetchsize 在前面的几节中,我们经常会提到一个词“Fetchsize”,并且在《JBOSS 连接池调优 2- 合理的设置 PreparedStatementCache》一节中研究了 fetchsize 对应用内存的影响。网上关 于 fetchsize 的文章不是很多,很难对这个参数有一个全面的了解,确实,如果从开发的角 度来理解 fetchsize,确实是存在一些困难的。那么到时什么是 fetchsize?fetchsize 对内存 和性能到底有什么影响呢?我在前段日子作了不少测试和研究, 下面将自己的研究成果和大 家分享一下。 什么是 fetchsize? Oracle 中的 fetchsize: 先来简单解释一下,当我们执行一个 SQL 查询语句的时候,需要在客户端和服务器端 都打开一个游标,并且分别申请一块内存空间,作为存放查询的数据的一个缓冲区。这块内 存区,存放多少条数据就由 fetchsize 来决定,同时每次网络包会传送 fetchsize 条记录到客 户端。应该很容易理解,如果 fetchsize 设置为 20,当我们从服务器端查询数据往客户端传 送时, 每次可以传送 20 条数据, 但是两端分别需要 20 条数据的内存空闲来保存这些数据。 fetchsize 决定了每批次可以传输的记录条数,但同时,也决定了内存的大小。这块内存, 在 oracle 服务器端是动态分配的(大家可以想想为什么)。而在客户端(JBOSS),PS 对 象会存在一个缓冲中(LRU 链表),也就是说,这块内存是事先配好的,应用端内存的分配 在 conn.prepareStatement(sql)或都 conn.CreateStatement(sql)的时候完成。 在 java 程序中,我们会执行以下代码: //打开游标,执行查询,但是并不获取任何的数据,网络上没有数据的传输。 rs = stmt.executeQuery(); //获取具体的数据,网络一般每次传输 fetchsize 条数据。 while (rs.next()){ } MYSQL 中的 fetchsize: MYSQL 的 preparestament 基本上不占用内存,为什么呢?因为 MYSQL 并不需要象 oracle 那样的一块内存来保存结果集缓冲区,为什么不需要缓冲区,其中根本的原因是由 MYSQL 的通讯方式决定的。 MYSQL 客户端/服务器协议是半双工的,即 MYSQL 只能在给定的时间,发送或接受数 据,但不能同时发送和接收。所以,MYSQL 在数据查询结果集传送的时候,需要一次性将 数据全部传送到客户端,在客户数据接收完之后,释放相关的锁等资源。因为这种半双工的 通讯方式,所以 MYSQL 不需要客户端的游标,但是客户端 API 通过把结果取到内存中,可 以模拟游标的操作。所以,我们可以在 JAVA 程序中,可以象 ORACLE 那样来实现 MYSQL 的访问。 如何设置 fetchsize? Fetchsize 可以在任何一层进行设置 ,ORACLE JDBC 驱动默认的 FETCHSIZE 为 10。 一般为了方便,我们会在数据源层面上来设置 fetchsize。
  • 61. 语句级别的设置: 我们可以在 jdbc 中调用 Preparedstatement .setFetchSize()的进行设置: stmt = conn.prepareStatement(sql); stmt.setFetchSize(50); 也可以在 Ibatis, hibernate 等框架上直接针对某个语句进行设置: < select id="getAllProduct"> select * from employee < /select> 数据源中的全局设置: JBOSS 连接中设置: < connection-property name="defaultRowPrefetch">50 Fetchsize 的核心源码: 可以在 JDBC 驱动类 Oracle.jdbc.driver.OracleStatment 中找到这个方法, setPrefetchInternal 方法中传入的默认值为 0,伪代码如下: void setPrefetchInternal(int paramInt){ if (paramInt < 0) { DatabaseError.throwSqlException(68, "setFetchSize"); } //获取连接池中的 DefaultRowPrefetch 属性 else if (paramInt == 0) { paramInt = this.connection.getDefaultRowPrefetch(); } if (paramInt == this.defaultRowPrefetch) return; this.defaultRowPrefetch = paramInt; if ((this.currentResultSet == null) || (this.currentResultSet.closed)) { this.rowPrefetchChanged = true; } } Fetchsize 对性能影响的测试: 空查询结果集的测试: 查询的表一共有 300 条记录,测试中查询的结果集为空,执行的是全表扫描。 SQL> select count(*) from test10000; COUNT(*) ----------