美文网首页HiKariCP
hikari数据库连接池源码分析以及相关概念

hikari数据库连接池源码分析以及相关概念

作者: JackpotDC | 来源:发表于2021-01-06 14:44 被阅读0次

    hikari简介以及相关的概念

    "Simplicity is prerequisite for reliability."
    - Edsger Dijkstra

    hikari

    hikari,日语中“光”的意思,作者为这个数据库连接池命名为光,寓意是像光一样快。在分析hikariCP之前简单介绍下JDBC和数据库连接池。

    JDBC

    全称Java Database Connectivity,java入门课本中基本都会介绍到的部分。

    以常见的MySQL数据库为例,JDBC可以简单概括为就是一个jar包,这个jar包中提供了相应的interface与class,封装好了与MySQL服务端连接的协议,通过Java代码就可以实现mysql client上的select、update、delete等功能。

    JDBC

    原始的JDBC使用姿势是这样的(爷青回):

    
     try {
        // 初始化驱动类com.mysql.jdbc.Driver
        Class.forName("com.mysql.jdbc.Driver");
        // 获取connection连接
        conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8","root", "admin");
    
        // 查询
        sql = "select * from test"
        pstmt = (PreparedStatement) conn.prepareStatement(sql);
        rs = (ResultSet) pstmt.executeQuery();
        // 后续将ResultSet转化为对象返回...
    } catch (ClassNotFoundException e) {                
        e.printStackTrace();
    }catch (SQLException e) {                           
        e.printStackTrace();
    }
    
    

    数据库连接池(Connection Pool)

    以上DriverManager.getConnection建立jdbc连接的过程是很昂贵的,需要经历 TCP握手+MySQL认证,如果服务在不断并发处理多个请求的时候每次都重新建立JDBC连接并且在请求结束后关闭JDBC连接,那么将会导致:

    • 网络IO多,因为不断的TCP握手和TCP关闭,大量新建后立刻关闭的TIME_WAIT状态的TCP连接占用系统资源
    • 数据库负载高,数据库也要重新为连接建立各种数据结构,并且在短暂的查询结束后又要销毁
    • 查询耗时长,多了TCP握手和MySQL认证的耗时。同时,不断产生又回收掉JDBC连接资源会频繁触发GC

    为了能够 复用已经辛辛苦苦建立好的JDBC连接,就有了数据库连接池的概念,同理线程池、redis连接池、HBase连接池等等都同理。

    数据库连接池应该具有的功能

    • 在连接应用启动的时候建立连接,并且保存到Java的容器或自定义的容器中维护
    • 在应用运行的过程中,间歇性测试连接的可用性(select 1),踢出不可用的连接,加入新的连接
    • 在应用结束后关闭JDBC连接

    Spring-JDBC

    基于以上原JDBC的使用方式可以看出,JDBC在使用上存在大量可以封装起来的功能(connection的获取、构造PreparedStatement、connection的回收等)。总之,Spring-JDBC通过对JDBC的封装,简化了SQL的执行步骤,我们只需要输入sql语句,框架会输出sql的执行结果(类似于mysql-client控制台的效果)。

    Spring-JDBC

    如上图,有了Spring-JDBC等框架封装JDBC连接获取,prepareStatement赋值,JDBC连接的释放,使用者就只需要写好sql,调用Spring-JDBC提供的JdbcTemplate类封装的方法,等待获取结果即可。

    JdbcTemplate类会通过Spring依赖注入等方式设置好它的dataSource属性,而这个dataSource就是一个数据连接池(Connection Pool,CP),CP通过选择合适的数据结构来动态维护着与MySQL的Jdbc连接。整个封装起来对使用者来说无需感知,只需要知道输入SQL语句和输出结果就好。

    hikari核心功能

    HikariDataSource

    HikariDataSource的继承结构

    正如上图所示,dataSource代表着一个数据库连接池,而hikari的数据库连接池就是HikariDataSource,这个类是hikari的核心类。

    HikariDataSource

    HikariDataSource的继承关系如上图,重点关注HikariConfigDataSource

    • DataSource,是一个接口,就是数据库连接池在Java类中的体现,对JDBC框架来说不需要关注Connection怎么来的,只需要调这个接口的方法获取即可


      DataSource
    • HikariConfig,hikari配置类,有着jdbcUrl、用户名密码、超时时间、数据库连接池连接数等成员变量属性

    HikariDataSource的成员结构

    HikariDataSource的主要成员结构如下图:

    HikariDataSource的结构
    • isShutdown,一个AtomicBoolean(cas乐观锁)用来标识该dataSource是否已经关闭
    • fastPathPool 和 pool,都是指向HikariPool结构的引用(Hikari优化用来提升性能的手段)
    • 若干继承自HikariConfig的数据库连接池配置属性

    HikariPool

    HouseKeeper管家

    HikariPool的主要属性如上图所示,主要通过30s执行一次的定时任务HouseKeeper来维护管理连接池中的连接。
    HouseKeeper任务执行流程的伪代码如下:

    def run():
        # hikari的连接池维护机制大量依赖时间戳,如果发现系统时间倒退了,则会将连接池清空重来
        if detect_retrograde_time():
            softEvictConnections()
            return
        if detect_forward_time(): # 如果发现时间推前了,打日志,警告维持的连接会提前退休
            log()
        if idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize():
            # 清理过期连接
            for connection in connectionBag_NotInUse:
                if is_timeout(connection):
                    closeConnection(connection)
        # 初始化或清理过后
        fillPool() # 填充连接池,保证最小的连接数量(idleConnections),同时保证不超过最大连接数量(maximumPoolSize)        
    

    从伪代码中可以看出,HouseKeeper主要的工作分为:

    1. 检测系统时间的准确性,系统时间被用户篡改过,则处理清空连接池
    2. 对空闲的连接,根据idleTimeout属性清理过期连接
    3. 在初始化或清理过后,填充新的连接到连接池中

    关闭连接的过程(closeConnection)和填充的过程(fillPool)则分别是通过向上图中的closeConnectionExecutoraddConnectionExecutor连接池提交异步任务的方式,分别执行JDBC连接关闭和异步的建立JDBC连接的。

    hikari优化的连接池存储容器 - ConcurrentBag

    通过上面的分析,我们知道了hikari是通过在HikariPool这样的数据结构中执行30s一次的定时任务动态关闭/新增连接到连接池的。维护连接池中的连接就需要用到一种 容器,由于新增和关闭连接都是通过线程池异步执行的,而且getConnection()的操作大多数情况下是并发的,必然涉及到支持 并发。在Hikari中是通过自定义的容器类ConcurrentBag来维护JDBC连接的。

    ConcurrentBag的结构

    ConcurrentBag的结构如下图,重点关注

    • sharedList和threadList,是两个关于PoolEntry(JdbcConnection的封装)的列表,用来实际存储连接池中维护的Jdbc连接
    • listener,是指向HikariPool的引用,当ConcurrentBag获取连接时发现用完了,则通过listener的回调接口请求HikariPool多扩充点Jdbc连接入池
    • handoffQueue,和listener相关,HikariPool扩充连接池后将新的连接通过该队列提供给正在等待的线程,正在getConnection的线程会在queue的另一端阻塞住,等待扩充连接后喂给它们
    ConcurrentBag的结构

    PoolEntry,可以简单理解为Hikari对JdbcConnection的一层封装,有四种状态

    • int STATE_NOT_IN_USE = 0; 没在用
    • int STATE_IN_USE = 1; 使用中
    • int STATE_REMOVED = -1; 已移除
    • int STATE_RESERVED = -2; 预定

    ConcurrentBag#add

    新增的接口很简单,这里直接贴出源码。

    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.add(bagEntry);
    
          // spin until a thread takes it or none are waiting
          while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) {
             yield();
          }
       }
    

    从代码中可以看出主要的工作分为两步:

    1. sharedList.add()
    2. 如果有等待中的线程(正在getConnection),则尝试将新加入的JDBC连接通过队列handoffQueue喂给它

    ConcurrentBag#remove

    1. 通过cas确保当前这个connection在使用中或被预定,使用中是在getConnection的时候通过探活(select 1)发现连接已经已经失效时,将其关闭;被预定是在HouseKeeper管家第2步对空闲的连接清理的时候,提前将要清理的连接锁住,并且执行close操作时。
    2. sharedList.remove(bagEntry); -- 从list中移除

    ConcurrentBag#borrow

    borrow方法的伪代码如下。

    def borrow():
       if (bagEntry = find_from_threadList()) != null: # 1.优先从threadList中找
           return bagEntry
       waiters++
       try:
           for bagEntry in sharedList: # 2. 从sharedList中找
               if not_in_use(bagEntry):
                   if waiters > 1: 
                       # 2.1 如果从sharedList中找到了,但是有waiting的,大概率是在并发情况下抢到了别人的,那就帮他再申请一个JDBC连接
                       listener.addBagItem(waiters - 1);
                   return bagEntry
           # 3. sharedList中没找到,则申请新的JDBC连接
           listener.addBagItem(waiting);
           # 4. 申请了新的JDBC连接后,站在handoffQueue的一边,等待JDBC连接创建好的结果
           while(not_timeout()):
               bagEntry = handoffQueue.poll()
               if not_in_use(bagEntry):
                   return bagEntry
        finally:
            waiters--
    

    ConcurrentBag、hikari与Spring-JDBC

    上面的解释可能太过底层和抽象,基于上面对ConcurrentBag主要方法的源码分析,我们这里可以将hikari的ConcurrentBag与Spring-JDBC整个串起来,分析一条sql语句执行的整个过程。

    ConcurrentBag、hikari与Spring-JDBC

    以语句select * from xxx为例,

    1. 在业务代码层面通过调用Spring-JDBC提供的JdbcTemplate的query方法,获取查询结果
    2. JdbcTemplate调用内部的execute方法时,调用了HikariDataSource#getConnection方法(dataSource可以通过手动注入,或SpringIOC注入)
    3. HikariDataSource通过向其内部维护的连接池对象HikariPool请求getConnection获取连接(注意,这里有两个HikariPool,是hikari的一种优化手段,下文中会详细分析)
    4. HikariPool通过向其内部维护的connectionBag(即ConcurrentBag对象)借用(borrow)来获取连接,借用的过程就是从写时拷贝list sharedListthreadLocal threadList获取连接,最终再一层层返回给JdbcTemplate,由JdbcTemplate执行sql语句,并将最终结果返回给业务代码
    5. 在第4步中,可能存在ConcurrentBag中维护的连接不够用的情况,这时候会通过listener指向的HikariPool请求扩充连接池中的连接

    综上,我们可以看出,最终一条sql是通过这样层层抽象封装来实现的,

    • HikariDataSource只是为了对Spring-JDBC的DataSource接口做适配而产生的
    • HikariPool才是Hikari的核心功能,它像是一个专门负责管理DB连接的管家,背着一个名叫ConcurrentBag的重重的背包,在你(业务侧)需要JDBC连接的时候提供给你,同时管家自己会是不是检查和扩充背包里的连接(就像跟女神和高富帅一起爬山时跟在最后背包的工具人)泪目( Ĭ ^ Ĭ )
    工具人

    Hikari的优化点

    通过上面的分析,读者应该已经清楚了Hikari的大致原理,有了大致的了解之后,Hikari的代码实现的很多细节通过阅读源码也可以细化了解。我们这里回到最关键的问题,Hikari为什么这么快,为什么敢用 来命名自己。

    字节码精简

    字节码精简:优化代码,直到编译后的字节码最少,这样,CPU缓存可以加载更多的程序代码;
    优化代理和拦截器:减少代码,例如HikariCP的Statement proxy只有100行代码,只有BoneCP的十分之一

    以上是从网上搜到的,确实可以体现出Hikari极致优化的特性,但是对性能影响可能微乎其微。

    fastPathPool 和 pool

    如上面关于HikariDataSource与HikariPool的关系时,我们可以看到HikariDataSource内部有两个指向HikariPool的引用。这里也是Hikari优化性能的一种手段。

    fastPathPool 和 pool

    HikariDataSource有两种初始化的方式:

    • 一种是无参数的初始化方式,这种时候当执行到getConnection的时候,会通过 懒汉单例模式 懒加载初始化HikariPool,赋值给pool引用,懒汉模式涉及到并发情况,所以一般会考虑采用 Double-Check-Lock(DCL) 方式加锁,DCL方式中涉及到JVM的字节码指令重排优化的问题,所以需要将pool引用设置为volatile,废弃掉JVM的字节码指令优化。(【5】Java中DCL(Double-Check-Lock)对volatile必要性的疑惑)但是同时因为volatile关键字,导致pool相关的加载都会有性能问题。所以获取HikariPool时优先通过fastPathPool引用。
    • 另一种是带参数的初始化,在构造函数中同时将初始化完的HikariPool赋值给pool和fastPathPool引用,在获取HikariPool引用的时候总是优先获取fastPathPool,以防止volatile关键字对性能的影响


      构造函数

    自定义的容器类型

    1、 定义数组类型(FastStatementList)代替ArrayList:避免每次get()调用都要进行range check,避免调用remove()时的从头到尾的扫描;【6】【追光者系列】HikariCP源码分析之FastList

    2、 自定义集合类型(ConcurrentBag):提高并发读写的效率;
    通过之前对ConcurrentBag的borrow方法的了解,我们知道,hikariPool是通过ConcurrentBag#borrow方法来获取连接的,而ConcurrentBag中的逻辑则是:优先从threadList(ThreadLocal)获取;获取不到时再从sharedList(CopyOnWriteArrayList)获取

    ThreadLocal:例如业务代码执行了两次SQL,获取了两次JDBC连接,在第一次执行完毕之后ConcurrentBag会回收该连接,但是会回收到ThreadLocal中。当业务代码第二次执行SQl需要获取JDBC连接时,只要是同一个线程,则会从ThreadLocal中获取到连接。(关于ThreadLocal的原理,可以参考笔者的往期文章 【7】图解分析ThreadLocal的原理与应用场景

    threadList

    CopyOnWriteArrayList:写时拷贝技术,并发情况下线程安全版本的ArrayList,写时拷贝技术源自于unix系统的fork系统调用,指的是读操作时不加锁,只有当写操作执行的时候锁住整个list,然后执行替换(狸猫换太子)

    CopyOnWriteArrayList CopyOnWriteArrayList

    扩展知识

    事务管理器transactionManager

    在需要支持事务操作的方法上,我们通常会加上这样的注解, @Transactional(rollbackFor = Exception.class, value = "txManagerXXX"), 用来表示注解包裹住的方法内部是一个事务,需要符合事务的特性(如原子性)
    在Spring中的实现是通过AOP切面技术生成了动态代理,封装了@Transactional注解的方法

    transactionManager

    JNDI

    JNDI,全称是 Java Naming and Directory Interface,Java 命名与目录接口,也是跟原始的JDBC用法息息相关的概念。数据库的实现有很多种,Java服务是通过JDBC驱动类来获取JDBC连接的,那么原始的JDBC用法就会有几种问题:

    1. 数据库的地址、账户名、密码、连接池的参数发生改变,要修改代码
    2. 数据库可能从MySQL改为Oracle或者其它DB,需要修改底层代码,换JDBC驱动jar包,由此会引发一系列的相关功能的回归

    基于以上问题,J2EE规范提出了JNDI的规范,对于业务代码来说只需要关心获取的DataSource是哪个(通过naming-名字),不需要知道这个DataSource背后的细节(url地址、账户名密码等),即findResouceByName,而resources的相关参数是配置在xml文件里的。
    经常用Spring的读者可以看出,这就是SpringIOC容器提供的getBean(String beanName)方法,即Spring的xml文件,所以SpringIOC就是在J2EE的规范上实现的。(【8】Spring IOC 前世今生之 JDNI - binarylei - 博客园

    references

    1. ^ Introduction to HikariCP | Baeldung
    2. ^ GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last.
    3. ^ HikariCP连接池 - 简书
    4. ^ 源码详解系列(八) ------ 全面讲解HikariCP的使用和源码 - 子月生 - 博客园
    5. ^ Java中DCL(Double-Check-Lock)对volatile必要性的疑惑_u010131029的博客-CSDN博客
    6. ^ 【追光者系列】HikariCP源码分析之FastList_weixin_34304013的博客-CSDN博客
    7. ^ 图解分析ThreadLocal的原理与应用场景
    8. ^ Spring IOC 前世今生之 JDNI - binarylei - 博客园

    相关文章

      网友评论

        本文标题:hikari数据库连接池源码分析以及相关概念

        本文链接:https://www.haomeiwen.com/subject/vainoktx.html