SpringBoot 2.0에서 HikariCP 데이터베이스 연결 풀의 원리 분석

백그라운드 서비스 개발로 일상 업무에서 매일 데이터베이스를 다루고 있으며 다양한 CRUD 작업을 수행하고 있으며 데이터베이스 커넥션 풀을 사용할 것입니다. 개발 역사에 따르면 업계에서 잘 알려진 데이터베이스 커넥션 풀에는 c3p0, DBCP, Tomcat JDBC 커넥션 풀, Druid 등이 있지만 최근 가장 인기있는 것은 HiKariCP입니다.

HiKariCP는 업계에서 가장 빠른 데이터베이스 연결 풀로 알려져 있으며, SpringBoot 2.0이이를 기본 데이터베이스 연결 풀로 채택한 이후 개발 모멘텀은 멈출 수 없었습니다. 왜 그렇게 빠른가요? 오늘 우리는 이유에 초점을 맞출 것입니다.

첫째, 데이터베이스 연결 풀이 란

HiKariCP를 설명하기 전에 데이터베이스 커넥션 풀 (Database Connection Pooling)이 무엇인지, 왜 데이터베이스 커넥션 풀이 있는지에 대해 간략히 소개하겠습니다.

기본적으로 말하면 일반적으로 사용되는 스레드 풀과 같은 데이터베이스 연결 풀은 풀링 된 리소스로, 프로그램이 초기화 될 때 특정 개수의 데이터베이스 연결 개체를 생성하여 메모리 영역에 저장합니다. 응용 프로그램이 기존 데이터베이스 연결을 재사용 할 수 있도록합니다. SQL을 실행해야하는 경우 데이터베이스 연결을 다시 설정하는 대신 연결 풀에서 직접 연결을 얻습니다. SQL을 실행하면 데이터베이스 연결이 true가 아닙니다. 꺼져 있지만 데이터베이스 연결 풀로 되돌립니다. 데이터베이스에 대한 액세스 수가 특정 제어 가능한 범위 내에 있도록 연결 풀의 매개 변수를 구성하여 연결 풀의 초기 연결 수, 최소 연결, 최대 연결, 최대 유휴 시간 및 기타 매개 변수를 제어 할 수 있습니다. 시스템 충돌을 방지하고 좋은 사용자 경험을 보장합니다. 데이터베이스 연결 풀 다이어그램은 다음과 같습니다.

SpringBoot 2.0에서 HikariCP 데이터베이스 연결 풀의 원리 분석

따라서 데이터베이스 연결 풀 사용의 핵심 기능은 데이터베이스 연결의 빈번한 생성 및 파괴를 방지하고 시스템 오버 헤드를 줄이는 것입니다. 데이터베이스 연결은 제한적이고 비용이 많이 들기 때문에 데이터베이스 연결을 만들고 해제하는 데 많은 시간이 소요됩니다. 이러한 작업을 자주 수행하면 많은 성능 오버 헤드가 발생하여 웹 사이트 응답 속도가 느려지고 서버가 중단 될 수도 있습니다.

2. 공통 데이터베이스 연결 풀의 비교 분석

일반적인 데이터베이스 커넥션 풀의 다양한 기능 비교에 대한 자세한 요약입니다. 현재 주류 인 Alibaba Druid와 HikariCP를 분석하는 데 중점을 둡니다. HikariCP는 Druid 커넥션 풀보다 성능이 완전히 뛰어납니다. Druid의 성능은 잠금 메커니즘이 다르기 때문에 약간 나 빠지며, Druid는 모니터링, SQL 가로 채기, 파싱 등 더 풍부한 기능을 제공합니다. 두 가지의 초점이 다릅니다. HikariCP는 최고의 고성능을 추구합니다.

SpringBoot 2.0에서 HikariCP 데이터베이스 연결 풀의 원리 분석

다음은 공식 웹 사이트에서 제공하는 성능 비교 차트입니다. 성능 측면에서 5 개의 데이터베이스 연결 풀 순서는 다음과 같습니다. HikariCP> druid> tomcat-jdbc> dbcp> c3p0 :

SpringBoot 2.0에서 HikariCP 데이터베이스 연결 풀의 원리 분석

3. HikariCP 데이터베이스 커넥션 풀 소개

HikariCP는 역사상 최고의 데이터베이스 연결 풀이라고 주장하며 SpringBoot 2.0은이를 기본 데이터 소스 연결 풀로 설정합니다. 다른 커넥션 풀에 비해 Hikari는 성능이 훨씬 높은데 어떻게하면 되나요? HikariCP 공식 웹 사이트의 소개를 보면 HikariCP의 최적화가 다음과 같이 요약됩니다.

1. 바이트 코드 간소화 : 최적화 된 코드, 컴파일 후 바이트 코드의 양이 매우 적기 때문에 CPU 캐시가 더 많은 프로그램 코드를로드 할 수 있습니다.

HikariCP는 또한 타사 Java 바이트 코드를 사용하여 클래스 라이브러리 Javassist를 수정하여 위임 된 동적 프록시를 생성함으로써 바이트 코드를 최적화하고 간소화하기 위해 많은 노력을 기울였습니다. 동적 프록시 구현은 JDK에 비해 더 빠른 ProxyFactory 클래스에 있습니다. Proxy Less 바이트 코드가 생성되고 많은 불필요한 바이트 코드가 간소화됩니다.

2. 프록시 및 인터셉터 최적화 : 코드를 줄입니다. 예를 들어 HikariCP의 Statement 프록시에는 100 줄의 코드 만 있고 BoneCP의 10 분의 1 만 있습니다.

3. ArrayList 대신 사용자 지정 배열 유형 (FastStatementList) : ArrayList의 get () 때마다 범위 검사를 피하고 remove () 호출시 처음부터 끝까지 스캔하지 마십시오 (연결 특성은 연결이 확보 된 후 연결이 해제된다는 것입니다. ) ;

4. 사용자 지정 컬렉션 유형 (ConcurrentBag) : 동시 읽기 및 쓰기의 효율성을 향상시킵니다.

5. BoneCP 결함에 대한 기타 최적화 ( 예 : 둘 이상의 CPU 시간 슬라이스를 사용하는 메소드 호출 연구).

물론 데이터베이스 커넥션 풀로서 조만간 소비자들의 존경을받을 것이라고는 말할 수 없으며, 견고 함과 안정성도 매우 뛰어납니다. HikariCP는 15 년 만에 출시 된 이래 광범위한 애플리케이션 시장의 테스트를 견뎌 왔으며 SpringBoot2.0에서 기본 데이터베이스 연결 풀로 성공적으로 승격되었으며 신뢰성 측면에서 신뢰할 수 있습니다. 둘째, 적은 양의 코드, 적은 양의 CPU 및 메모리로 실행 률이 매우 높습니다. 마지막으로 Spring 구성 HikariCP와 druid는 기본적으로 차이가 없으며 마이그레이션이 매우 편리하기 때문에 현재 HikariCP가 인기있는 이유입니다.

간소화 된 바이트 코드, 최적화 된 프록시 및 인터셉터, 맞춤형 배열 유형.

네, HikariCP 핵심 소스 코드 분석

4.1 FastList가 성능 문제를 최적화하는 방법

 먼저 데이터베이스 운영 표준화를 수행하는 단계를 살펴 보겠습니다.

  1. 데이터 소스를 통해 데이터베이스 연결을 확보하십시오.

  2. 성명서 작성;

  3. SQL 실행;

  4. ResultSet을 통해 SQL 실행 결과를 가져옵니다.

  5. ResultSet을 해제하십시오.

  6. 릴리스 성명;

  7. 데이터베이스 연결을 해제하십시오.

모든 데이터베이스 연결 풀은 현재이 순서에 따라 엄격하게 데이터베이스 작업을 수행합니다. 최종 해제 작업을 방지하기 위해 다양한 데이터베이스 연결 풀은 생성 된 Statement를 ArrayList 배열에 저장하여 연결이 닫힐 때 모두 해제 할 수 있습니다. 차례로 배열의 문. 이 단계를 처리 할 때 HiKariCP는 ArrayList의 일부 메서드 작업에 최적화의 여지가 있다고 생각하므로 List 인터페이스의 단순화 된 구현은 List 인터페이스의 여러 핵심 메서드에 최적화되어 있으며 다른 부분은 기본적으로 ArrayList와 동일합니다.

첫 번째는 get () 메서드입니다. ArrayList는 get () 메서드가 호출 될 때마다 rangeCheck를 수행하여 인덱스가 범위를 벗어 났는지 확인합니다. 데이터베이스 연결 풀이 다음의 적법성을 충족하므로 FastList 구현에서는이 검사가 제거됩니다. 현재 rangeCheck는 잘못된 계산 오버 헤드이므로 매번 경계를 체크 아웃 할 필요가 없습니다. 잦은 유효하지 않은 작업을 제거하면 성능 소비를 크게 줄일 수 있습니다.

  • FastList get () 작업
public T get(int index)
{
   // ArrayList 在此多了范围检测 rangeCheck(index);
   return elementData[index];
}

두 번째는 remove 메소드입니다 .conn.createStatement ()를 통해 Statement를 생성 할 때 ArrayList의 add () 메소드를 호출하여 ArrayList에 추가해야합니다. 이는 문제가되지 않지만 stmt.close를 통해 Statement를 닫을 때 (), ArrayList의 remove () 메서드를 호출하여 ArrayList에서 제거해야하며, ArrayList의 remove (Object) 메서드는 처음부터 배열을 탐색하고 FastList는 배열의 끝에서 탐색하므로 더 효율적입니다. Connection이 6 개의 Statement를 순차적으로 생성한다고 가정합니다. 즉 S1, S2, S3, S4, S5, S6, 그리고 Statements를 닫는 순서는 일반적으로 S6에서 S1로 반대로 바뀌고 ArrayList의 remove (Object o) 메소드는 순서입니다. 트래버스 검색, 역 삭제 및 순차 검색, 검색 효율성이 너무 느립니다. 따라서 FastList는이를 최적화하고 역 검색으로 변경합니다. 다음 코드는 FastList에서 구현 한 데이터 제거 작업으로 ArrayList의 remove () 코드에 비해 검사 범위와 검사 요소를 처음부터 끝까지 순회하는 단계를 제거하여 성능이 더 빠릅니다.

SpringBoot 2.0에서 HikariCP 데이터베이스 연결 풀의 원리 분석

  • FastList 삭제 작업
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);
         }
         //如果角标是最后面的 直接初始化为null
         elementData[--size] = null;
         return true;
      }
   }
   return false;
}

위의 소스 코드 분석을 통해 FastList의 최적화 포인트는 여전히 매우 간단합니다. ArrayList에 비해 랙 검사, 확장 최적화 등과 같은 사소한 조정 만 제거되며, 삭제시 어레이를 탐색하여 요소 및 기타 사소한 조정을 찾아 궁극적 인 성능을 추구합니다. 물론 FastList의 ArrayList 최적화는 ArrayList가 좋지 않다고 말할 수 없습니다. 소위 포지셔닝이 다르고 추구하는 방식이 다릅니다. 일반 컨테이너로서 ArrayList는보다 안전하고 안정적입니다. 동작 전 rangeCheck를 확인하고, Fail-Fast 메커니즘에 더 부합하는 불법 요청에 직접 예외를 던집니다. , FastList는 최고의 성능을 추구합니다.

HiKariCP의 또 다른 데이터 구조 인 ConcurrentBag에 대해 이야기하고 성능을 향상시키는 방법을 살펴 보겠습니다.

4.2 ConcurrentBag 실현 원리 분석

현재 주류 데이터베이스 연결 풀 구현 방법은 대부분 두 개의 차단 대기열로 구현됩니다. 하나는 대기열에 유휴 데이터베이스 연결을 저장하는 데 사용되고 다른 하나는 사용중인 대기열에 사용중인 데이터베이스 연결을 저장하는 데 사용됩니다. 연결이 확보되면 유휴 데이터베이스 연결이 유휴 대기열에서 사용중인 대기열로 이동하고 연결이 닫히면 데이터베이스 연결이 사용 중에서 유휴 상태로 이동합니다. 이 방식은 동시성 문제를 블로킹 큐에 위임하는데, 이는 구현이 간단하지만 성능이 그다지 만족스럽지 않습니다. Java SDK의 차단 대기열은 잠금으로 구현되고 동시성이 높은 시나리오의 잠금 경합은 성능에 큰 영향을 미칩니다.

HiKariCP는 Java SDK에서 차단 큐를 사용하지 않고 대신 ConcurrentBag라는 동시 컨테이너를 자체 구현하여 연결 풀 구현 (다중 스레드 데이터 상호 작용)에서 LinkedBlockingQueue 및 LinkedTransferQueue보다 성능이 더 우수합니다.

ConcurrentBag에는 4 가지 가장 중요한 속성이 있습니다. 즉, 모든 데이터베이스 연결을 저장하는 데 사용되는 공유 큐 sharedList, 스레드 로컬 스토리지 threadList, 데이터베이스 연결을 기다리는 스레드 수, 대기자 및 데이터베이스 연결 할당을위한 도구 handoffQueue입니다. 이 중 handoffQueue는 Java SDK에서 제공하는 SynchronousQueue를 사용하며 SynchronousQueue는 주로 쓰레드 간 데이터 전송에 사용됩니다.

  • ConcurrentBag의 주요 속성
// 存放共享元素,用于存储所有的数据库连接
private final CopyOnWriteArrayList<T> sharedList;
// 在 ThreadLocal 缓存线程本地的数据库连接,避免线程争用
private final ThreadLocal<List<Object>> threadList;
// 等待数据库连接的线程数
private final AtomicInteger waiters;
// 接力队列,用来分配数据库连接
private final SynchronousQueue<T> handoffQueue;

ConcurrentBag는 add () 메서드를 통해서만 모든 리소스를 추가 할 수 있도록 보장합니다. 스레드 풀이 데이터베이스 연결을 생성 할 때 ConcurrentBag의 add () 메서드를 호출하여 ConcurrentBag에 추가하고 remove () 메서드를 통해 제거합니다. 다음은 add () 메소드 및 remove () 메소드의 구체적인 구현입니다. 추가시 공유 대기열 sharedList에 연결이 추가됩니다. 이때 데이터베이스 연결을 기다리는 스레드가 있으면 연결이 할당됩니다. handoffQueue 스레드를 통해 대기합니다.

  • ConcurrentBag의 add () 및 remove () 메서드
public void add(final T bagEntry)
{
   if (closed) {
      LOGGER.info("ConcurrentBag has been closed, ignoring add()");
      throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()");
   }
   // 新添加的资源优先放入sharedList
   sharedList.add(bagEntry);

   // 当有等待资源的线程时,将资源交到等待线程 handoffQueue 后才返回
   while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) {
      yield();
   }
}
public boolean remove(final T bagEntry)
{
   // 如果资源正在使用且无法进行状态切换,则返回失败
   if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && !closed) {
      LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry);
      return false;
   }
   // 从sharedList中移出
   final boolean removed = sharedList.remove(bagEntry);
   if (!removed && !closed) {
      LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);
   }
   return removed;
}

동시에 ConcurrentBag는 제공된 차입 () 메서드를 통해 유휴 데이터베이스 연결을 얻고 return () 메서드를 통해 자원을 복구합니다 .borrow ()의 주요 논리는 다음과 같습니다.

  1. 스레드 로컬 스토리지 threadList에 유휴 연결이 있는지 확인하십시오. 그렇다면 유휴 연결을 리턴하십시오.
  2. 스레드 로컬 스토리지에 유휴 연결이 없으면 공유 큐 sharedList에서 가져옵니다.
  3. 공유 큐에 사용 가능한 연결이 없으면 요청 스레드가 기다려야합니다.
  • ConcurrentBag의 Borrow () 및 requite () 메서드
// 该方法会从连接池中获取连接, 如果没有连接可用, 会一直等待timeout超时
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
{
   // 首先查看线程本地资源threadList是否有空闲连接
   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方法防止重复分配
      if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
         return bagEntry;
      }
   }
   // 当无可用本地化资源时,遍历全部资源,查看可用资源,并用CAS方法防止资源被重复分配
   final int waiting = waiters.incrementAndGet();
   try {
      for (T bagEntry : sharedList) {
         if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            // 因为可能“抢走”了其他线程的资源,因此提醒包裹进行资源添加
            if (waiting > 1) {
               listener.addBagItem(waiting - 1);
            }
            return bagEntry;
         }
      }

      listener.addBagItem(waiting);
      timeout = timeUnit.toNanos(timeout);
      do {
         final long start = currentTime();
         // 当现有全部资源都在使用中时,等待一个被释放的资源或者一个新资源
         final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
         if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
         }
         timeout -= elapsedNanos(start);
      } while (timeout > 10_000);
      return null;
   }
   finally {
      waiters.decrementAndGet();
   }
}

public void requite(final T bagEntry)
{
   // 将资源状态转为未在使用
   bagEntry.setState(STATE_NOT_IN_USE);
   // 判断是否存在等待线程,若存在,则直接转手资源
   for (int i = 0; waiters.get() > 0; i++) {
      if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
         return;
      }
      else if ((i & 0xff) == 0xff) {
         parkNanos(MICROSECONDS.toNanos(10));
      }
      else {
         yield();
      }
   }
   // 否则,进行资源本地化处理
   final List<Object> threadLocalList = threadList.get();
   if (threadLocalList.size() < 50) {
      threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
   }
}

차입 () 메서드는 전체 HikariCP에서 가장 핵심적인 메서드라고 할 수 있으며, 커넥션 풀에서 연결을 얻을 때 결국 호출하게 될 메서드입니다. 차입 () 메서드는 객체 참조 만 제공하고 객체를 제거하지 않으므로 사용시 return () 메서드를 통해 되돌려 야합니다. 그렇지 않으면 메모리 누수가 발생하기 쉽습니다. requite () 메서드는 먼저 데이터베이스 연결 상태를 사용되지 않음으로 변경 한 다음 대기중인 스레드가 있는지 확인하고 대기 스레드가있는 경우 대기중인 스레드에 할당되고 그렇지 않으면 데이터베이스 연결이 스레드 로컬 저장소에 저장됩니다.

ConcurrentBag 구현은 큐 탈취 메커니즘을 사용하여 요소를 가져옵니다. 먼저 ThreadLocal에서 현재 스레드에 속하는 요소를 가져와 잠금 경쟁을 피하고 사용 가능한 요소가없는 경우 공유 CopyOnWriteArrayList에서 다시 가져옵니다. 또한 ThreadLocal 및 CopyOnWriteArrayList는 모두 ConcurrentBag의 멤버 변수이며 스레드간에 공유되지 않으므로 잘못된 공유를 방지 할 수 있습니다. 동시에 스레드 로컬 스토리지의 연결이 다른 스레드에 의해 도난 당할 수 있고 공유 큐에서 유휴 연결이 확보되기 때문에 중복 할당을 방지하기 위해 CAS 방법이 필요합니다. 

다섯, 요약

SpringBoot2.0의 기본 연결 풀인 Hikari는 현재 업계에서 널리 사용되고 있으며 대부분의 비즈니스에서 빠르게 액세스하여 효율적인 연결을 달성하는 데 사용할 수 있습니다.

참고

  1. https://github.com/brettwooldridge/HikariCP

  2. https://github.com/alibaba/druid

저자 : 생체 게임 기술 팀

추천

출처blog.51cto.com/14291117/2606509