参考文献:
[druid 常见问题](https://github.com/alibaba/druid/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98)
[池化技术(一)Druid是如何管理数据库连接的? ](https://www.cnblogs.com/hama1993/p/11421576.html)
[Druid Spring Boot Starter](https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter)

# 1. 使用方式

## 1.1 maven 仓库

```xml
<!-- spring -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>${druid-version}</version>
</dependency>

<!-- springboot -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>${druid-version}</version>
</dependency>

1.2 配置文件

参考文献:
DruidDataSource配置
DRUID连接池的实用 配置详解

spring:
  datasource:
    druid:
      # 基本属性 url、user、password
      url: @mysqlUrl@
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: @mysqlUsername@
      password: @mysqlPassword@
      # 配置初始化大小、最小、最大
      initial-size: 10
      min-idle: 10
      max-active: 20
      # 配置获取连接等待超时的时间
      max-wait: 6000
      max-wait-thread-count: -1
      # 配置一个连接在池中最小和最大生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      max-evictable-idle-time-millis: 900000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      time-between-eviction-runs-millis: 60000
      # 是否需要主动回收连接,一般不配置
      remove-abandoned: true
      # 若距离连接被获取,已经超过此值,则连接会被清掉
      remove-abandoned-timeout-millis: 10000
      # 什么时候进行链接有效性检测
      validation-query: SELECT 1
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      # 是否缓存 preparedStatement,也就是 PSCache。PSCache对支持游标的数据库性能提升巨大,比如说 oracle。在 mysql下建议关闭
      pool-prepared-statements: true
      max-open-prepared-statements: 20
      # 保持链接在 minIdle 左右
      keep-alive: true
      # 针对分布式数据库优化,每个链接的最大使用次数
      phy-max-use-count: 500
      # 如果有 initialSize 数量较多时,打开会加快应用启动时间
      async-init: true
      # 配置监控统计拦截的filters
      filters: stat
  • 基本属性 url , username, password,数据库基本配置,不再累述。

    • druid 可以根据 url 自动识别 driver-class-name,可以不配置
  • initial-size:初始化时建立物理连接的个数。

    • 缺省值 0
    • 初始化发生在显示调用 init 方法,或者第一次 getConnection 时。
    • 当我们程序启动,并没有显示调用 init 的时候,并不会初始化连接池,在一次 getConnection 才会初始化,可能会造成第一次请求过慢。
  • min-idle:连接池中最小空闲连接数量。实际可能会小于此值。

    • 缺省值 0
  • max-active:连接池最大连接数量

    • 缺省值 8
    • 需要连接数超过最大连接数量的时候,不会再创建新的,会进入等待状态,等待旧的连接释放
  • max-wait:从连接池获取连接时最大等待时间,单位毫秒。

    • 缺省值 -1
    • 配置了maxWait 之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置 useUnfairLock 属性为 true 使用非公平锁。
    • 去线程池获取连接是,若超过 max-wait,则抛出异常。
  • min-evictable-idle-time-millis:连接池中连接最小生存时间。

    • 缺省值 1000L * 60L * 30L,30 分钟
    • 空闲时间还没有超最小生存时间 minEvictableldleTimeMillis 时,是不会回收的。
    • 空闲时间超过最小生存时间是并不会全部回收,只会回收 poolingCount(连接池中数量) - minldle,minldle 数量暂时不会回收。
  • max-evictable-idle-time-millis:连接池中连接最大生存时间。

    • 缺省值 1000L * 60L * 60L * 7 , 7 小时
    • 当空闲大于最大生存时间时 maxEvictableldleTimeMillis 时,由客户端口全部回收
  • validation-query:检测连接有效性是执行的语句,一般配置为 select 1

    • mysql 默认情况下,发现一个连接空闲 8h, 则会主动断开连接,此语句的作用就是检验连接是否可用
  • time-between-eviction-runs-millis: 配置间隔多久才进行一次检测,检测需要关闭的空闲连接

    • 缺省值 6000
    • mysql 默认情况下,发现一个连接空闲 8h, 则会主动断开连接,此项配置隔多久检查一次。
    • 具体需要根据 mysql 的 wait_time 来设置。
    • 若配置不当,会取出一个无效连接。
  • test-while-idle:

    • 缺省值 true
    • 建议配置为true,不影响性能,并且保证安全性。
    • 申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,执行 validationQuery 检测连接是否有效。
  • test-on-borrow

    • 缺省值 false
    • 申请连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
  • test-on-return

    • 缺省值 false
    • 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
  • pool-prepared-statements:

    • 缺省值 false
    • 是否缓存 preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大。
    • oracle 建议开启。在 mysql 下建议关闭。
  • max-open-prepared-statements:

    • 缺省值 -1
    • 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements 自动触发修改为 true。
    • 在Druid中,不会存在 Oracle 下 PSCache 占用内存过多的问题,可以把这个数值配置大一些,比如说 100
  • keep-alive:

    • 缺省值 false
    • 开启后,初始化连接池时会填充到 minIdle 数量。
    • 开启后,连接池中的 minIdle 数量以内的连接,空闲时间超过 minEvictableIdleTimeMillis,则会执行 keepAlive 操作。
    • 开启后,当网络断开等原因产生的由 ExceptionSorter 检测出来的死连接被清除后,自动补充连接到 minIdle 数量。
    • 简单来说,开启后,通过超时重新创建连接的手段,可以把连接数量,维持在 min-idle 左右(即使空闲时间超过 max-evictable-idle-time-millis),防止并发上升时创建连接花费太长时间。
  • keep-alive-between-time-millis:

    • 开启 keepAlive 时,闲置时间超过此会进行检测,检测不会和 testWhileIdle 冲突
    • 当 min-idle 内连接数量,闲置时间大于 minEvictableIdleTimeMillis 且小于 minEvictableIdleTimeMillis 闲置时间时,若闲置时间大于 keepAliveBetweenTimeMillis,则会进行活性检测。
    • keepAliveBetweenTimeMillis 必须大于 time-between-eviction-runs-millis,但检测次数不会冲突,time-between-eviction-runs-millis 进行检测时,会判断闲置距离上次 keep-alive-between-time-millis 是不是大于 time-between-eviction-runs-millis。
  • phy-max-use-count:

    • 缺省值 -1
    • 针对分布式数据库优化,每个链接的最大使用次数,若达到使用次数,则回收,创建新的,达到每个数据库负载均衡的目的。
  • filters:在下文介绍

  • async-init:是 1.1.4 中新增加的配置,如果有 initialSize 数量较多时,打开会加快应用启动时间。

    • 缺省值 false
    • 初始化时,线程池异步新增,加快 initial-size 速度。
    • 不开启的话使用默认方式:开启不同的守护线程通过 await、signal 通信实现线程池新增。
  • max-wait-thread-count

    • 缺省值 -1,不启用
    • 如果 maxWaitThreadCount 配置大于0,表示启用,这是 druid 做的一种丢弃措施,如果你不希望在池子里的连接完全不够用导阻塞的业务线程过多,就可以考虑配置该项,这个属性的意思是说在连接不够用时最多让多少个业务线程发生阻塞,一旦超过甚至不触发 pollLast 的调用(防止新增等待线程),直接抛错。
  • remove-abandoned

    • 缺省值 false
    • 对于被从连接池取走的连接,若一直不归还,不归还的时间超过 removeAbandonedTimeoutMillis, 则会被当做泄露连接清除掉

2. filters

参考:Druid Spring Boot Starter

2.1 使用 druid 内置监控页面

参考:配置_StatViewServlet配置

# StatViewServlet配置

#是否启用StatViewServlet(监控页面)默认值为false(考虑到安全问题默认并未启动,如需启用建议设置密码或白名单以保障安全)
spring.datasource.druid.stat-view-servlet.enabled=true 
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*

# 配置界面的访问账号和密码
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=admin

# 在 StatViewSerlvet 输出的 html 页面中,有一个功能是 Reset All,执行这个操作之后,会导致所有计数器清零,重新计数。你可以通过配置参数关闭它。
spring.datasource.druid.stat-view-servlet.reset-enable=true

# 访问控制
spring.datasource.druid.stat-view-servlet.allow=128.242.127.1/24,128.242.128.1
spring.datasource.druid.stat-view-servlet.deny=128.242.127.4
  • deny 优先于 allow,如果在 deny 列表中,就算在 allow 列表中,也会被拒绝。
  • 如果 allow 没有配置或者为空,则允许所有访问
  • 配置完成后可通过 /druid/index.html 看到界面,此时并没有 sql 监控信息

2.2 stat 监控统计

参考:配置_StatFilter

2.2.1 开启监控

Druid 的监控统计功能是通过 filter-chain 扩展实现,如果要打开监控统计功能,配置 StatFilter。

spring.datasource.druid.filters=stat

配置完成后即可查看到 sql 监控项

image-20220302174733763

2.2.2 sql 合并配置

当你程序中存在没有参数化的sql执行时,sql统计的效果会不好。比如:

select * from t where id = 1
select * from t where id = 2
select * from t where id = 3

在统计中,显示为3条sql,这不是我们希望要的效果。StatFilter提供合并的功能,能够将这3个SQL合并为如下的SQL

select * from t where id = ?

做如下配置即可

spring.datasource.druid.filters=stat,mergeStat

2.2.3 慢 sql 记录

# 慢 sql 时间,缺省值 为 3000
spring.datasource.druid.filter.stat.slow-sql-millis=5000
# 是否记录 log
spring.datasource.druid.filter.stat.log-slow-sql=true

2.3 web 关联监控配置

参考:配置_配置WebStatFilter

# WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter
# 是否启用StatFilter默认值false
spring.datasource.druid.web-stat-filter.enabled=true
spring.datasource.druid.web-stat-filter.url-pattern=/*
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*

# session 监控相关
spring.datasource.druid.web-stat-filter.session-stat-enable=true
spring.datasource.druid.web-stat-filter.session-stat-max-count=1000  # 缺省 1000
# 配置 principalSessionName,使得 druid 能够知道当前的 session 的用户是谁。
spring.datasource.druid.web-stat-filter.principal-session-name=sessionName
# 如果 user 信息保存在 cookie 中,你可以配置 principalCookieName,使得 druid 知道当前的 user 是谁
spring.datasource.druid.web-stat-filter.principal-cookie-name=cookieName

# 配置 profileEnable 能够监控单个 url 调用的 sql 列表(点击 url 有个 profiles)
spring.datasource.druid.web-stat-filter.profile-enable=true

配置完成后,我们可以看到如下效果

image-20220302183405869

2.4 spring 关联监控配置

参考:配置_Druid和Spring关联监控配置

# Spring 监控配置,配置 _Druid 和 Spring 关联监控配置
spring.datasource.druid.aop-patterns= # Spring监控AOP切入点,如x.y.z.service.*,配置多个英文逗号分隔

image-20220302183947270

2.5 wall

参考:配置 wallfilter

Druid 提供了 WallFilter,它是基于 SQL 语义分析来实现防御 SQL 注入攻击的。

配置方式

spring.datasource.druid.filters=wall

与其他 filter 一起使用

spring.datasource.druid.filters=stat,wall

若 stat 在 wall 后面则无法统计拦截检测的时间

具体详细配置参考官网。

2.6 logFilter

参考:配置_LogFilter

我们想看 druid 日志的话,做如下配置

spring.datasource.druid.filters=slf4j
logging.level.druid.sql=debug

若需要指定日志,可以查看官网配置。

3. 流程分析

下面的比较难,做了解即可

参考:池化技术(一)Druid是如何管理数据库连接的?

类图和属性表:druid 类图 属性表

3.1 流程浏览

基于依赖程序的版本信息:druid:1.1.16 驱动程序mysql-connector-java:8.0.17

下方流程中涉及到的类、属性、方法名均列在这里:druid 类图 属性表 ← 该表格用来辅助理解下面的流程图和代码,不用细看,混乱时可用来理清关系。

本文会通过 getConnection 作为入口,探索在 druid 里,一个连接的生命周期。大体流程被划分成了以下几个主流程:

1569484-20190922130915740-1234796908

3.2 主流程 1:获取连接流程

首先从入口来看看它在获取连接时做了哪些操作:

1569484-20190912001026114-815718351

主流程 1 (对应源代码 druid 获取连接源码

上述为获取连接时的流程图,首先会调用 init 进行连接池的初始化,然后运行责任链上的每一个 filter,最终执行 getConnectionDirect 获取真正的连接对象,如果开启了 testOnBorrow,则每次都会去测试连接是否可用*(这也是官方不建议设置 testOnBorrow 为 true 的原因,影响性能,这里的测试是指测试 mysql 服务端的长连接是否断开,一般 mysql 服务端长连保活时间是8h,被使用一次则刷新一次使用时间,若一个连接距离上次被使用超过了保活时间,那么再次使用时将无法与 mysql 服务端通信)*,如果 testOnBorrow 没有被置为 true,则会进行 testWhileIdle 的检查(这一项官方建议设置为 true,缺省值也是 true),检查时会判断当前连接对象距离上次被使用的时间是否超过规定检查的时间,若超过,则进行检查一次,这个检查时间通过 timeBetweenEvictionRunsMillis 来控制,默认60s,每个连接对象会记录下上次被使用的时间,用当前时间减去上一次的使用时间得出闲置时间,闲置时间再跟 timeBetweenEvictionRunsMillis 比较,超过这个时间就做一次连接可用性检查,这个相比 testOnBorrow 每次都检查来说,性能会提升很多,用的时候无需关注该值,因为缺省值是 true,经测试如果将该值设置为 falsetestOnBorrow 也设置为 false,数据库服务端长连保活时间改为 60s,60s 内不使用连接,超过 60s 后使用将会报连接错误。若使用 testConnectionInternal 方法测试长连接结果为 false,则证明该连接已被服务端断开或者有其他的网络原因导致该连接不可用,则会触发 discardConnection 进行连接回收(对应流程1.4,因为丢弃了一个连接,因此该方法会唤醒主流程3进行检查是否需要新建连接)。整个流程运行在一个死循环内,直到取到可用连接或者超过重试上限报错退出(在连接没有超过连接池上限的话,最多重试一次(重试次数默认重试1次,可以通过 notFullTimeoutRetryCount 属性来控制),所以取连接这里一旦发生等待,在连接池没有满的情况下,最大等待 maxWait 的时间)。

特别说明 1

为了保证性能,不建议将 testOnBorrow 设置为 true,或者说牵扯到长连接可用检测的那几项配置使用 druid 默认的配置就可以保证性能是最好的,如上所说,默认长连接检查是 60s 一次,所以不启用 testOnBorrow 的情况下要想保证万无一失,自己要确认下所连的那个 mysql 服务端的长连接保活时间(虽然默认是8h,但是 dba 可能给测试环境设置的时间远小于这个时间,所以如果这个时间小于 60s,就需要手动设置 timeBetweenEvictionRunsMillis 了,如果 mysql 服务端长连接时间是 8h 或者更长,则用默认值即可。

我们配置的时候,wait_time(数据库最大闲置断开时间) ,maxEvictableIdleTimeMillis 应该小于 wait_time ,并且 maxEvictableIdleTimeMillis + 2 * timeBetweenEvictionRunsMillis <= wait_time,来保证连接池不会出现死连接。

特别说明 2

为了防止不必要的扩容,在 mysql 服务端长连接够用的情况下,对于一些 qps 较高的服务、网关业务,建议把池子的最小闲置连接数 minIdle 和最大连接数 maxActive 设置成一样的,且按照需要调大,且开启 keepAlive 进行连接活性检查(参考流程4.1),这样就不会后期发生动态新建连接的情况(建连还是个比较重的操作,所以不如一开始就申请好所有需要的连接,个人意见,仅供参考),但是像管理后台这种,长期 qps 非常低,但是有的时候需要用管理后台做一些巨大的操作(比如导数据什么的)导致需要的连接暴增,且管理后台不会特别要求性能,就适合将 minIdle 的值设置的比 maxActive 小,这样不会造成不必要的连接浪费,也不会在需要暴增连接的时候无法动态扩增连接。

3.3 主流程 2:初始化连接池

通过上面的流程图可以看到,在获取一个连接的时候首先会检查连接池是否已经初始化完毕(通过 inited 来控制,bool 类型,未初始化为 flase,初始化完毕为 true,这个判断过程在 init 方法内完成),若没有初始化,则调用 init 进行初始化(图主流程1中的紫色部分),下面来看看 init 方法里又做了哪些操作:

1569484-20191014184421283-439311837

主流程 2 (对应源代码 druid 初始化连接池)

可以看到,实例化的时候会初始化全局的重入锁 lock,在初始化过程中包括后续的连接池操作都会利用该锁保证线程安全,初始化连接池的时候首先会进行双重检查是否已经初始化过,若没有,则进行连接池的初始化,这时候还会通过SPI机制额外加载责任链上的 filter,但是这类 filter 需要在类上加上 @AutoLoad 注解。然后初始化了三个数组,容积都为 maxActive,首先 connections 就是用来存放池子里连接对象的,evictConnections 用来存放每次检查需要抛弃的连接(结合流程4.1理解),keepAliveConnections 用于存放需要连接检查的存活连接(同样结合流程4.1理解),然后生成初始化数(initialSize)个连接,放进 connections,然后生成两个必须的守护线程,用来添加连接进池以及从池子里摘除不需要的连接,这俩过程较复杂,因此拆出来单说(主流程3主流程4)。

特别说明 1

从流程上看如果一开始实例化的时候不对连接池进行初始化(这个初始化是指对池子本身的初始化,并非单纯的指 druid 对象属性的初始化),那么在第一次调用 getConnection 时就会走上图那么多逻辑,尤其是耗时较久的建立连接操作,被重复执行了很多次,导致第一次 getConnection 时耗时过久,如果你的程序并发量很大,那么第一次获取连接时就会因为初始化流程而发生排队,所以建议在实例化连接池后对其进行预热,通过调用 **init 方法 **或者 getConnection 方法都可以。

特别说明 2

在构建全局重入锁的时候,利用 lock 对象生成了俩 Condition,对这俩 Condition 解释如下:

当连接池连接够用时,利用 empty 阻塞添加连接的守护线程(主流程3),当连接池连接不够用时,获取连接的那个线程(这里记为业务线程A)就会阻塞在 notEmpty 上,且唤起阻塞在 empty 上的添加连接的守护线程,走完添加连接的流程,走完后会重新唤起阻塞在 notEmpty 上的业务线程 A,业务线程 A 就会继续尝试获取连接。

3.4 流程 1.1:责任链

WARN: 这块东西结合源码看更容易理解

1569484-20190921100847272-265680782

流程 1.1 对应源码 druid 责任链

这里对应流程1里获取连接时需要执行的责任链,每个 DruidAbstractDataSource 里都包含 filters 属性,filters 是对 DruidFilters 接口的实现,里面有很多对应着连接池里的映射方法,比如例子中 dataSource 的 getConnection 方法在触发的时候就会利用 FilterChain 把每个 filter 里的 dataSource_getConnection 给执行一遍,这里也要说明下 FilterChain,通过流程1.1可以看出来,datasource 是利用 FilterChain 来触发各个 filter 的执行的,FilterChain 里也有一堆 datasource 里的映射方法,比如上图里的 dataSource_connect,这个方法会把 datasource 里的 filters 全部执行一遍直到 nextFilter 取不到值,才会触发 dataSource.getConnectionDirect,这个结合代码会比较容易理解。

3.5 流程 1.2:从池中获取连接流程

11

流程 1.2 (对应源代码 druid 获取连接实现)

通过 getConnectionInternal 方法从池子里获取真正的连接对象,druid 支持两种方式新增连接,一种是通过开启不同的守护线程通过 await、signal 通信实现(本文启用的方式,也是默认的方式),另一种是直接通过线程池异步新增,这个方式通过在初始化 druid 时传入 asyncInit=true,再把一个线程池对象赋值给 createScheduler,就成功启用了这种模式,没仔细研究这种方式,所以本文的流程图和代码块都会规避这个模式。

上面的流程很简单,连接足够时就直接 poolingCount-1,数组取值,返回,activeCount+1,整体复杂度为 O(1),关键还是看取不到连接时的做法,取不到连接时,druid 会先唤起新增连接的守护线程新增连接,然后陷入等待状态,然后唤醒该等待的点有两处,一个是用完了连接 **recycle(主流程5)**进池子后触发,另外一个就是新增连接的守护线程成功新增了一个连接后触发,await 被唤起后继续加入锁竞争,然后往下走如果发现池子里的连接数仍然是0(说明在唤醒后参与锁竞争里刚被放进来的连接又被别的线程拿去了),则继续下一次的 await,这里采用的是 awaitNanos 方法,初始值是 maxWait,然后下次被刷新后就是 maxWait 减去上次阻塞花费的实际时间,每次 await 的时间会逐步减少,直到归零,整体时间是约等于 maxWait 的,但实际比 maxActive 要大,因为程序本身存在耗时以及被唤醒后又要参与锁竞争导致也存在一定的耗时。

如果最终都没办法拿到连接则返回 null 出去,紧接着触发主流程1中的重试逻辑。

druid如何防止在获取不到连接时阻塞过多的业务线程?

通过上面的流程图和流程描述,如果非常极端的情况,池子里的连接完全不够用时,会阻塞过多的业务线程,甚至会阻塞超过 maxWait 这么久,有没有一种措施是可以在连接不够用的时候控制阻塞线程的个数,超过这个限制后直接报错,而不是陷入等待呢?

druid 其实支持这种策略的,在 maxWaitThreadCount 属性为默认值(-1)的情况下不启用,如果 maxWaitThreadCount 配置大于0,表示启用,这是 druid 做的一种丢弃措施,如果你不希望在池子里的连接完全不够用导阻塞的业务线程过多,就可以考虑配置该项,这个属性的意思是说在连接不够用时最多让多少个业务线程发生阻塞,流程1.2的图里没有体现这个开关的用途,可以在代码里查看,每次在 pollLast 方法里陷入等待前会把属性 notEmptyWaitThreadCount 进行累加,阻塞结束后会递减,由此可见 notEmptyWaitThreadCount 就是表示当前等待可用连接时阻塞的业务线程的总个数,而 getConnectionInternal 在每次调用 pollLast 前都会判断这样一段代码:

if (maxWaitThreadCount > 0 && notEmptyWaitThreadCount >= maxWaitThreadCount) {
    connectErrorCountUpdater.incrementAndGet(this);
    throw new SQLException("maxWaitThreadCount " + maxWaitThreadCount + ", current wait Thread count "
            + lock.getQueueLength()); //直接抛异常,而不是陷入等待状态阻塞业务线程
}

可以看到,如果配置了 maxWaitThreadCount 所限制的等待线程个数,那么会直接判断当前陷入等待的业务线程是否超过了 maxWaitThreadCount,一旦超过甚至不触发 pollLast 的调用(防止新增等待线程),直接抛错。

一般情况下不需要启用该项,一定要启用建议考虑好 maxWaitThreadCount 的取值,一般来说发生大量等待说明代码里存在不合理的地方:比如典型的连接池基本配置不合理,高 qps 的系统里 maxActive 配置过小;比如借出去的连接没有及时 close 归还;比如存在慢查询或者慢事务导致连接借出时间过久。这些要比配置 maxWaitThreadCount 更值得优先考虑,当然配置这个做一个极限保护也是没问题的,只是要结合实际情况考虑好取值。

3.6 流程 1.3:连接可用性检测

3.6.1 init-checker

讲这块的东西之前,先来了解下如何初始化检测连接用的 checker,整个流程参考下图:

1569484-20190912000425345-1240245652

init-checker流程图(对应源代码:druid 初始化连接池) 中的 initValidConnectionChecker 方法与 MySqlValidConnectionChecker 构造器)

初始化 checker 发生在 init 阶段(限于篇幅,没有在主流程2(init阶段)里体现出来,只需要记住初始化 checker 也是发生在 init 阶段就好),druid 支持多种数据库的连接源,所以 checker 针对不同的驱动程序都做了适配,所以才看到图中 checker 有不同的实现,我们根据加载到的驱动类名匹配不同的数据库 checker,上图匹配至 mysql 的 checker,checker 的初始化里做了一件事情,就是判断驱动内是否有 ping 方法(jdbc4 开始支持,mysql-connector-java 早在 3.x 的版本就有 ping 方法的实现了),如果有,则把 usePingMethod 置为 true,用于后续启用 checker 时做判断用(下面会讲,这里置为 true,则通过反射的方式调用驱动程序的 ping 方法,如果为 false,则触发普通的 SELECT 1 查询检测,SELECT 1 就是我们非常熟悉的那个东西啦,新建 statement,然后执行 SELECT 1,然后再判断连接是否可用)。

3.6.2 testConnectionInternal

然后回到本节探讨的方法:流程 1.3 对应的 testConnectionInternal

1569484-20190912144327137-2129610810

流程 1.3 (对应源代码 druid 连接可用性测试)

这个方法会利用 主流程2(init阶段) 里初始化好的 checker 对象(流程参考 init-checker)里的 isValidConnection 方法,如果启用 ping,则该方法会利用 invoke 触发驱动程序里的 ping 方法,如果不启用 ping,就采用 SELECT 1 方式(从 init-checker 里可以看出启不启用取决于加载到的驱动程序里是否存在相应的方法)。

3.7 流程 1.4:抛弃连接

1569484-20190912160602807-645950562

流程 1.4 (对应源代码 druid 丢弃连接)

经过流程1.3返回的测试结果,如果发现连接不可用,则直接触发抛弃连接逻辑,这个过程非常简单,如上图所示,由流程1.2获取到该连接时累加上去的 activeCount,在本流程里会再次减一,表示被取出来的连接不可用,并不是 active 状态。其次这里的 close 是拿着驱动那个连接对象进行 close,正常情况下一个连接对象会被 druid 封装成 DruidPooledConnection 对象,内部持有的 conn 就是真正的驱动 Connection对象,上图中的关闭连接就是获取的该对象进行 close,如果使用包装类 DruidPooledConnection 进行 close,则代表回收连接对象**(recycle,参考主流程5)**

3.8 主流程 3:添加连接的守护进程

1569484-20190912204627147-1035809259

主流程 3 (源代码 druid 新增连接)

在**主流程2(init 初始化阶段)**时就开启了该流程,该流程独立运行,大部分时间处于等待状态,不会抢占 cpu,但是当连接不够用时,就会被唤起追加连接,成功创建连接后将会唤醒其他正在等待获取可用连接的线程,比如:

结合流程1.2来看,当连接不够用时,会通过 empty.signal 唤醒该线程进行补充连接(阻塞在 empty 上的线程只有主流程3的单线程),然后通过 notEmpty 阻塞自己,当该线程补充连接成功后,又会对阻塞在 notEmpty 上的线程进行唤醒,让其进入锁竞争状态,简单理解就是一个生产-消费模型。这里有一些细节,比如池子里的连接使用中(activeCount)加上池子里剩余连接数(poolingCount)就是指当前一共生成了多少个连接,这个数不能比 maxActive 还大,如果比 maxActive 还大,则再次陷入等待。而在往池子里 put 连接时,则判断 poolingCount 是否大于 maxActive 来决定最终是否入池。

3.9 主流程 4:抛弃连接的守护线程

1569484-20190916170027155-1875776405

主流程 3 (对应源代码 druid 丢弃连接的守护线程)

3.9.1 流程 4.1 连接池瘦身

连接池瘦身,检查连接是否可用以及丢弃多余连接

1569484-20191008235109857-736157559

流程 4.1 (对应源代码 druid 连接池瘦身

整个流程分成图中主要的几步,首先利用 poolingCount 减去 minIdle 计算出需要做丢弃检查的连接对象区间,意味着这个区间的对象有被丢弃的可能,具体要不要放进丢弃队列 evictConnections,要判断两个属性:

minEvictableIdleTimeMillis:最小检查间隙,缺省值30min,官方解释:一个连接在池中最小生存的时间(结合检查区间来看,闲置时间超过这个时间,才会被丢弃)。

**maxEvictableIdleTimeMillis:**最大检查间隙,缺省值 7h,官方解释:一个连接在池中最大生存的时间(无视检查区间,只要闲置时间超过这个时间,就一定会被丢弃)。

如果当前连接对象闲置时间超过 minEvictableIdleTimeMillis 且下标在 evictCheck 区间内,则加入丢弃队列 evictConnections,如果闲置时间超过 maxEvictableIdleTimeMillis,则直接放入 evictConnections(一般情况下会命中第一个判断条件,除非一个连接不在检查区间,且闲置时间超过 maxEvictableIdleTimeMillis)。

如果连接对象不在 evictCheck区间内,且 keepAlive 属性为 true,则判断该对象闲置时间是否超出 keepAliveBetweenTimeMillis(缺省值60s),若超出,则意味着该连接需要进行连接可用性检查,则将该对象放入 keepAliveConnections 队列。

两个队列赋值完成后,则池子会进行一次压缩,没有涉及到的连接对象会被压缩到队首。

然后就是处理 evictConnectionskeepAliveConnections 两个队列了,evictConnections 里的对象会被 close 最后释放掉,keepAliveConnections 里面的对象将会其进行检测(流程参考流程1.3的 isValidConnection),碰到不可用的连接会调用 discard(流程1.4)抛弃掉,可用的连接会再次被放进连接池。

整个流程可以看出,连接闲置后,也并非一下子就减少到 minIdle 的,如果之前产生一堆的连接(不超过 maxActive),突然闲置了下来,则至少需要花 minEvictableIdleTimeMillis 的时间才可以被移出连接池,如果一个连接闲置时间超过 maxEvictableIdleTimeMillis 则必定被回收,所以极端情况下(比如一个连接池从初始化后就没有再被使用过),连接池里并不会一直保持 minIdle 个连接,而是一个都没有,生产环境下这是非常不常见的,默认的 maxEvictableIdleTimeMillis 都有 7h,除非是极度冷门的系统才会出现这种情况,而开启 keepAlive 也不会推翻这个规则,keepAlive 的优先级是低于 maxEvictableIdleTimeMillis 的,keepAlive 只是保证了那些检查中不需要被移出连接池的连接在指定检测时间内去检测其连接活性,从而决定是否放入池子或者直接 discard

3.9.2 主动回收链接

主动回收连接,防止内存泄漏

1569484-20190918044323246-916646998

流程 4.2 (对应源代码 druid 连接瘦身-主动回收连接)

这个流程在 removeAbandoned 设置为 true 的情况下才会触发,用于回收那些拿出去的使用长期未归还(归还:调用 close 方法触发主流程5)的连接。

先来看看 activeConnections 是什么,activeConnections 用来保存当前从池子里被借出去的连接,这个可以通过主流程1看出来,每次调用 getConnection 时,如果开启 removeAbandoned,则会把连接对象放到 activeConnections,然后如果长期不调用 close,那么这个被借出去的连接将永远无法被重新放回池子,这是一件很麻烦的事情,这将存在内存泄漏的风险,因为不 close,意味着池子会不断产生新的连接放进 connections,不符合连接池预期(连接池出发点是尽可能少的创建连接),然后之前被借出去的连接对象还有一直无法被回收的风险,存在内存泄漏的风险,因此为了解决这个问题,就有了这个流程,流程整体很简单,就是将现在借出去还没有归还的连接,做一次判断,符合条件的将会被放进 abandonedList 进行连接回收(这个 list 里的连接对象里的 abandoned 将会被置为 true,标记已被该流程处理过,防止主流程5再次处理,具体可以参考代码段 druid 回收连接)。

这个如果在实践中能保证每次都可以正常 close,完全不用设置 removeAbandoned=true,目前如果使用了类似 mybatis、spring 等开源框架,框架内部是一定会 close 的,所以此项是不建议设置的,视情况而定。

3.10 主流程 5:回收连接

这个流程通常是靠连接包装类 DruidPooledConnectionclose 方法触发的,目标方法为 recycle,流程图如下:1569484-20191110095639249-1275559516

主流程 5 (对应源代码 druid 回收连接)

这也是非常重要的一个流程,连接用完要归还,就是利用该流程完成归还的动作,利用 druid 对外包装的 Connecion 包装类 DruidPooledConnectionclose 方法触发,该方法会通过自己内部的 close 或者 syncClose 方法来间接触发 dataSource 对象的 recycle 方法,从而达到回收的目的。

最终的 recycle 方法:

①如果 removeAbandoned 被设置为 true,则通过 traceEnable 判断是否需要从 activeConnections 移除该连接对象,防止流程4.2再次检测到该连接对象,当然如果是流程4.2主动触发的该流程,那么意味着流程4.2里已经 remove 过该对象了,traceEnable 会被置为 false,本流程就不再触发 remove了(这个流程都是在 removeAbandoned=true 的情况下进行的,在主流程1里连接被放进 activeConnectionstraceEnable被置为 true,而在 removeAbandoned=false 的情况下 traceEnable 恒等于 false)。

②如果回收过程中发现存在有未处理完的事务,则触发回滚(比较有可能触发这一条的是流程4.2里强制归还连接,也有可能是单纯使用连接,开启事务却没有提交事务就直接 close 的情况),然后利用 holder.reset 进行恢复连接对象里一些属性的默认值,除此之外, holder 对象还会把由它产生的 statement 对象放到自己的一个 arraylist 里面,reset 方法会循环着关闭内部未关闭的 statement 对象,最后清空list,当然,statement 对象自己也会记录下其产生的所有的 resultSet 对象,然后关闭 statement 时同样也会循环关闭内部未关闭的 resultSet 对象,这是连接池做的一种保护措施,防止用户拿着连接对象做完一些操作没有对打开的资源关闭。

③判断是否开启 testOnReturn,这个跟 testOnBorrow 一样,官方默认不开启,也不建议开启,影响性能,理由参考主流程1里针对 testOnBorrow 的解释。

④直接放回池子(当前 connections 的尾部),然后需要注意的是 putLast 方法和 put 方法的不同之处, putLast 会把 lastActiveTimeMillis 置为当前时间,也就是说不管一个连接被借出去过久,只要归还了,最后活跃时间就是当前时间,这就会有造成某种特殊异常情况的发生(非常极端,几乎不会触发,可以选择不看):

如果不开启 testOnBorrow 和 testOnReturn,并且 keepAlive 设置为 false,那么长连接可用测试的间隔依据就是利用当前时间减去上次活跃时间(lastActiveTimeMillis)得出闲置时间,然后再利用闲置时间跟 timeBetweenEvictionRunsMillis(默认60s)进行对比,超过才进行长连接可用测试。

那么如果一个 mysql 服务端的长连接保活时间被人为调整为 60s,然后 timeBetweenEvictionRunsMillis 被设置为59s,这个设置是非常合理的,保证了测试间隔小于长连接实际保活时间,然后如果这时一个连接被拿出去后一直过了 61s 才被 close 回收,该连接对象的 lastActiveTimeMillis 被刷为当前时间,如果在 59s 内再次拿到该连接对象,就会绕过连接检查直接报连接不可用的错误。