1、什么是keepAlive
先简单说一下keepAlive在服务架构上的工作模式
1、高可用的解决方案通常为:冗余+故障自动发现转移,当一台节点负责接收请求时,发生故障那么整个应用就停止了,这时候会启动多台服务器有一台作为master提供服务,其余的为backup,master会定期向backup发送心跳,证明master还活着。
2、那么每台服务器的ip肯定是不同的,这时候会有一个虚拟ip在master上,client发起的请求都是请求的这个虚拟ip,master宕机后,重新发起ARP广播,刷新mac地址,并且把虚拟ip给新选举成功的服务器上,这样就做到了高可用。
keepAlive保活机制目的就来保障整个集群的高可用性
那么,keepAlive思想在Druid连接池中是如何运用的?
2、keepAlive在Druid连接池中的作用
存活机制能够保证连接池中的连接是真实有效的连接,假如遇到特殊情况导致连接不可用时,keepAlive机制将无效连接进行驱逐。
开启keepAlive
// 一个连接在连接池中最小生存的时间
dataSurce.setMinEvictableIdleTimeMillis(60 * 1000);单位毫秒
// 开启keepAlive
dataSource.setKeepAlive(true);
当连接池中连接的空闲时间大于最小驱逐空闲时间,并且开启了keepAlive,那么就将该连接放入保活连接数组,并且对保活连接数组中的连接进行有效性检查。
先看DruidDataSource中的两个成员变量
// 存放检查需要抛弃的连接
private DruidConnectionHolder[] evictConnections;
// 用来存放需要连接检查的存活连接
private DruidConnectionHolder[] keepAliveConnections;
createAndStartdestroyThread()创建的destroytask线程
destroy守护线程无限循环destroyTask线程
@Override
public void run() {
shrink(true, keepAlive);
if (isRemoveAbandoned()) {
removeAbandoned();
}
}
关键方法shrink(true,keepAlive)
public void shrink(boolean checkTime, boolean keepAlive) {
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
return;
}
// 是否需要填充,开启keepAlive并且池中可用连接数+正在使用连接 < minIdle时,needFile=true
boolean needFill = false;
// 驱逐连接数量
int evictCount = 0;
// 存活连接数量
int keepAliveCount = 0;
int fatalErrorIncrement = fatalErrorCount - fatalErrorCountLastShrink;
fatalErrorCountLastShrink = fatalErrorCount;
try {
// 未init直接return
if (!inited) {
return;
}
// 池中可用连接数 - 最小连接数 = 待检查的连接数量
// 检测连接存活有效性,也是从这批连接中进行检查
final int checkCount = poolingCount - minIdle;
final long currentTimeMillis = System.currentTimeMillis();
for (int i = 0; i < poolingCount; ++i) {
// connections:可用连接的数组,从该数组中取出连接
DruidConnectionHolder connection = connections[i];
/*
* lastFatalErrorTimeMillis:最后发生错误的系统时间
* onFatalError:是否发生致命错误
*
* 如果发生了致命错误 并且 发生错误的时间 > 当前connection的连接时间,说明是在连接之后发生的错误
* 那么将这个连接放入keepAliveConnections数组中,这个数组用来保存待检查的存活连接,并退出此次循环
*/
if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis)) {
// keepAliveConnections: 保存待检查的存活连接
keepAliveConnections[keepAliveCount++] = connection;
continue;
}
if (checkTime) {
// pyhyTimeoutMillis:物理超时时间,默认为-1,可以通过setPhyTimeoutMillis(time);进行配置
if (phyTimeoutMillis > 0) {
// 当前系统时间 - 当前连接的连接时间 = 当前连接已经存活的时长
long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
// 当前连接存活时长 > 配置的物理连接超时时长,那么将此连接放入驱逐数组,并且退出此次循环
if (phyConnectTimeMillis > phyTimeoutMillis) {
evictConnections[evictCount++] = connection;
continue;
}
}
// connection.lastActiveTimeMillis:当前连接上一次使用的时间
// 当前时间 - 上一次使用时间 = 当前连接的空闲时间
long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
// minEvictableIdleTimeMillis:连接保持空闲而不被驱逐的最长时间,默认30分钟
// keepAliveBetweenTimeMillis:保活检查时间间隔,默认2分钟
// 空闲时间 < 最下保持空闲时长 并且 小于保活检查时间间隔,则不进行该连接的保活检查
if (idleMillis < minEvictableIdleTimeMillis
&& idleMillis < keepAliveBetweenTimeMillis
) {
break;
}
// 空闲时间 >= 最小驱逐空闲时间
if (idleMillis >= minEvictableIdleTimeMillis) {
// final int checkCount = poolingCount - minIdle;
// 当前连接的索引 < 空闲连接数量
if (checkTime && i < checkCount) {
// 说明当前连接空闲时间超时了,要加入到驱逐数组中
evictConnections[evictCount++] = connection;
continue;
} else if (idleMillis > maxEvictableIdleTimeMillis) {
evictConnections[evictCount++] = connection;
continue;
}
}
// keepAliveBetweenTimeMillis:保活检查时间间隔
// keepAliveConnections:待检查的存活连接
// 开启了保活机制 并且 空闲时长 >= 保活检查时间间隔,那么将这个连接放入待检查存活连接数组
if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) {
keepAliveConnections[keepAliveCount++] = connection;
}
} else {
// final int checkCount = poolingCount - minIdle;
// 只保留最小连接数 其余的都放入驱逐数组
if (i < checkCount) {
evictConnections[evictCount++] = connection;
} else {
break;
}
}
}
// 待驱逐的连接数量 + 待保活校验的连接数量
int removeCount = evictCount + keepAliveCount;
if (removeCount > 0) {
// 将connections数组中的removeCount个连接删掉
// {obj,obj,obj,obj,obj} -> {obj,obj,obj,null,null}
System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
poolingCount -= removeCount;
}
// 保活连接校验数量
keepAliveCheckCount += keepAliveCount;
// 开启了保活 并且 可用连接池中连接 < 配置的最小连接
if (keepAlive && poolingCount + activeCount < minIdle) {
// 需要向连接池中添加连接标记
needFill = true;
}
} finally {
lock.unlock();
}
// 驱逐连接
if (evictCount > 0) {
for (int i = 0; i < evictCount; ++i) {
DruidConnectionHolder item = evictConnections[i];
Connection connection = item.getConnection();
JdbcUtils.close(connection);
destroyCountUpdater.incrementAndGet(this);
}
Arrays.fill(evictConnections, null);
}
// 保活校验
if (keepAliveCount > 0) {
// 从后往前遍历校验
// keep order
for (int i = keepAliveCount - 1; i >= 0; --i) {
DruidConnectionHolder holer = keepAliveConnections[i];
Connection connection = holer.getConnection();
holer.incrementKeepAliveCheckCount();
boolean validate = false;
try {
// 做一些sql检测,判断当前连接是否有效
this.validateConnection(connection);
// validate = true:连接有效,如果连接无效则直接抛异常 并 记录日志
validate = true;
} catch (Throwable error) {
if (LOG.isDebugEnabled()) {
LOG.debug("keepAliveErr", error);
}
// skip
}
boolean discard = !validate;
if (validate) {
holer.lastKeepTimeMillis = System.currentTimeMillis();
// 若连接有效,则再次将此连接放入connections可用连接池数组中
boolean putOk = put(holer, 0L, true);
if (!putOk) {
discard = true;
}
}
// 若连接无效则丢弃
if (discard) {
try {
connection.close();
} catch (Exception e) {
// skip
}
lock.lock();
try {
discardCount++;
// 若 正在使用的连接数量 + 可用连接 <= 最小连接数,则释放empty信号
if (activeCount + poolingCount <= minIdle) {
emptySignal();
}
} finally {
lock.unlock();
}
}
}
this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount);
Arrays.fill(keepAliveConnections, null);
}
if (needFill) {
lock.lock();
try {
/*
poolingCount:池中可用连接数
activeCount:正在使用的连接数
createTaskCount:正在生成的连接数
填充连接数以满足minIdle最小连接数
*/
int fillCount = minIdle - (activeCount + poolingCount + createTaskCount);
for (int i = 0; i < fillCount; ++i) {
emptySignal();
}
} finally {
lock.unlock();
}
} else if (onFatalError || fatalErrorIncrement > 0) {
lock.lock();
try {
emptySignal();
} finally {
lock.unlock();
}
}
}
特别关注keepAliveConnections和evictConnections两个数组
keepAliveConnections
1、如何发生致命错误(onFatalError)并且发生错误时间是在当前连接创建之后,那么将此连接放入keepAliveConnections数组
if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis)) { // keepAliveConnections: 保存待检查的存活连接 keepAliveConnections[keepAliveCount++] = connection; continue; }
2、开启了keepAlive 并且 连接的空闲时间 >= 保活检查时间间隔,那么将此连接放入keepAliveConnections数组
if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) { keepAliveConnections[keepAliveCount++] = connection; }
evictConnections
1、当前连接存活时长 > 配置的物理连接时间时长,则放入evictConnections
if (phyConnectTimeMillis > phyTimeoutMillis) { evictConnections[evictCount++] = connection; continue; }
2、空闲时间 > 最小驱逐时间
// 当前连接的索引 < 空闲连接数量 if (checkTime && i < checkCount) { // 说明当前连接空闲时间超时了,要加入到驱逐数组中 evictConnections[evictCount++] = connection; continue; } else if (idleMillis > maxEvictableIdleTimeMillis) { evictConnections[evictCount++] = connection; continue; }
总结
keepAlive的作用就是让池中的连接保持一定数量 并且都是有效的,在高并发场景中尽量不去重复创建connection
在高并发场景中 启动datasource时,要对连接池进行预热 ,等请求来时减少创建连接的等待
对于访问量小的服务来说,keepAlive反而会增加数据库服务器的压力 因为要维护多个连接进程,所以这个参数因项目实际情况来选择配置