Druid系列--连接池原理
Posted 入境繁华
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Druid系列--连接池原理相关的知识,希望对你有一定的参考价值。
作者 | 畅为
与其他连接池不同,druid没有一个专门的连接池类,比如ConnnetPool。druid的连接池都交给了DruidDataSource负责管理,用数据connects存放连接。本文将介绍 Druid 是如何维护这个连接池,如何完成连接的获取和回收。
首先看druid 是通过怎样的数据结构存储连接:
//连接池
private DruidConnectionHolder[] connections = new DruidConnectionHolder[maxActive];
private int poolingCount = 0; // 当前连接池中数据库连接的数量
private int activeCount = 0; // 正在使用的连接数量
// 线程之间的通讯
protected Condition notEmpty;
protected Condition empty;
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 中几个永恒的准则。
连接池数量最大不能超过 maxActive.
最小连接数不能低于 minIdle.
用户通过DruidDataSource 去获取和回收连接。
获取连接时首先判断连接池是否有可用连接,如果有直接从conecets 尾部获取一个。否则发送empty 信号告诉 CreateConenctionThread 创建连接,并挂起等待,直到有新的连接。
CreateConenctionThread 创建连接完成后,发起 notEmpty 通知等待线程。等待线程恢复,并从connects 尾部获取连接。
DestroyConnectionThread 负责收缩连接池,如果连接数量小于 minIdle 则通知 CreateConnectionThread.
LogStatsThread 则负责记录日志。
获取连接
当用户需要连接时,调用 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 通知,通知正在等待连接的线程。
如名字所示,这个主要是一个负责创建连接。不是时刻都在创建连接,当没有可用连接时,用户线程进入等待挂起状态,并且连接数量小于maxActive 时才创建连接,否则就偷懒睡觉。在他的世界里准守以下两个规则:
必须是存在线程等待,才会去创建连接。否则挂起线程睡眠等待。
如果当前连接数量小于 maxActive 才去创建,否则挂起睡眠等待。
如果以上两个规则满足了,则会去创建连接。创建连接的底层使用 JDBC 这一套逻辑, 利用Driver 和配置信息连接到数据库:
conn = getDriver().connect(url, info);
创建完的连接直接存在 connections 末尾,并发一个notEmpty信号告诉所有在等待的线程。等待线程则从睡眠中恢复,再尝试去获取连接。
该线程主要工作是将空闲及无效的连接销毁。默认情况下每1000ms 执行一次,可以通过timeBetweenEvictionRunsMillis 时间设置执行间隔。 这里使用的是 最近最久未使用策略进行回收。创建连接和获取连接的时候都是从 connects 尾部获取,尾部连接相对活跃,而头部的连接可能长时间未被使用,相对而言活性更低。因此每次回收都是从connects 头部开始遍历,主要回收几类连接:
连接的空闲时间大于 maxEvictableIdleTimeMillis, 则进行回收。
大于minIdle 部分的连接会被回收。保证连接池空闲连接不会太多。如果连接池中有大于 minIdle 的连接,说明当前连接池不是很繁忙,因此可以清理。如果为了避免频繁销毁和关闭连接,建议设置 minIdle。
检查连接活跃度,不健康的连接则关闭。默认不检查,可以通过 druid.keepAlive 打开连接的健康检查,当开关打开以后,如果空闲时间超过 keepAliveBetweenTimeMills 则会判断连接的健康情况(默认 keepAliveBetweenTimeMills 是 2 分钟)。这种在网络不稳定的场景下可以使用,利用 DestroyConnectionThread 的定时机制去检查连接状态,保证连接池连接的健康。
经过以上的情况,连接数量可能会小于 minIdle,则不能维持线程平衡。那么就需要发送信号 emptySignal 通知创建连接线程去创建新的连接。
和公司中的会计一样负责记账,主要负责实施记录 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系列--连接池原理的主要内容,如果未能解决你的问题,请参考以下文章