HikariCP是什么,springmvc实战百度云

2021年09月15日 阅读数:5
这篇文章主要向大家介绍HikariCP是什么,springmvc实战百度云,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

由于须要在web环境中使用,若是直接建类写个main方法测试,会一直报错的,目前没找到好的办法。这里就简单地使用jsp来测试吧。java

<body>
    <%
        String jndiName = "java:comp/env/jdbc/druid-test";

        InitialContext ic = new InitialContext();
        // 获取JNDI上的ComboPooledDataSource
        DataSource ds = (DataSource) ic.lookup(jndiName);

        JDBCUtils.setDataSource(ds);

        // 建立sql
        String sql = "select * from demo_user where deleted = false";
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;

        // 查询用户
        try {
            // 得到链接
            connection = JDBCUtils.getConnection();
            // 得到Statement对象
            statement = connection.prepareStatement(sql);
            // 执行
            resultSet = statement.executeQuery();
            // 遍历结果集
            while(resultSet.next()) {
                String name = resultSet.getString(2);
                int age = resultSet.getInt(3);
                System.err.println("用户名:" + name + ",年龄:" + age);
            }
        } catch(SQLException e) {
            System.err.println("查询用户异常");
        } finally {
            // 释放资源
            JDBCUtils.release(connection, statement, resultSet);
        }
    %>
</body>

测试结果

打包项目在tomcat9上运行,访问 ?http://localhost:8080/hikari-demo/testJNDI.jsp ,控制台打印以下内容:mysql

用户名:zzs001,年龄:18
用户名:zzs002,年龄:18
用户名:zzs003,年龄:25
用户名:zzf001,年龄:26
用户名:zzf002,年龄:17
用户名:zzf003,年龄:18

使用例子-经过JMX管理链接池

需求

开启 HikariCP 的 JMX 功能,并使用 jconsole 查看。git

修改hikari.properties

在例子一基础上增长以下配置。这要设置 registerMbeans 为 true,JMX 功能就会开启。github

#-------------JMX--------------------------------
#是否容许经过JMX挂起和恢复链接池
#默认为false
allowPoolSuspension=false

#是否开启JMX
#默认false
registerMbeans=true

#数据源名,通常用于JMX。
#默认自动生成
poolName=zzs001

编写测试类

为了查看具体效果,这里让主线程进入睡眠,避免结束。web

    public static void main(String[] args) throws InterruptedException {
        new HikariDataSourceTest().findAll();
        Thread.sleep(60 * 60 * 1000);
    }

使用jconsole查看

运行项目,打开 jconsole,选择咱们的项目后点链接,在 MBean 选项卡能够看到咱们的项目。经过 PoolConfig 能够动态修改配置(只有部分参数容许修改);经过 Pool 能够获取链接池的链接数(活跃、空闲和全部)、获取等待链接的线程数、挂起和恢复链接池、丢弃未使用链接等。面试

HikariCP是什么,springmvc实战百度云

配置文件详解编写

相比其余链接池,HikariCP 的配置参数很是简单,其中有几个功能须要注意:HikariCP 强制开启借出测试和空闲测试,不开启回收测试,可选的只有泄露测试。spring

数据库链接参数

注意,这里在url后面拼接了多个参数用于避免乱码、时区报错问题。?补充下,若是不想加入时区的参数,能够在mysql命令窗口执行以下命令:set global time_zone='+8:00'。sql

#-------------基本属性--------------------------------
jdbcUrl=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
username=root
password=root
#JDBC驱动使用的Driver实现类类名
#默认为空。会根据jdbcUrl来解析
driverClassName=com.mysql.cj.jdbc.Driver

链接池数据基本参数

这两个参数都比较经常使用,建议根据具体项目调整。数据库

#-------------链接池大小相关参数--------------------------------
#最大链接池数量
#默认为10。可经过JMX动态修改
maximumPoolSize=10

#最小空闲链接数量
#默认与maximumPoolSize一致。可经过JMX动态修改
minimumIdle=0

链接检查参数

针对链接失效的问题,HikariCP 强制开启借出测试和空闲测试,不开启回收测试,可选的只有泄露测试。数组


#-------------链接检测状况--------------------------------
#用来检测链接是否有效的sql,要求是一个查询语句,经常使用select 'x'
#若是驱动支持JDBC4,建议不设置,由于这时默认会调用Connection.isValid()方法来检测,该方式效率会更高
#默认为空
connectionTestQuery=select 1 from dual

#检测链接是否有效的超时时间,单位毫秒
#最小容许值250 ms
#默认5000 ms。可经过JMX动态修改
validationTimeout=5000

#链接保持空闲而不被驱逐的最小时间。单位毫秒。
#该配置只有再minimumIdle < maximumPoolSize才会生效,最小容许值为10000 ms。
#默认值10000*60 = 10分钟。可经过JMX动态修改
idleTimeout=600000

#链接对象容许“泄露”的最大时间。单位毫秒
#最小容许值为2000 ms。
#默认0,表示不开启泄露检测。可经过JMX动态修改
leakDetectionThreshold=0

#链接最大存活时间。单位毫秒
#最小容许值30000 ms
#默认30分钟。可经过JMX动态修改
maxLifetime=1800000

#获取链接时最大等待时间,单位毫秒
#获取时间超过该配置,将抛出异常。最小容许值250 ms
#默认30000 ms。可经过JMX动态修改
connectionTimeout=300000

#在启动链接池前获取链接的超时时间,单位毫秒
#>0时,会尝试获取链接。若是获取时间超过指定时长,不会开启链接池,并抛出异常
#=0时,会尝试获取并验证链接。若是获取成功但验证失败则不开启池,可是若是获取失败仍是会开启池
#<0时,不论是否获取或校验成功都会开启池。
#默认为1
initializationFailTimeout=1

事务相关参数

建议保留默认就行。

#-------------事务相关的属性--------------------------------
#当链接返回池中时是否设置自动提交
#默认为true
autoCommit=true

#当链接从池中取出时是否设置为只读
#默认值false
readOnly=false

#链接池建立的链接的默认的TransactionIsolation状态
#可用值为下列之一:NONE,TRANSACTION_READ_UNCOMMITTED, TRANSACTION_READ_COMMITTED, TRANSACTION_REPEATABLE_READ, TRANSACTION_SERIALIZABLE
#默认值为空,由驱动决定
transactionIsolation=TRANSACTION_REPEATABLE_READ

#是否在事务中隔离内部查询。
#autoCommit为false时才生效
#默认false
isolateInternalQueries=false

JMX参数

建议不开启 allowPoolSuspension,对性能影响较大,后面源码分析会解释缘由。

#-------------JMX--------------------------------

#是否容许经过JMX挂起和恢复链接池
#默认为false
allowPoolSuspension=false

#是否开启JMX
#默认false
registerMbeans=true

#数据源名,通常用于JMX。
#默认自动生成
poolName=zzs001

其余

注意,这里的 dataSourceJndiName 不是前面例子中的 jdbc/hikariCP-test,这个数据源是用来建立原生链接对象的,通常用不到。

#-------------其余--------------------------------
#数据库目录
#默认由驱动决定
catalog=github_demo

#由JDBC驱动提供的数据源类名
#不支持XA数据源。若是不设置,默认会采用DriverManager来获取链接对象
#注意,若是设置了driverClassName,则不容许再设置dataSourceClassName,不然会报错
#默认为空
#dataSourceClassName=

#JNDI配置的数据源名
#默认为空
#dataSourceJndiName=

#在每一个链接获取后、放入池前,须要执行的初始化语句
#若是执行失败,该链接会被丢弃
#默认为空
#connectionInitSql=

#-------------如下参数仅支持经过IOC容器或代码配置的方式--------------------------------

#TODO
#默认为空
#metricRegistry

#TODO
#默认为空
#healthCheckRegistry

#用于Hikari包装的数据源实例
#默认为空
#dataSource

#用于建立线程的工厂
#默认为空
#threadFactory=

#用于执行定时任务的线程池
#默认为空
#scheduledExecutor=

源码分析

HikariCP 的源码轻巧且简单,读起来不会太吃力,因此,此次不会从头至尾地分析代码逻辑,更多地会分析一些设计巧妙的地方。在阅读 HiakriCP 源码以前,须要掌握:CopyOnWriteArrayList、AtomicInteger、SynchronousQueue、Semaphore、AtomicIntegerFieldUpdater等工具。注意:考虑篇幅和可读性,如下代码通过删减,仅保留所需部分 。

HikariCP为何快?

结合源码分析以及参考资料,相比 DBCP 和 C3P0 等链接池,HikariCP 快主要有如下几个缘由:

  1. 经过代码设计和优化大幅减小线程间的锁竞争。这一点主要经过 ConcurrentBag 来实现,下文会展开。

  2. 引入了更多 JDK 的特性,尤为是 concurrent 包的工具。DBCP 和 C3P0 出现时间较早,基于早期的 JDK 进行开发,也就很难享受到后面更新带来的福利;

  3. 使用 javassist 直接修改 class 文件生成动态代理,精简了不少没必要要的字节码,提升代理方法运行速度。相比 JDK 和 cglib 的动态代理,经过 javassist 直接修改 class 文件生成的代理类在运行上会更快一些(这是网上找到的说法,可是目前 JDK 和 cglib 已经通过了屡次优化,在代理类的运行速度上应该不会差一个数量级,我抽空再测试下吧)。HikariCP 涉及 javassist 的代码在 JavassistProxyFactory 类中,相关内容请自行查阅;

  4. 重视代码细节对性能的影响。下文到的 fastPathPool 就是一个例子,仔细琢磨 HikariCP 的代码就会发现许多相似的细节优化,除此以外还有 FastList 等自定义集合类;

接下来,本文将在分析源码的过程当中对以上几点展开讨论。

HikariCP的架构

在分析具体代码以前,这里先介绍下 HikariCP 的总体架构,和 DBCP2 的有点相似(可见 HikariCP 与 DBCP2 性能差别并非因为架构设计)。

HikariCP是什么,springmvc实战百度云

咱们和 HikariCP 打交道,通常经过如下几个入口:

  1. 经过 JMX 调用HikariConfigMXBean来动态修改配置(只有部分参数容许修改,在配置详解里有注明);

  2. 经过 JMX 调用HikariPoolMXBean来获取链接池的链接数(活跃、空闲和全部)、获取等待链接的线程数、挂起和恢复链接池、丢弃未使用链接等;

  3. 使用HikariConfig加载配置文件,或手动配置HikariConfig的参数,通常它会做为入参来构造HikariDataSource对象;

  4. 使用HikariDataSource获取和丢弃链接对象,另外,由于继承了HikariConfig,咱们也能够经过HikariDataSource来配置参数,但这种方式不支持配置文件。

为何HikariDataSource持有HikariPool的两个引用

在图中能够看到,HikariDataSource持有了HikariPool的引用,看过源码的同窗可能会问,为何属性里会有两个HikariPool,以下:

public class HikariDataSource extends HikariConfig implements DataSource, Closeable{
   private final HikariPool fastPathPool;
   private volatile HikariPool pool;
}

这里补充说明下,其实这里的两个HikariPool的不一样取值表明了不一样的配置方式:配置方式一:当经过有参构造new HikariDataSource(HikariConfig configuration)来建立HikariDataSource时,fastPathPool 和 pool 是非空且相同的;配置方式二:当经过无参构造new HikariDataSource()来建立HikariDataSource并手动配置时,fastPathPool 为空,pool 不为空(在第一次 getConnectionI() 时初始化),以下;


   public Connection getConnection() throws SQLException
{
      if (isClosed()) {
         throw new SQLException("HikariDataSource " + this + " has been closed.");
      }

      if (fastPathPool != null) {
         return fastPathPool.getConnection();
      }

      // 第二种配置方式会在第一次 getConnectionI() 时初始化pool
      HikariPool result = pool;
      if (result == null) {
         synchronized (this) {
            result = pool;
            if (result == null) {
               validate();
               LOGGER.info("{} - Starting...", getPoolName());
               try {
                  pool = result = new HikariPool(this);
               }
               catch (PoolInitializationException pie) {
                  if (pie.getCause() instanceof SQLException) {
                     throw (SQLException) pie.getCause();
                  }
                  else {
                     throw pie;
                  }
               }
               LOGGER.info("{} - Start completed.", getPoolName());
            }
         }
      }

      return result.getConnection();
   }

针对以上两种配置方式,其实使用一个 pool 就能够完成,那为何会有两个?咱们比较下这两种方式的区别:

   private final T t1;
   private volatile T t2;
   public void method01(){
      if (t1 != null) {
         // do something
      }
   }
   public void method02(){
      T result = t2;
      if (result != null) {
         // do something
      }
   }

上面的两个方法中,执行的代码几乎同样,可是 method02 在性能上会比 method01 稍差。固然,主要问题不是出在 method02 多定义了一个变量,而在于 t2 的 volatile 性质,正由于 t2 被 volatile 修饰,为了实现数据一致性会出现没必要要的开销,因此 method02 在性能上会比 method01 稍差。pool 和 fastPathPool 的问题也是同理,因此,第二种配置方式不建议使用。经过上面的问题就会发现,HiakriCP 在追求性能方面很是重视细节,怪不得可以成为最快的链接池!

HikariPool--管理链接的池塘

HikariPool 是一个很是重要的类,它负责管理链接,涉及到比较多的代码逻辑。这里先简单介绍下这个类,对下文代码的具体分析会有所帮助。

HikariCP是什么,springmvc实战百度云

HikariPool 的几个属性说明以下:属性类型和属性名说明

HikariCP是什么,springmvc实战百度云

为了更清晰地理解上面几个字段的含义,我简单画了个图,不是很严谨,将就看下吧。在这个图中,PoolEntry 封装了 Connection 对象,在图中把它当作是链接对象会更好理解一些。咱们能够看到ConcurrentBag 是整个 HikariPool 的核心,其余对象都围绕着它进行操做,后面会单独讲解这个类。客户端线程能够调用它的 borrow、requite 和 remove 方法,houseKeepingExecutorService 线程能够调用它的 remove 方法,只有 addConnectionExecutor 能够进行 add 操做。

HikariCP是什么,springmvc实战百度云

borrow 和 requite 对于 ConcurrentBag 而言是只读的操做,addConnectionExecutor 只开启一个线程执行任务,因此 add 操做是单线程的,惟一存在锁竞争的就是 remove 方法。接下来会具体讲解 ConcurrentBag。

ConcurrentBag--更少的锁冲突

在 HikariCP 中ConcurrentBag用于存放PoolEntry对象(封装了Connection对象,IConcurrentBagEntry实现类),本质上能够将它就是一个资源池。

HikariCP是什么,springmvc实战百度云

下面简单介绍下几个字段的做用:属性描述

HikariCP是什么,springmvc实战百度云

描述

HikariCP是什么,springmvc实战百度云

这几个字段在ConcurrentBag中如何使用呢,咱们来看看borrow的方法:

   public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
   {
      // 1. 首先从threadList获取对象

      // 获取绑定在当前线程的List<Object>对象,注意这个集合的实现通常为FastList,这是HikariCP本身实现的,后面会讲到
      final List<Object> list = threadList.get();
       // 遍历结合
      for (int i = list.size() - 1; i >= 0; i--) {
         // 获取当前元素,并将它从集合中删除
         final Object entry = list.remove(i);
         // 若是设置了weakThreadLocals,则存放的是WeakReference对象,不然为咱们一开始设置的PoolEntry对象
         @SuppressWarnings("unchecked")
         final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
          // 采用CAS方式将获取的对象状态由未使用改成使用中,若是失败说明其余线程正在使用它,这里可知,threadList上的元素能够被其余线程“偷走”。
         if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
         }
      }

      // 2.若是还没获取到,会从sharedList中获取对象

      // 等待获取链接的线程数+1
      final int waiting = waiters.incrementAndGet();
      try {
         // 遍历sharedList
         for (T bagEntry : sharedList) {
            // 采用CAS方式将获取的对象状态由未使用改成使用中,若是当前元素正在使用,则没法修改为功,进入下一循环
            if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
               // 通知监听器添加包元素。若是waiting - addConnectionQueue.size() >= 0,则会让addConnectionExecutor执行PoolEntryCreator任务
               if (waiting > 1) {
                  listener.addBagItem(waiting - 1);
               }
               return bagEntry;
            }
         }
         // 通知监听器添加包元素。
         listener.addBagItem(waiting);

         // 3.若是还没获取到,会从轮训进入handoffQueue队列获取链接对象

         timeout = timeUnit.toNanos(timeout);
         do {
            final long start = currentTime();
               // 从handoffQueue队列中获取并删除元素。这是一个无容量的阻塞队列,插入操做须要阻塞等待删除操做,而删除操做不须要等待,若是没有元素插入,会返回null,若是设置了超时时间则须要等待
            final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
            // 这里会出现三种状况,
            // 1.超时,返回null
            // 2.获取到元素,但状态为正在使用,继续执行
            // 3.获取到元素,元素状态未未使用,修改未使用并返回
            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 {
         // 等待获取链接的线程数-1
         waiters.decrementAndGet();
      }
   }

在以上方法中,惟一可能出现线程切换到就是handoffQueue.poll(timeout, NANOSECONDS),除此以外,咱们没有看到任何的 synchronized 和 lock。之因此能够作到这样主要因为如下几点:

1.元素状态的引入,以及使用CAS方法修改状态。在ConcurrentBag中,使用使用中、未使用、删除和保留等表示元素的状态,而不是使用不一样的集合来维护不一样状态的元素。元素状态这一律念的引入很是关键,为后面的几点提供了基础。ConcurrentBag的方法中多处调用 CAS 方法来判断和修改元素状态,这一过程不须要加锁。

2.threadList 的使用。当前线程归还的元素会被绑定到ThreadLocal,该线程再次获取元素时,在该元素未被偷走的前提下可直接获取到,不须要去 sharedList 遍历获取;

3.采用CopyOnWriteArrayList来存放元素。在CopyOnWriteArrayList中,读和写使用的是不一样的数组,避免了二者的锁竞争,至于多个线程写入,则会加 ReentrantLock 锁。

4.sharedList 的读写控制。borrow 和 requite 对 sharedList 来讲都是不加锁的,缺点就是会牺牲一致性。用户线程没法进行增长元素的操做,只有 addConnectionExecutor 能够,而 addConnectionExecutor 只会开启一个线程执行任务,因此 add 操做不会存在锁竞争。至于 remove 是惟一会形成锁竞争的方法,这一点我认为也能够参照 addConnectionExecutor 来处理,在加入任务队列前把 PoolEntry 的状态标记为删除中。

其实,咱们会发现,ConcurrentBag在减小锁冲突的问题上,除了设计改进,还使用了比较多的 JDK 特性。

如何加载配置

在HikariCP 中,HikariConfig用于加载配置,具体的代码并不复杂,但相比其余项目,它的加载要更加简洁一些。咱们直接从PropertyElf.setTargetFromProperties(Object, Properties)方法开始看,以下:


   // 这个方法就是将properties的参数设置到HikariConfig中
   public static void setTargetFromProperties(final Object target, final Properties properties)
   {
      if (target == null || properties == null) {
         return;
      }

      // 在这里会利用反射获取
      List<Method> methods = Arrays.asList(target.getClass().getMethods());
      // 遍历
      properties.forEach((key, value) -> {
         // 若是是dataSource.*的参数,直接加入到dataSourceProperties属性
         if (target instanceof HikariConfig && key.toString().startsWith("dataSource.")) {
            ((HikariConfig) target).addDataSourceProperty(key.toString().substring("dataSource.".length()), value);
         }
         else {
            // 若是不是,则经过set方法设置
            setProperty(target, key.toString(), value, methods);
         }
      });

## 最后

须要的朋友能够点击:[**戳这里免费领取**](https://gitee.com/vip204888/java-p7)。

还有Java核心知识点+全套架构师学习资料和视频+一线大厂面试宝典+面试简历模板能够领取+阿里美团网易腾讯小米爱奇艺快手哔哩哔哩面试题+Spring源码合集+Java架构实战电子书+2021年最新大厂面试题。
![在这里插入图片描述](https://s2.51cto.com/images/20210906/1630894316531991.jpg)