阿⾥的Druid来告诉你池化技术有多⽜
零、类图&流程预览
本⽂会通过getConnection作为⼊⼝,探索在druid⾥,⼀个连接的⽣命周期。⼤体流程被划分成了以下⼏个主流程:
主流程1:获取连接流程
⾸先会调⽤init进⾏连接池的初始化,然后运⾏责任链上的每⼀个filter,最终执⾏getConnectionDirect获取真正的连接对象,如果开启
了testOnBorrow,则每次都会去测试连接是否可⽤(这也是官⽅不建议设置testOnBorrow为true的原因,影响性能,这⾥的测试是指测试mysql服务端的长连接是否断开,⼀般mysql服务端长连保活时间是8h,被使⽤⼀次则刷新⼀次使⽤时间,若⼀个连接距离上次被使⽤超过了保活时间,那么再次使⽤时将⽆法与mysql服务端通信)。
如果testOnBorrow没有被置为true,则会进⾏testWhileIdle的检查(这⼀项官⽅建议设置为true,缺省值也是true),检查时会判断当前连接对象距离上次被使⽤的时间是否超过规定检查的时间,若超过,则进⾏检查⼀次,这个检查时间通
thread技术
过timeBetweenEvictionRunsMillis来控制,默认60s。
每个连接对象会记录下上次被使⽤的时间,⽤当前时间减去上⼀次的使⽤时间得出闲置时间,闲置时间再跟timeBetweenEvictionRunsMillis⽐较,超过这个时间就做⼀次连接可⽤性检查,这个相⽐testOnBorrow每次都检查来说,性能会提升很多,⽤的时候⽆需关注该值,因为缺省值是true,经测试如果将该值设置为false,testOnBorro w也设置为false,数据库服务端长连保活时间改为60s,60s内不使⽤连接,超过60s后使⽤将会报连接错误。
若使⽤testConnectionInternal⽅法测试长连接结果为false,则证明该连接已被服务端断开或者有其他的⽹络原因导致该连接不可⽤,则会触
发discardConnection进⾏连接回收(对应流程1.4,因为丢弃了⼀个连接,因此该⽅法会唤醒主流程3进⾏检查是否需要新建连接)。整个流程运⾏在⼀个死循环内,直到取到可⽤连接或者超过重试上限报错退出(在连接没有超过连接池上限的话,最多重试⼀次(重试次数默认重试1次,可以通过notFullTimeoutRetryCount属性来控制),所以取连接这⾥⼀旦发⽣等待,在连接池没有满的情况下,最⼤等待2 × maxWait 的时间 ←这个有待验证)。
特别说明①
为了保证性能,不建议将testOnBorrow设置为true,或者说牵扯到长连接可⽤检测的那⼏项配置使⽤druid默认的配置就可以保证性能是最好的,如上所说,默认长连接检查是60s⼀次,所以不启⽤testOnBorrow的情况下要想保证万⽆⼀失,⾃⼰要确认下所连的那个mysql服务端的长连接保活时间(虽然默认是8h,但是dba可能给测试环境设置的时间远⼩于这个时间,所以如果这个时间⼩于60s,就需要⼿动设置timeBetweenEvictionRunsMillis了,如果mysql服务端长连接时间是8h或者更长,则⽤默认值即可。
特别说明②
为了防⽌不必要的扩容,在mysql服务端长连接够⽤的情况下,对于⼀些qps较⾼的服务、⽹关业务,建议把池⼦的最⼩闲置连接数minIdle和最⼤连接数maxActive设置成⼀样的,且按照需要调⼤,且开启keepAlive进⾏连接活性检查(参考流程4.1),这样就不会后期发⽣动态新建连接的情况(建连还是个⽐较重的操作,所以不如⼀开始就申请好所有需要的连接,个⼈意见,仅供参考),但是像管理后台这种,长期qps⾮常低,但是有的时候需要⽤管理后台做⼀些巨⼤的操作(⽐如导数据什么的)导致需要的连接暴增,且管理后台不会特别要求性能,就适合将minIdle的值设置的⽐maxActive⼩,这样不会造成不必要的连接浪费,也不会在需要暴增连接的时候⽆法动态扩增连接。
主流程2:初始化连接池
通过上⾯的流程图可以看到,在获取⼀个连接的时候⾸先会检查连接池是否已经初始化完毕(通过inited来控制,bool类型,未初始化为flase,初始化完毕为true,这个判断过程在init⽅法内完成),若没有初始化,则调⽤init进⾏初始化(图主流程1中的紫⾊部分),下⾯来看看init⽅法
⾥⼜做了哪些操作:
可以看到,实例化的时候会初始化全局的重⼊锁lock,在初始化过程中包括后续的连接池操作都会利⽤该锁保证线程安全,初始化连接池的时候⾸先会进⾏双重检查是否已经初始化过,若没有,则进⾏连接池的初始化,这时候还会通过SPI 机制额外加载责任链上的filter。
但是这类filter需要在类上加上@AutoLoad注解。然后初始化了三个数组,容积都为maxActive,⾸先connections就是⽤来存放池⼦⾥连接对象的,evictConnections⽤来存放每次检查需要抛弃的连接(结合流程4.1理解),keepAliveConnections⽤于存放需要连接检查的存活连接(同样结合流程4.1理解),然后⽣成初始化数(initialSize)个连接,放进connections,然后⽣成两个必须的守护线程,⽤来添加连接进池以及从池⼦⾥摘除不需要的连接,这俩过程较复杂,因此拆出来单说(主流程3和主流程4)。
特别说明①
从流程上看如果⼀开始实例化的时候不对连接池进⾏初始化(这个初始化是指对池⼦本⾝的初始化,并⾮单纯的指druid对象属性的初始化),那么在第⼀次调⽤getConnection时就会⾛上图那么多逻辑,尤其是耗时较久的建⽴连接操作,被重复执⾏了很多次,导致第⼀次getConnection 时耗时过久,如果你的程序并发量很⼤,那么第⼀次获取连接时就会因为初始化流程⽽发⽣排队,所以建议在实例化连接池后对其进⾏预热,通过调⽤init⽅法或者getConnection⽅法都可以。
特别说明②
在构建全局重⼊锁的时候,利⽤lock对象⽣成了俩Condition,对这俩Condition解释如下:
当连接池连接够⽤时,利⽤empty阻塞添加连接的守护线程(主流程3),当连接池连接不够⽤时,获取连接的那个线程(这⾥记为业务线程A)就会阻塞在notEmpty上,且唤起阻塞在empty上的添加连接的守护线程,⾛完添加连接的流程,⾛完后会重新唤起阻塞在notEmpty上的业务线程A,业务线程A就会继续尝试获取连接。
三、流程1.1:责任链
WARN:这块东西结合源码看更容易理解
这⾥对应流程1⾥获取连接时需要执⾏的责任链,每个DruidAbstractDataSource⾥都包含filters属性,filt
ers是对Druid⾥Filters接⼝的实现,⾥⾯有很多对应着连接池⾥的映射⽅法,⽐如例⼦中dataSource的getConnection⽅法在触发的时候就会利⽤FilterChain把每个filter⾥的dataSource_getConnection给执⾏⼀遍,这⾥也要说明下FilterChain,通过流程1.1可以看出来,datasource是利⽤FilterChain来触发各个filter的执⾏的,FilterChain⾥也有⼀堆datasource⾥的映射⽅法,⽐如上图⾥的dataSource_connect,这个⽅法会把datasource⾥的filters 全部执⾏⼀遍直到nextFilter取不到值,才会触发ConnectionDirect,这个结合代码会⽐较容易理解。
四、流程1.2:从池中获取连接的流程
通过getConnectionInternal⽅法从池⼦⾥获取真正的连接对象,druid⽀持两种⽅式新增连接,⼀种是通过开启不同的守护线程通过await、signal通信实现(本⽂启⽤的⽅式,也是默认的⽅式),另⼀种是直接通过线程池异步新增,这个⽅式通过在初始化druid时传⼊
asyncInit=true,再把⼀个线程池对象赋值给createScheduler,就成功启⽤了这种模式,没仔细研究这种⽅式,所以本⽂的流程图和代码块都会规避这个模式。
上⾯的流程很简单,连接⾜够时就直接poolingCount-1,数组取值,返回,activeCount+1,整体复杂度为O(1),关键还是看取不到连接时的做法,取不到连接时,druid会先唤起新增连接的守护线程新增连接,然后陷⼊等待状态,然后唤醒该等待的点有两处,⼀个是⽤完了连接recycle(主流程5)进池⼦后
触发,另外⼀个就是新增连接的守护线程成功新增了⼀个连接后触发,await被唤起后继续加⼊锁竞争,然后往下⾛
这个⽅法会利⽤主流程2(init阶段)⾥初始化好的checker对象(流程参考init-checker)⾥的isValidConnection⽅法,如果启⽤ping,则该⽅法会利⽤invoke触发驱动程序⾥的ping⽅法,如果不启⽤ping,就采⽤SELECT 1⽅式(从init-checker⾥可以看出启不启⽤取决于加载到的驱动程序⾥是否存在相应的⽅法)。
六、流程1.4:抛弃连接
经过流程1.3返回的测试结果,如果发现连接不可⽤,则直接触发抛弃连接逻辑,这个过程⾮常简单,如上图所⽰,由流程1.2获取到该连接时累加上去的activeCount,在本流程⾥会再次减⼀,表⽰被取出来的连接不可⽤,并不能active状态。其次这⾥的close是拿着驱动那个连接对象进⾏close,正常情况下⼀个连接对象会被druid封装成DruidPooledConnection对象,内部持有的conn就是真正的驱动Connection对象,上图中的关闭连接就是获取的该对象进⾏close,如果使⽤包装类DruidPooledConnection进⾏close,则代表回收连接对象(recycle,参考主流程5)。
七、主流程3:添加连接的守护线程
在主流程2(init初始化阶段)时就开启了该流程,该流程独⽴运⾏,⼤部分时间处于等待状态,不会抢占cpu,但是当连接不够⽤时,就会被唤起追加连接,成功创建连接后将会唤醒其他正在等待获取可⽤连接的线程,⽐如:
结合流程1.2来看,当连接不够⽤时,会通过empty.signal唤醒该线程进⾏补充连接(阻塞在empty上的线程只有主流程3的单线程),然后通过notEmpty阻塞⾃⼰,当该线程补充连接成功后,⼜会对阻塞在notEmpty上的线程进⾏唤醒,让其进⼊锁竞争状态,简单理解就是⼀个⽣产-消费模型。这⾥有⼀些细节,⽐如池⼦⾥的连接使⽤中(activeCount)加上池⼦⾥剩余连接数(poolingCount)就是指当前⼀共⽣成了多少个连接,这个数不能⽐maxActive还⼤,如果⽐maxActive还⼤,则再次陷⼊等待。⽽在往池⼦⾥put连接时,则判断poolingCount是否⼤于maxActive来决定最终是否⼊池。
流程4.1:连接池瘦⾝,检查连接是否可⽤以及丢弃多余连接
整个过程如下:
整个流程分成图中主要的⼏步,⾸先利⽤poolingCount减去minIdle计算出需要做丢弃检查的连接对象区间,意味着这个区间的对象有被丢弃的可能,具体要不要放进丢弃队列evictConnections,要判断两个属性:
minEvictableIdleTimeMillis:最⼩检查间隙,缺省值30min,官⽅解释:⼀个连接在池中最⼩⽣存的时间(结合检查区间来看,闲置时间超过这个时间,才会被丢弃)。
maxEvictableIdleTimeMillis:最⼤检查间隙,缺省值7h,官⽅解释:⼀个连接在池中最⼤⽣存的时间(⽆视检查区间,只要闲置时间超过这个时间,就⼀定会被丢弃)。
如果当前连接对象闲置时间超过minEvictableIdleTimeMillis且下标在evictCheck区间内,则加⼊丢弃队列evictConnections,如果闲置时间超过maxEvictableIdleTimeMillis,则直接放⼊evictConnections(⼀般情况下会命中第⼀个判断条件,除⾮⼀个连接不在检查区间,且闲置时间超过maxEvictableIdleTimeMillis)。
如果连接对象不在evictCheck区间内,且keepAlive属性为true,则判断该对象闲置时间是否超出keepAliveBetweenTimeMillis(缺省值
60s),若超出,则意味着该连接需要进⾏连接可⽤性检查,则将该对象放⼊keepAliveConnections队列。
两个队列赋值完成后,则池⼦会进⾏⼀次压缩,没有涉及到的连接对象会被压缩到队⾸。
然后就是处理evictConnections和keepAliveConnections两个队列了,evictConnections⾥的对象会被close最后释放
掉,keepAliveConnections⾥⾯的对象将会其进⾏检测(流程参考流程1.3的isValidConnection),碰到不可⽤的连接会调⽤discard(流程1.4)抛弃掉,可⽤的连接会再次被放进连接池。
整个流程可以看出,连接闲置后,也并⾮⼀下⼦就减少到minIdle的,如果之前产⽣⼀堆的连接(不超过
maxActive),突然闲置了下来,则⾄少需要花minEvictableIdleTimeMillis的时间才可以被移出连接池,如果⼀个连接闲置时间超过maxEvictableIdleTimeMillis则必定被回收,所以极端情况下(⽐如⼀个连接池从初始化后就没有再被使⽤过),连接池⾥并不会⼀直保持minIdle个连接,⽽是⼀个都没有,⽣产环境下这是⾮常不常见的,默认的maxEvictableIdleTimeMillis都有7h,除⾮是极度冷门的系统才会出现这种情况,⽽开启keepAlive也不会推翻这个规
则,keepAlive的优先级是低于maxEvictableIdleTimeMillis的,keepAlive只是保证了那些检查中不需要被移出连接池的连接在指定检测时间内去检测其连接活性,从⽽决定是否放⼊池⼦或者直接discard。
流程4.2:主动回收连接,防⽌内存泄漏
这个流程在removeAbandoned设置为true的情况下才会触发,⽤于回收那些拿出去的使⽤长期未归还(归还:调⽤close⽅法触发主流程5)的连接。
先来看看activeConnections是什么,activeConnections⽤来保存当前从池⼦⾥被借出去的连接,这个可以通过主流程1看出来,每次调⽤getConnection时,如果开启removeAbandoned,则会把连接对象放到activeConnections,然后如果长期不调⽤close,那么这个被借出去的连接将永远⽆法被重新放回池⼦,这是⼀件很⿇烦的事情,这将存在内存泄漏的风险,因为不close,意味着池⼦会不断产⽣新的连接放进connections,不符合连接池预期(连接池出发点是尽可能少的创建连接),然后之前被借出去的连
接对象还有⼀直⽆法被回收的风险,存在内存泄漏的风险,因此为了解决这个问题,就有了这个流程,流程整体很简单,就是将现在借出去还没有归还的连接,做⼀次判断,符合条件的将会被放进abandonedList进⾏连接回收(这个list⾥的连接对象⾥的abandoned将会被置为true,标记已被该流程处理过,防⽌主流程5再次处理)。
这个如果在实践中能保证每次都可以正常close,完全不⽤设置removeAbandoned=true,⽬前如果使⽤了类似
mybatis、spring等开源框架,框架内部是⼀定会close的,所以此项是不建议设置的,视情况⽽定。
九、主流程5:回收连接
这个流程通常是靠连接包装类DruidPooledConnection的close⽅法触发的,⽬标⽅法为recycle,流程图如下:
这也是⾮常重要的⼀个流程,连接⽤完要归还,就是利⽤该流程完成归还的动作,利⽤druid对外包装的Connecion包装类DruidPooledConnection的close⽅法触发,该⽅法会通过⾃⼰内部的close或者syncClose⽅法来间接触发dataSource对象的recycle⽅法,从⽽达到回收的⽬的。
最终的recycle⽅法:
①如果removeAbandoned被设置为true,则通过traceEnable判断是否需要从activeConnections移除该连接对象,防⽌流程4.2再次检测到该连接对象,当然如果是流程4.2主动触发的该流程,那么意味着流程4.2⾥已经remove过该对象了,traceEnable会被置为false,本流程就不再触发remove了(这个流程都是在removeAbandoned=true的情况下进⾏的,在主流程1⾥连接被放进activeConnections时traceEnable被置为true,⽽在removeAbandoned=false的情况下traceEnable恒等于false)。
②如果回收过程中发现存在有未处理完的事务,则触发回滚(⽐较有可能触发这⼀条的是流程4.2⾥强制归还连接,也有可能是单纯使⽤连接,开启事务却没有提交事务就直接close的情况),然后利⽤set进⾏恢复连接对象⾥⼀些属性的默认值,除此之外,holder对象还会把由它产⽣的statement对象放到⾃⼰的⼀个arraylist⾥⾯,reset⽅法会循环着关闭内部未关闭的statement对象,最后清空list,当然,statement 对象⾃⼰也会记录下其产⽣的所有的resultSet对象,然后关闭statement时同样也会循环关闭内部未关闭的resultSet对象,这是连接池做的⼀
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论