数据库连接池剖析3-2(hikariCP原理分析)

 2、hikariCP原理分析

2.1 HikariCP为什么这么快

1、优化并精简字节码:HikariCP利用了一个第三方的Java字节码修改类库Javassist来生成委托实现动态代理。之所以使用Javassist生成动态代理,是因为其速度更快,相比于JDK Proxy生成的字节码更少,精简了很多不必要的字节码。

2. 优化代理和拦截器:减少代码,例如HikariCP的Statement proxy只有100行代码,只有BoneCP的十分之一;

3、ConcurrentBag:更好的并发集合类实现

ConcurrentBag内部同时使用了ThreadLocal和CopyOnWriteArrayList来存储元素,其中CopyOnWriteArrayList是线程共享的。ConcurrentBag采用了queue-stealing的机制获取元素:首先尝试从ThreadLocal中获取属于当前线程的元素来避免锁竞争,如果没有可用元素则扫描公共集合、再次从共享的CopyOnWriteArrayList中获取

4、使用FastList替代ArrayList:自定义数组类型(FastList)代替ArrayList:避免每次get()调用都要进行range check,避免调用remove()时的从头到尾的扫描

5. 其他针对BoneCP缺陷的优化,比如对于耗时超过一个CPU时间片的方法调用的研究。

2.2连接池获取

  • 1、采用 threadLocal 缓存同一个线程使用过的db连接,并且是从最近一个开始遍历int i = list.size() - 1, 这样同一个java线程多次获取db连接的时候就能快速获取到连接,而且获取到最鲜活的,还可避免 HikariPool#getConnection 的连接可用性的检查
  • 2、ConcurrentBag 底层 sharedList 用的 copyOnWriteArrayList,所以读和写之间就不需要加锁了
  • 3、ConcurrentBag 申请连接borrow 和归还连接 requite 只是进行 cas 状态变更的无锁操作,并不会从 copyOnWriteArrayList 移除元素
  • 4、单线程的线程池:新建和关闭连接的线程池都是单线程的,也就避免了线程间的协调开销
  • 5、handoffQueue: SynchronousQueue 当申请连接 borrow() 的时候,如果空闲连接不够用,线程就会阻塞在 handoffQueue#poll ; 在连接新增 add() 或归还 requite() 时,如果发现有线程在等待中则会把这个连接交给 handoffQueue ,这样等待中的线程直接能直接获取到,而不用再遍历 sharedList 同时也保障了公平性 先到先得
  • 6、minimumIdle 默认等于 maximumPoolSize, 尽量减少在申请连接的时候还需要创建连接的开销
public Connection getConnection(final long hardTimeout) throws SQLException
{
   suspendResumeLock.acquire();
   final long startTime = currentTime();

   try {
      long timeout = hardTimeout;
      PoolEntry poolEntry = null;
      try {
         do {
            poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
            if (poolEntry == null) {
               break; // We timed out... break and throw exception
            }

            final long now = currentTime();
            //1、是否标志为抛弃
            //2、当前连接对象距离上次被使用时间是否超过500(默认500 可配置)
            //3、isConnectionAlive
            if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) {
               closeConnection(poolEntry, "(connection is evicted or dead)"); // Throw away the dead connection (passed max age or failed alive test)
               //减去本次耗时,重试
               timeout = hardTimeout - elapsedMillis(startTime);
            }
            else {
               metricsTracker.recordBorrowStats(poolEntry, startTime);
               return poolEntry.createProxyConnection(leakTask.schedule(poolEntry), now);
            }
         } while (timeout > 0L);
      }
      catch (InterruptedException e) {
         if (poolEntry != null) {
            poolEntry.recycle(startTime);
         }
         Thread.currentThread().interrupt();
         throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
      }
   }
   finally {
      suspendResumeLock.release();
   }

   throw createTimeoutException(startTime);
}

2.3 链接池初始化

初始化入口:com.zaxxer.hikari.pool.HikariPool#HikariPool

该流程用于初始化整个连接池,这个流程会给连接池内所有的属性做初始化的工作,其中比较主要的几个流程上图已经指出,简单概括如下:

  1. 利用config初始化各种连接池属性,并且产生一个用于生产物理连接的数据源DriverDataSource
  1. 初始化存放连接对象的核心类ConcurrentBag
  1. 初始化一个延时任务线程池类型的对象houseKeepingExecutorService,用于后续执行一些延时/定时类任务,比如连接泄漏检查延时任务,除此之外maxLifeTime后主动回收关闭连接也是交由该对象来执行的
  1. 预热连接池,HikariCP会在该流程的checkFailFast里初始化好一个连接对象放进池子内,当然触发该流程得保证initializationTimeout > 0(默认值1),这个配置属性表示留给预热操作的时间,默认值1在预热失败时不会发生重试
  1. 初始化一个线程池对象addConnectionExecutor,用于后续扩充连接对象
public HikariPool(final HikariConfig config)
{
   //利用config初始化各种连接池属性,并且产生一个用于生产物理连接的数据源DriverDataSource
   super(config);
   //初始化存放连接对象的核心类ConcurrentBag
   this.connectionBag = new ConcurrentBag<>(this);
   //初始化型号量,用于控制获取连接的速度,isAllowPoolSuspension=true生效
   this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;
   //初始化一个延时任务线程池类型的对象houseKeepingExecutorService,用于后续执行一些延时/定时类任务,
   // 比如连接泄漏检查延时任务,除此之外maxLifeTime后主动回收关闭连接也是交由该对象来执行的
   this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();
   //预热连接池,HikariCP会在该流程的checkFailFast里初始化好一个连接对象放进池子内,
   // 当然触发该流程得保证initializationTimeout > 0(默认值1),
   // 这个配置属性表示留给预热操作的时间,默认值1在预热失败时不会发生重试
   checkFailFast();

   if (config.getMetricsTrackerFactory() != null) {
      setMetricsTrackerFactory(config.getMetricsTrackerFactory());
   }
   else {
      //注入MetricRegistry来实现对连接池指标的收集。这样我们可以较为方便的监控连接池的运行状态
      setMetricRegistry(config.getMetricRegistry());
   }

   setHealthCheckRegistry(config.getHealthCheckRegistry());

   registerMBeans(this);

   ThreadFactory threadFactory = config.getThreadFactory();

   LinkedBlockingQueue<Runnable> addConnectionQueue = new LinkedBlockingQueue<>(config.getMaximumPoolSize());
   this.addConnectionQueue = unmodifiableCollection(addConnectionQueue);
   //初始化一个线程池对象addConnectionExecutor,用于后续扩充连接对象
   this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardPolicy());
   //初始化一个线程池对象closeConnectionExecutor,用于关闭一些连接对象
   this.closeConnectionExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
   //初始化连接泄露告警器
   this.leakTask = new ProxyLeakTask(config.getLeakDetectionThreshold(), houseKeepingExecutorService);
   //初始化扩/缩容定时器
   this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, HOUSEKEEPING_PERIOD_MS, MILLISECONDS);
}

2.3 checkFailFast

与Druid通过initialSize控制预热连接对象数不一样的是,HikariCP仅预热进池一个连接对象。

2.4 ConcurrentBag

ConcurrentBag为整个链接池中最为核心的类,这个类用来存放最终的PoolEntry(原生链接的包装)类型的连接对象,提供了基本的增删查的功能,被HikariPool持有,上面那么多的操作,几乎都是在HikariPool中完成的,HikariPool用来管理实际的连接生产动作和回收动作,实际操作的是ConcurrentBag类。ConcurrentBag是个并发管理类,性能非常好。

2.4.1 状态转换
public static interface IConcurrentBagEntry
{
   // // 闲置
   int STATE_NOT_IN_USE = 0;
   // 使用中
   int STATE_IN_USE = 1;
   // 已废弃
   int STATE_REMOVED = -1;
   // 标记保留,介于闲置和废弃之间的中间状态,主要由缩容那里触发修改
   int STATE_RESERVED = -2;
   //// 利用cas修改连接对象的状态值
   boolean compareAndSet(int expectState, int newState);
   void setState(int newState);
   int getState();
}

2.4.2 成员属性
private static final Logger LOGGER = LoggerFactory.getLogger(ConcurrentBag.class);
// sharedList保存了所有的连接,CopyOnWriteArrayList
private final CopyOnWriteArrayList<T> sharedList;
// 是否弱引用,默认false
private final boolean weakThreadLocals;
// 线程级的缓存,从sharedList拿到的连接对象,使用后会被缓存进当前线程内,borrow时会先从缓存中拿,从而达到池内无锁实现
private final ThreadLocal<List<Object>> threadList;
// 内部接口,HikariPool实现了该接口,主要用于ConcurrentBag主动通知HikariPool触发添加连接对象的异步操作
private final IBagStateListener listener;
// 当前等待获取连接的线程数
private final AtomicInteger waiters;
private volatile boolean closed;
// 这是个即产即销的队列,用于在连接不够用时,及时获取到add方法里新创建的连接对象
private final SynchronousQueue<T> handoffQueue;
2.4.3 borrow()获取链接

com.zaxxer.hikari.util.ConcurrentBag#borrow

  1. 先从ThreadLocal中获取链接
  1. ThreadLocal中获取不到链接,那么从sharedList中获取
  1. sharedList中还获取不到,触发添加链接操作
  1. 在timeout时间内循环,从handoffQueue中poll链接
  1. 超出了timeout时间,返回null

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
{
   // Try the thread-local list first
   // 首先从当前线程的缓存里拿到之前被缓存进来的连接对象集合
   final List<Object> list = threadList.get();
   // 从后往前反向遍历是有好处的, 因为最后一次使用的连接, 空闲的可能性比较大, 之前的连接可能会被其他线程偷窃走了
   for (int i = list.size() - 1; i >= 0; i--) {
      final Object entry = list.remove(i);
      @SuppressWarnings("unchecked")
      // 默认不启用弱引用
      final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
      // 获取到对象后,通过cas尝试把其状态从STATE_NOT_IN_USE 变为 STATE_IN_USE,
      // 注意,这里如果其他线程也在使用这个连接对象,并且成功修改属性,那么当前线程的cas会失败,那么就会继续循环尝试获取下一个连接对象
      if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
         return bagEntry;
      }
   }

   // Otherwise, scan the shared list ... then poll the handoff queue
   // 如果缓存内找不到一个可用的连接对象,则认为需要“回源”,waiters+1
   final int waiting = waiters.incrementAndGet();
   try {
      for (T bagEntry : sharedList) {
         // 循环sharedList,尝试把连接状态值从STATE_NOT_IN_USE 变为 STATE_IN_USE
         if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            // If we may have stolen another waiter's connection, request another bag add.
            // 阻塞线程数大于1时,需要触发HikariPool的addBagItem方法来进行添加连接入池
            if (waiting > 1) {
               listener.addBagItem(waiting - 1);
            }
            // cas设置成功,跟上面的逻辑一样,表示当前线程绕过其他线程干扰,成功获取到该连接对象,直接返回
            return bagEntry;
         }
      }
      // 走到这里说明不光线程缓存里的列表竞争不到连接对象,连sharedList里也找不到可用的连接,这时则认为需要通知HikariPool,该触发添加连接操作了
      listener.addBagItem(waiting);
      // 利用timeout控制获取时间
      timeout = timeUnit.toNanos(timeout);
      do {
         final long start = currentTime();
         // 尝试从handoffQueue队列里获取最新被加进来的连接对象(一般新入的连接对象除了加进sharedList之外,还会被offer进该队列)
         final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
         // 如果超出指定时间后仍然没有获取到可用的连接对象,或者获取到对象后通过cas设置成功,这两种情况都不需要重试,直接返回对象
         if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
         }
         // 走到这里说明从队列内获取到了连接对象,但是cas设置失败,说明又该对象又被其他线程率先拿去用了,若时间还够,则再次尝试获取
         timeout -= elapsedNanos(start);
      } while (timeout > 10_000);

      return null;
   }
   finally {
      // 这一步出去后,HikariPool收到borrow的结果,算是走出阻塞,所以waiters-1
      waiters.decrementAndGet();
   }
}

2.5 FastList

FastList是一个List接口的精简实现,只实现了接口中必要的几个方法。JDK ArrayList每次调用get()方法时都会进行rangeCheck检查索引是否越界,FastList的实现中去除了这一检查,只要保证索引合法那么rangeCheck就成为了不必要的计算开销(当然开销极小)。此外,HikariCP使用List来保存打开的Statement,当Statement关闭或Connection关闭时需要将对应的Statement从List中移除。通常情况下,同一个Connection创建了多个Statement时,后打开的Statement会先关闭。ArrayList的remove(Object)方法是从头开始遍历数组,而FastList是从数组的尾部开始遍历,因此更为高效。

java.util.ArrayList
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
com.zaxxer.hikari.util.FastList
public final class FastList<T> implements List<T>, RandomAccess, Serializable
我们先看一下java.util.ArrayList的get方法
 public E get(int index) { 
     rangeCheck(index); 
     return elementData(index); 
 }
我们可以看到每次get的时候都会进行rangeCheck
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
我们对比一下com.zaxxer.hikari.util.FastList的get方法,可以看到取消了rangeCheck,一定程度下追求了极致
@Override
 public T get(int index)
 {
    return elementData[index];
 }
我们再来看一下ArrayList的remove(Object)方法
public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}
再对比看一下FastList的remove(Object方法)
@Override
public boolean remove(Object element)
{
   for (int index = size - 1; index >= 0; index--) {
      if (element == elementData[index]) {
         final int numMoved = size - index - 1;
         if (numMoved > 0) {
            System.arraycopy(elementData, index + 1, elementData, index, numMoved);
         }
         elementData[--size] = null;
         return true;
      }
   }
   return false;
}

2.6 HouseKeeper

HouseKeeper主要做了三件事

  • 检测时钟回拨
  • 关闭空闲时间过长的链接
  • 添加链接

hikariCP在解决系统时钟被回拨时做出的一种措施,通过流程图可以看到,它是直接把池子里所有的连接对象取出来挨个的标记成废弃,并且尝试把状态值修改为STATE_RESERVED,如果系统时钟没有发生改变,会把当前池内所有处于闲置状态(STATE_NOT_IN_USE)的连接拿出来,然后计算需要检查的范围,然后循环着修改连接的状态

private final class HouseKeeper implements Runnable
{
   private volatile long previous = plusMillis(currentTime(), -HOUSEKEEPING_PERIOD_MS);

   @Override
   public void run()
   {
      try {
         // refresh timeouts in case they changed via MBean
         connectionTimeout = config.getConnectionTimeout();
         validationTimeout = config.getValidationTimeout();
         leakTask.updateLeakDetectionThreshold(config.getLeakDetectionThreshold());

         final long idleTimeout = config.getIdleTimeout();
         final long now = currentTime();

         // Detect retrograde time, allowing +128ms as per NTP spec.
         if (plusMillis(now, 128) < plusMillis(previous, HOUSEKEEPING_PERIOD_MS)) {
            LOGGER.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from pool.",
                        poolName, elapsedDisplayString(previous, now));
            previous = now;
            softEvictConnections();
            fillPool();
            return;
         }
         else if (now > plusMillis(previous, (3 * HOUSEKEEPING_PERIOD_MS) / 2)) {
            // No point evicting for forward clock motion, this merely accelerates connection retirement anyway
            LOGGER.warn("{} - Thread starvation or clock leap detected (housekeeper delta={}).", poolName, elapsedDisplayString(previous, now));
         }

         previous = now;

         String afterPrefix = "Pool ";
         if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
            logPoolState("Before cleanup ");
            afterPrefix = "After cleanup  ";

            connectionBag
               .values(STATE_NOT_IN_USE)
               .stream()
               .sorted(LASTACCESS_REVERSE_COMPARABLE)
               .skip(config.getMinimumIdle())
               .filter(p -> elapsedMillis(p.lastAccessed, now) > idleTimeout)
               .filter(connectionBag::reserve)
               .forEachOrdered(p -> closeConnection(p, "(connection has passed idleTimeout)"));
         }

         logPoolState(afterPrefix);

         fillPool(); // Try to maintain minimum connections
      }
      catch (Exception e) {
         LOGGER.error("Unexpected exception in housekeeping task", e);
      }
   }
}

参考文档

git源码:

alibaba/druid pool analysis · Issue #232 · brettwooldridge/HikariCP · GitHub

https://github.com/alibaba/druid

技术博客

HikariCP 实现原理与源码解析系统 —— 精品合集 | 芋道源码 —— 纯源码解析博客

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值