Druid系列--连接池原理

Posted 入境繁华

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Druid系列--连接池原理相关的知识,希望对你有一定的参考价值。

 作者 | 畅为


连接是一种昂贵资源,每次创建销毁需要开销。Druid 可以看做一家 数据库连接池的租赁公司,负责租赁连接,出租给需要的用户,用户使用完成就返还给 Druid。用户就不用关心连接池的创建和销毁细节。


   与其他连接池不同,druid没有一个专门的连接池类,比如ConnnetPool。druid的连接池都交给了DruidDataSource负责管理,用数据connects存放连接。本文将介绍 Druid 是如何维护这个连接池,如何完成连接的获取和回收。


数据结构


首先看druid 是通过怎样的数据结构存储连接:

  1. //连接池

  2. private DruidConnectionHolder[] connections = new DruidConnectionHolder[maxActive];

  3. private int   poolingCount  = 0; // 当前连接池中数据库连接的数量

  4. private int   activeCount   = 0; // 正在使用的连接数量

  5. // 线程之间的通讯

  6. protected Condition  notEmpty;

  7. protected Condition  empty;

  8. protected ReentrantLock  lock; // 保证现在安全

connections 存放着当前可用的所有连接,但这些不是 druid 的所有连接,druid 还有一部分连接可能正在被使用。connections 存放的是一个代理类,将连接做一层包装。在代理类中可以做很多事情,比如监控拦截、统计等。同时在执行 close 时不直接关闭,可以在这里做资源回收操作。

  • poolingCount 是当前可用连接数量,即存放在 connections 中的链接数量。

  • activeCount 是当前正在被使用的连接数量,这部分已经被用户拿去使用还没归还。


poolingCount + activeCount 的数量才是 druid 拥有的所有连接数量。

这里为什么可用使用数组结构存储呢?数据库连接池在初始化时就已经指定了可分配连接的最大数量 maxActive,因此存储空间是固定的。


Druid 还有三个总要的属性:

  • notEmpty:不为空通知。用来通知其他线程池已经有可用连接。如果没有连接了 notEmpty 上等待,将线程挂起,等待数据库连接;当有新的连接的时候就 通过 notEmpty 进行通知广播等待的线程

  • empty:  空通知。用来通知其他线程目前没有可用连接,通知创建新连接。创建线程在 empty 上等待,发现有empty 信号时就会去创建连接。

  • lock:是一把全局锁,为了保证在多线程环境下对 connections、poolingCount、activeCount 操作的数据一致性。因此在涉及到这些参数变化的地方都使用这边锁。


连接池原理


DruidDataSource 是数据库连接池的核心部分,可以看做是 Druid 连接池租赁公司的老板。这家公司创立之初,老板会去中招聘其他三位核心成员:

  • 创建连接池线程 CreateConnectionThread。主要职责就是创建连接,当连接不够的时候都是交给他创建,满足用户对连接的需求。

  • 销毁连接线程  DestroyConnectionThread。主要职责线程池的销毁,将一些空闲连接、不健康连接清除,维持一个最小空闲连接数。

  • 日志线程 LogStatsThread。可以看做公司的财务,每隔一段时间记录连接的消费和使用细节。


DruidDataSource 在初始化的时候就 创建以上三个线程。初始化时还会做以下几个事情:

  • 将 connections 初始化,大小设置为 maxActive。一旦创建好以后就不能发生变化了,连接最大数量不能改变。

  • 根据用户配置的 minActive 数量初始化连接,这样启动以后就有直接可用的连接。为了提高启动速度也可以将这个初始化工作进行异步化(druid.asyncInit 参数设置为true即可)。


在介绍公司是如何运作之前,需要简单介绍在 druid 中几个永恒的准则。

  1. 连接池数量最大不能超过 maxActive. 

  2. 最小连接数不能低于 minIdle.


              

  • 用户通过DruidDataSource 去获取和回收连接。

  • 获取连接时首先判断连接池是否有可用连接,如果有直接从conecets 尾部获取一个。否则发送empty 信号告诉 CreateConenctionThread 创建连接,并挂起等待,直到有新的连接。

  • CreateConenctionThread 创建连接完成后,发起 notEmpty 通知等待线程。等待线程恢复,并从connects 尾部获取连接。

  • DestroyConnectionThread 负责收缩连接池,如果连接数量小于 minIdle 则通知 CreateConnectionThread.

  • LogStatsThread 则负责记录日志。



DruidDataSource

获取连接

当用户需要连接时,调用 dataSource的getConnection 方法。 DruidDataSource 首先需要判断当前线程池是否有可用的连接,如果有可用的连接则采用LRU策略获取,直接从 connections 尾部获取一个连接返回。尾部的连接是当前连接池中最活跃的连接,要么是刚刚释放回去的连接,要么就是新创建的。因此最大程度保证连接是可用的,健康的。


decrementPoolingCount();

DruidConnectionHolder last = connections[poolingCount];

如果当前连接池没有可用的连接,那么首先会发一个信号告诉 CreateConnectionThread 去创建新的连接,然后将自己挂起直到有新的可用连接。新的连接会从两个地方获得,一个是 CreateConnectionThread,一个是刚刚回收的连接。


emptySignal();   

notEmpty.await(); 

取连接的时候提供了两种方式:

  • getConnection() 进行无限等待, 直到有连接以后才返回。这种会导致业务方无限等待。

  • getConnection(nanos) 指定时间等待,如果在指定时间没有可用连接则直接返回。


连接从连接池中获取,有的连接在连接池中存放时间很久可能已经失去活性不能使用。因此在druid 中支持连接获取后对连接校验的能力,只要配置  testOnBorrow=true 并且配置 validationQuery即可, druid 连接活性校验方式实际上是通过执行sql 方式来验证,直接执行配置的 validationQuery 。 


stmt.executeQuery(validationQuery);

如果没有配置 testOnBorrow, druid 也支持在获取连接以后对于空闲很久的连接做检查 ,可以通过testWhileIdle开关设置。如果连接的空闲时间大于 timeBetweenEvictionRunsMillis, 则执行 validationQuery 检查连接的可用性。对于不可用的连接进行销毁,并再次获取新的连接。可以通过这些方式尽可能让连接处于活跃、健康状态。


回收连接

用户使用的Connection 是代理代理类 DruidConnectionHolder, 在执行 close 时不是直接将连接关闭,而是将连接回收。


回收时同时也支持对连接做个检查,将健康的连接存入到连接池。默认不检查,可以通过配置 testOnReturn=true, 则会执行 validationQuery 来检查连接是否可用。如果不可用则不回收,并销毁。回收的连接放在connects 末尾,存放完毕后发发一个 notEmpty 通知,通知正在等待连接的线程。


CreateConnectionThread


如名字所示,这个主要是一个负责创建连接。不是时刻都在创建连接,当没有可用连接时,用户线程进入等待挂起状态,并且连接数量小于maxActive 时才创建连接,否则就偷懒睡觉。在他的世界里准守以下两个规则:

  • 必须是存在线程等待,才会去创建连接。否则挂起线程睡眠等待。

  • 如果当前连接数量小于 maxActive 才去创建,否则挂起睡眠等待。


如果以上两个规则满足了,则会去创建连接。创建连接的底层使用 JDBC 这一套逻辑, 利用Driver 和配置信息连接到数据库:


conn = getDriver().connect(url, info);

创建完的连接直接存在 connections 末尾,并发一个notEmpty信号告诉所有在等待的线程。等待线程则从睡眠中恢复,再尝试去获取连接。


DestroyConnectionThread


该线程主要工作是将空闲及无效的连接销毁。默认情况下每1000ms 执行一次,可以通过timeBetweenEvictionRunsMillis 时间设置执行间隔。 这里使用的是 最近最久未使用策略进行回收。创建连接和获取连接的时候都是从 connects 尾部获取,尾部连接相对活跃,而头部的连接可能长时间未被使用,相对而言活性更低。因此每次回收都是从connects 头部开始遍历,主要回收几类连接:

  • 连接的空闲时间大于 maxEvictableIdleTimeMillis, 则进行回收。

  • 大于minIdle 部分的连接会被回收。保证连接池空闲连接不会太多。如果连接池中有大于 minIdle 的连接,说明当前连接池不是很繁忙,因此可以清理。如果为了避免频繁销毁和关闭连接,建议设置 minIdle。 

  • 检查连接活跃度,不健康的连接则关闭。默认不检查,可以通过 druid.keepAlive 打开连接的健康检查,当开关打开以后,如果空闲时间超过 keepAliveBetweenTimeMills 则会判断连接的健康情况(默认 keepAliveBetweenTimeMills 是 2 分钟)。这种在网络不稳定的场景下可以使用,利用 DestroyConnectionThread 的定时机制去检查连接状态,保证连接池连接的健康。 


经过以上的情况,连接数量可能会小于 minIdle,则不能维持线程平衡。那么就需要发送信号 emptySignal 通知创建连接线程去创建新的连接。


LogStatsThread


和公司中的会计一样负责记账,主要负责实施记录 Druid的开销情况,包括当前连接池数量、activeCount、这期间关闭的连接数量等等。LogStatsThread 按配置的timeBetweenLogStatsMillis 的周期时间,定期执行。如果 timeBetweenLogStatsMillis 没有配置,则该线程将不会被开启。


应用场景举例


了解了druid 连接池原理以后,我们看实际生产环境上如何应用 druid 的特性。假设某个机房因为设备原因,到时mysql 之间会出现周期性断连,导致数据库连接失效。在这种场景下如何保证数据库连接健康,并且对强依赖数据库的系统不会造成致命故障呢?


这里可用使用几种方式:

1. 开启testBrrow,每次获取连接的时候判断连接是否可用。但这种方式会找出很大的开销。

2. 关闭testBrrow,开启testWhileIdle。那么每次获取连接时只会对空闲时间超过指定时间的连接进行探活。

3. 开启keepAlive 校验,利用DestroyConnectionThread 进行周期性校验。其校验间隔也是较短。


THE END

Everything you see exists together in a delicate balance.

  世界上所有的生命都在微妙的平衡中生存。





---《狮子王》

以上是关于Druid系列--连接池原理的主要内容,如果未能解决你的问题,请参考以下文章

druid的关键参数+数据库连接池运行原理

Druid连接池

Spring Boot [使用 Druid 数据库连接池]

druid数据库连接池配置

day66(YAML配置,使用Druid数据库连接池,编写持久层(数据访问层)代码,关于业务逻辑层(service层)

第13章—数据库连接池(Druid)