Day860.高性能数据库连接池HiKariCP -Java 并发编程实战

news2024/11/23 2:21:40

高性能数据库连接池HiKariCP

Hi,我是阿昌,今天学习记录的是关于高性能数据库连接池HiKariCP的内容。

实际工作中,总会难免和数据库打交道;

只要和数据库打交道,就免不了使用数据库连接池

业界知名的数据库连接池有不少,例如 c3p0、DBCP、Tomcat JDBC Connection Pool、Druid 等,不过最近最火的是 HiKariCP

HiKariCP 号称是业界跑得最快的数据库连接池,这两年发展得顺风顺水,尤其是 Springboot 2.0 将其作为默认数据库连接池后,江湖一哥的地位已是毋庸置疑了。


一、什么是数据库连接池

本质上,数据库连接池和线程池一样,都属于池化资源,作用都是避免重量级资源的频繁创建和销毁,对于数据库连接池来说,也就是避免数据库连接频繁创建和销毁。

如下图所示,服务端会在运行期持有一定数量的数据库连接,当需要执行 SQL 时,并不是直接创建一个数据库连接,而是从连接池中获取一个;

当 SQL 执行完,也并不是将数据库连接真的关掉,而是将其归还到连接池中。

数据库连接池示意图
在实际工作中,都是使用各种持久化框架来完成数据库的增删改查,基本上不会直接和数据库连接池打交道,下面的示例代码并没有使用任何框架,而是原生地使用 HiKariCP

执行数据库操作基本上是一系列规范化的步骤:

  1. 通过数据源获取一个数据库连接;
  2. 创建 Statement;
  3. 执行 SQL;
  4. 通过 ResultSet 获取 SQL 执行结果;
  5. 释放 ResultSet;
  6. 释放 Statement;
  7. 释放数据库连接。

下面的示例代码,通过 ds.getConnection() 获取一个数据库连接时,其实是向数据库连接池申请一个数据库连接,而不是创建一个新的数据库连接。

同样,通过 conn.close() 释放一个数据库连接时,也不是直接将连接关闭,而是将连接归还给数据库连接池。

//数据库连接池配置
HikariConfig config = new HikariConfig();
config.setMinimumIdle(1);
config.setMaximumPoolSize(2);
config.setConnectionTestQuery("SELECT 1");
config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource");
config.addDataSourceProperty("url", "jdbc:h2:mem:test");
// 创建数据源
DataSource ds = new HikariDataSource(config);
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
  // 获取数据库连接
  conn = ds.getConnection();
  // 创建Statement 
  stmt = conn.createStatement();
  // 执行SQL
  rs = stmt.executeQuery("select * from abc");
  // 获取结果
  while (rs.next()) {
    int id = rs.getInt(1);
    ......
  }
} catch(Exception e) {
   e.printStackTrace();
} finally {
  //关闭ResultSet
  close(rs);
  //关闭Statement 
  close(stmt);
  //关闭Connection
  close(conn);
}
//关闭资源
void close(AutoCloseable rs) {
  if (rs != null) {
    try {
      rs.close();
    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

HiKariCP 官方网站 解释了其性能之所以如此之高的秘密。

微观上 HiKariCP 程序编译出的字节码执行效率更高,站在字节码的角度去优化 Java 代码,HiKariCP 的作者对性能的执着可见一斑,不过遗憾的是他并没有详细解释都做了哪些优化。而宏观上主要是和两个数据结构有关:

  • FastList
  • ConcurrentBag

二、FastList 解决了哪些性能问题

按照规范步骤,执行完数据库操作之后,需要依次关闭 ResultSet、Statement、Connection,但是总有粗心的同学只是关闭了 Connection,而忘了关闭 ResultSet 和 Statement。为了解决这种问题,最好的办法是当关闭 Connection 时,能够自动关闭 Statement。

为了达到这个目标,Connection 就需要跟踪创建的 Statement,最简单的办法就是将创建的 Statement 保存在数组 ArrayList 里,这样当关闭 Connection 的时候,就可以依次将数组中的所有 Statement 关闭。

HiKariCP 觉得用 ArrayList 还是太慢,当通过 conn.createStatement() 创建一个 Statement 时,需要调用 ArrayList 的 add() 方法加入到 ArrayList 中,这个是没有问题的;但是当通过 stmt.close() 关闭 Statement 的时候,需要调用 ArrayList 的 remove() 方法来将其从 ArrayList 中删除,这里是有优化余地的。

假设一个 Connection 依次创建 6 个 Statement,分别是 S1、S2、S3、S4、S5、S6,按照正常的编码习惯,关闭 Statement 的顺序一般是逆序的,关闭的顺序是:S6、S5、S4、S3、S2、S1,而 ArrayList 的 remove(Object o) 方法是顺序遍历查找,逆序删除而顺序查找,这样的查找效率就太慢了。

如何优化呢?很简单,优化成逆序查找就可以了。

逆序删除示意图

HiKariCP 中的 FastList 相对于 ArrayList 的一个优化点就是将 remove(Object element) 方法的查找顺序变成了逆序查找

除此之外,FastList 还有另一个优化点,是 get(int index) 方法没有对 index 参数进行越界检查,HiKariCP 能保证不会越界,所以不用每次都进行越界检查。

整体来看,FastList 的优化点还是很简单的。


三、ConcurrentBag 解决了哪些性能问题

如果让自己来实现一个数据库连接池,最简单的办法就是用两个阻塞队列来实现:

  • 一个用于保存空闲数据库连接的队列 idle
  • 另一个用于保存忙碌数据库连接的队列 busy;

获取连接时将空闲的数据库连接从 idle 队列移动到 busy 队列,而关闭连接时将数据库连接从 busy 移动到 idle。

这种方案将并发问题委托给了阻塞队列,实现简单,但是性能并不是很理想。

因为 Java SDK 中的阻塞队列是用锁实现的,而高并发场景下锁的争用对性能影响很大。

//忙碌队列
BlockingQueue<Connection> busy;
//空闲队列
BlockingQueue<Connection> idle;

HiKariCP 并没有使用 Java SDK 中的阻塞队列,而是自己实现了一个叫做 ConcurrentBag 的并发容器

ConcurrentBag 的设计最初源自 C#,它的一个核心设计是使用 ThreadLocal 避免部分并发问题,不过 HiKariCP 中的 ConcurrentBag 并没有完全参考 C# 的实现,下面来看看它是如何实现的。

ConcurrentBag 中最关键的属性有 4 个,分别是:

  • 用于存储所有的数据库连接的共享队列 sharedList
  • 线程本地存储 threadList
  • 等待数据库连接的线程数 waiters
  • 分配数据库连接的工具 handoffQueue。

其中,handoffQueue 用的是 Java SDK 提供的 SynchronousQueue,SynchronousQueue 主要用于线程之间传递数据。

//用于存储所有的数据库连接
CopyOnWriteArrayList<T> sharedList;
//线程本地存储中的数据库连接
ThreadLocal<List<Object>> threadList;
//等待数据库连接的线程数
AtomicInteger waiters;
//分配数据库连接的工具
SynchronousQueue<T> handoffQueue;

当线程池创建了一个数据库连接时,通过调用 ConcurrentBag 的 add() 方法加入到 ConcurrentBag 中,下面是 add() 方法的具体实现,逻辑很简单,就是将这个连接加入到共享队列 sharedList 中,如果此时有线程在等待数据库连接,那么就通过 handoffQueue 将这个连接分配给等待的线程。

//将空闲连接添加到队列
void add(final T bagEntry){
  //加入共享队列
  sharedList.add(bagEntry);
  //如果有等待连接的线程,
  //则通过handoffQueue直接分配给等待的线程
  while (waiters.get() > 0 
    && bagEntry.getState() == STATE_NOT_IN_USE 
    && !handoffQueue.offer(bagEntry)) {
      yield();
  }
}

通过 ConcurrentBag 提供的 borrow() 方法,可以获取一个空闲的数据库连接,borrow() 的主要逻辑是:

  1. 首先查看线程本地存储是否有空闲连接,如果有,则返回一个空闲的连接;
  2. 如果线程本地存储中无空闲连接,则从共享队列中获取。
  3. 如果共享队列中也没有空闲的连接,则请求线程需要等待。

需要注意的是,线程本地存储中的连接是可以被其他线程窃取的,所以需要用 CAS 方法防止重复分配。

sharedlist和其他线程的threadlocal里有可能都有同一个连接,从前者取到连接,就相当于窃取了后者

在共享队列中获取空闲连接,也采用了 CAS 方法防止重复分配。

T borrow(long timeout, final TimeUnit timeUnit){
  // 先查看线程本地存储是否有空闲连接
  final List<Object> list = threadList.get();
  for (int i = list.size() - 1; i >= 0; i--) {
    final Object entry = list.remove(i);
    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;
    }
  }

  // 线程本地存储中无空闲连接,则从共享队列中获取
  final int waiting = waiters.incrementAndGet();
  try {
    for (T bagEntry : sharedList) {
      //如果共享队列中有空闲连接,则返回
      if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
        return bagEntry;
      }
    }
    
    //共享队列中没有连接,则需要等待
    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);
    //超时没有获取到连接,返回null
    return null;
  } finally {
    waiters.decrementAndGet();
  }
}

释放连接需要调用 ConcurrentBag 提供的 requite() 方法,该方法的逻辑很简单,首先将数据库连接状态更改为 STATE_NOT_IN_USE,之后查看是否存在等待线程,如果有,则分配给等待线程;如果没有,则将该数据库连接保存到线程本地存储里。

//释放连接
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 中的 FastList ConcurrentBag 这两个数据结构使用得非常巧妙,虽然实现起来并不复杂,但是对于性能的提升非常明显,根本原因在于这两个数据结构适用于数据库连接池这个特定的场景。

FastList 适用于逆序删除场景;而 ConcurrentBag 通过 ThreadLocal 做一次预分配,避免直接竞争共享资源,非常适合池化资源的分配。

在实际工作中,遇到的并发问题千差万别,这时选择合适的并发数据结构就非常重要了。

当然能选对的前提是对特定场景的并发特性有深入的了解,只有了解到无谓的性能消耗在哪里,才能对症下药。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/165267.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023/1/15 JS-作用域与作用域链

1 作用域 理解 - 就是一块"地盘", 一个代码段所在的区域&#xff0c;它是静态的(相对于上下文对象), 在编写代码时就确定了 分类&#xff1a; 全局作用域函数作用域 没有块作用域(ES6有了) -->(java语言也有) if (true) {var a 3}console.log(a); // 3作用&am…

用互联网思维做产品,做超出用户预期的产品

做出超出用户预期的产品&#xff0c;做出让用户惊喜和兴奋的产品。超越用户期望极致体验极致服务极致产品(极致产品功能情感温度)关心、关注、尊重用户理解用户理解人性用户参与。只有深刻了解&#xff0c;深刻理解用户&#xff0c;深刻理解人性&#xff0c;才能做出好的产品。…

Maven安装教程讲解

目录一、下载安装JDK二、下载 Maven三、配置 Maven 环境变量四、配置 Maven 仓库地址五、配置 Maven 镜像六、配置 Maven JDK七、IDE配置 Maven八、IDE新建 Maven 项目九、IDE执行 Maven 命令一、下载安装JDK 教程&#xff1a;https://blog.csdn.net/weixin_43888891/article/…

【阶段四】Python深度学习09篇:深度学习项目实战:循环神经网络处理时序数据项目实战:CNN和RNN组合模型

本篇的思维导图: 项目背景 时间序列数据集中的所有数据都伴随着一个时戳,比如股票、天气数据。这个数据集,是科学家们多年间用开普勒天文望远镜观察并记录下来的银河系中的一些恒星的亮度。广袤的宇宙,浩瀚的星空在过去很长一段时间里,人类是没有办法证明系外行星…

关于一次python服务性能的问题分析定位

今天项目遇到一个比较棘手的问题&#xff0c;我们做的接口平台&#xff0c;提供了一个给用户自己编辑关键字的逻辑&#xff0c;发现对应服务的cpu基本都满了&#xff0c;并且通过扩容的方式也没有好转&#xff0c;也同样被打满。 找了测试的同学了解了下&#xff0c;发现他们使…

Enterprise JavaBean 简介

Enterprise JavaBean 简介 在顶层我们使用的框架是面向服务的&#xff0c;而在其之下的开发方法面向组件&#xff0c;最基层的便是面向对象面向过程的JAVA编程。 面向过程和面向对象是两种具体程序设计时的编程范式&#xff0c;他们没有本质区别&#xff0c;只是使用不同的方…

【Linux】Linux命令行git和Linux调试器-gdb的使用

文章目录一、Linux命令行git1. git是什么2. Gitee上创建仓库&#xff0c;克隆仓库到本地3. git三板斧二、Linux调试器-gdb的使用1. debug和release的区别2. 常用命令一、Linux命令行git 1. git是什么 git是一个开源的分布式版本控制系统&#xff0c;可以有效、高速地处理从很…

分享52个Java源码,总有一款适合您

Java源码 分享52个Java源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c;大家下载后可以看到。 源码下载链接&#xff1a;https://pan.baidu.com/s/1YpNL5QWwQ18Y-KRmFHFs5g?pwdqc8w …

吴恩达机器学习课程笔记:监督学习、无监督学习

1.吴恩达机器学习课程笔记&#xff1a;监督学习、无监督学习 吴恩达机器学习系列课程&#xff1a;监督学习 吴恩达机器学习系列课程&#xff1a;无监督学习 仅作为个人学习笔记&#xff0c;若各位大佬发现错误请指正 机器学习的学习算法&#xff1a;监督学习、无监督学习、半…

1591_AURIX_TC275_PMU_Flash的保护

全部学习汇总&#xff1a; GreyZhang/g_TC275: happy hacking for TC275! (github.com) 可以支持保护功能的Flash是前面文档中呼出来的几块&#xff0c;其中DFlash由于多模块公用&#xff0c;还有具体的拆分。PFlash可以进行两方面的保护&#xff0c;一个是错误的读取方式保护&…

python列表(list)底层实现

list Python内存管理中的基石 Python中所有类型创建对象时&#xff0c;底层都是与PyObject和PyVarObject结构体实现&#xff0c;一般情况下由单个元素组成对象内部会使用PyObject结构体&#xff08;float&#xff09;、由多个元素组成的对象内部会使用PyVarObject结构体 2个…

应用层——电子邮件(SMTP、POP3、IMAP)

目录 1. 电子邮件系统及组成结构 1.1 电子邮件 1.2 电子邮件系统的组件 2. SMTP(邮件发送协议) 2.1 SMTP的特征 2.2 SMTP的基本操作 2.3 SMTP协议的基本流程 2.4 SMTP交互与应答 2.5 SMTP与HTTP的对比 3. 邮件报文格式与MIME 3.1 电子邮件格式 3.2 多用途网际…

STM32时钟系统原理和作用

时钟对于单片机来讲类似于心脏对于人体&#xff0c;单片机通过时钟信号控制单片机工作。 1.时钟系统工作流程 时钟源输入----被分频或倍频----输出至系统/外设/外部接口 2.时钟分类 RC时钟精度低&#xff0c;体积小&#xff0c; OSC时钟精度高&#xff0c;体积大。 3.部分…

5.多层感知机

5.多层感知机 目录 感知机 基本内容训练感知机感知机存在的问题总结 多层感知机 隐藏层 单隐藏层-单分类 激活函数 ReLU函数sigmoid函数tanh函数总结 多类分类 多层感知机的从零开始实现 初始化模型参数激活函数模型损失函数训练 多层感知机的简洁实现 模型模型选择、欠拟合和…

⭐基于unity物体移动的方法API的总结-GIF图文详解⭐

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 收录于专栏 unity常用API ⭐相关文章&#xff1a; 线性差值函数以及平滑阻尼的运用和实践&#xff08;Lerp AND SmoothDa…

文献阅读:Towards Out-of-Distribution Sequential Event Prediction:A Causal Treatment

文献阅读&#xff1a;Towards Out-of-Distribution Sequential Event Prediction&#xff1a;A Causal Treatment 1. 问题描述2. 方法 & 模型设计 1. loss定义2. 模型设计 3. 实验 & 结果 1. 基础实验2. CaseQ策略细节分析 4. 结论 & 总结 文献链接&#xff1a;ht…

GDB调试学习

文章目录简介准备工作常用命令简介 ​GDB 全称“GNU symbolic debugger”&#xff0c;它诞生于 GNU 计划&#xff08;同时诞生的还有 GCC、Emacs 等&#xff09;&#xff0c;是 Linux 下常用的程序调试器。发展至今&#xff0c;GDB 已经迭代了诸多个版本&#xff0c;当下的 GD…

五个了解自己天赋优势的分析工具(二)盖洛普优势测评

盖洛普优势测评 盖洛普优势识别器&#xff08;也叫克利夫顿优势评估&#xff09;&#xff0c;是盖洛普公司旗下⼀款王牌产品&#xff0c;是⼀个专⻔⽤来识别⼀个⼈隐形天赋、潜在优势、驱动⼒来源的⼯具&#xff0c;它可以帮助⼀个⼈科学认识⾃⼰。 它把⼈的思维⽅式、感受形…

网络初识

一些基本概念认识IP地址概念&#xff1a;IP地址用于定位主机的网络地址&#xff0c;标识了网络上设备所在的位置。也就是说主机在哪。格式&#xff1a;IP地址是一个32位的二进制数&#xff0c;通常被分割为4个字节。通常我们使用“点分十进制”的方式来表示主机的IP地址——>…

2.2 SQL入门和实战

文章目录数据库介绍MySQL的入门应用SQL的基础与DDLSQL的分类&#xff1a;SQL的基础语法特性&#xff1a;DDL库管理&#xff1a;DDL表管理&#xff1a;SQL-DMLSQL-DQLDQL基础查询DQL分组聚合排序分页Python&MySQLcommit数据库介绍 数据库的层级&#xff1a;按照库->表-&…