版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011039332/article/details/85381051
前言 
这个是接着 上次的 这篇文章 37 一次获取redis连接阻塞引起的 "Thread pool is EXHAUSTED 的疑问之一 "那么 redis 连接池 中的空闲链接 为什么会被占用满了呢?"

然后 另外一点疑问, "到底是谁中断了 dubbo 处理请求的线程的呢", 因为不好复现, 还是没有想明白

问题的发现
原因 确实是因为连接泄露了, 在我的 redis 操作工具类里面, 有下面这样的一个 工具方法, 是用于给常用的 set, get 方法提供支撑

然后 之前我检查的时候, 我发现 "doWithCallback" 方法只有 当前 RedisUtils 里面使用, 也只有这里拿了连接的

然后 我忽略了一点, "getResource" 方法本身是被外部引用了的, 外面的其他 有一个地方需要使用 Jedis 连接, 然后 就是这里的代码 获取了连接, 没有释放, 然后 导致了连接被泄露

public <T> T doWithCallback(IRedisClientCallback<T> callback) {
T result = null;
long start = System.currentTimeMillis();
try (ShardedJedis jedis = getResource()) {
if (jedis != null) {
result = callback.run(jedis);
}
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);

// todo for debug, remove this after stable
long spent = System.currentTimeMillis() - start;
JSONObject stat = new JSONObject();
stat.put("active", shardedJedisPool.getNumActive());
stat.put("idle", shardedJedisPool.getNumIdle());
stat.put("waiters", shardedJedisPool.getNumWaiters());
stat.put("maxBorrowWaitTimeMillis", shardedJedisPool.getMaxBorrowWaitTimeMillis());
stat.put("meanBorrowWaitTimeMillis", shardedJedisPool.getMeanBorrowWaitTimeMillis());
LOGGER.error(" [forDebug] [doWithCallback] stats : " + JSON.toJSONString(stat) + ", spent : " + spent + " ms ...");
}
return result;
}

此问题的几个阶段如下

1. 此问题 最开始的版本是 "getMaxWaitMillis" 为之前系统默认的 -1, 然后 之后出现了这个问题, 发现所有的线程 都阻塞在这里了, 然后 影响到了业务

2. 然后 之后看了一下 "GenericObjectPool. borrowObject" 的代码, 然后 发现可以配置 maxWiatMillis, 然后 为了不影响业务 配置了一个 maxWiatMillis, 然后 并加了一下 上面的这些调试代码

3. 然后 过了一段时间, 妈的 此问题又出现了, 然后 之后的时候, 继续看了一下 "GenericObjectPool. borrowObject" 的代码, 然后 发现 这样一段代码, 如果 资源吃紧的话 主动释放一部分的一段时间没有使用了的连接

AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
(getNumIdle() < 2) &&
(getNumActive() > getMaxTotal() - 3) ) {
removeAbandoned(ac);
}
然后 但是 "redis.clients.jedis.ShardedJedis" [jedis-2.9.0] 里面是没有提供 这个 AbandonedConfig 的配置的入口的, 然后 只好自己 copy 下来 改改, 增加了一个 配置 AbandonedConfig 的入口, 用起来 
4. 然后 后来想想, 这么处理 也不是办法啊, 然后 使劲找代码里面的问题, 然后 终于找到了 redis 连接泄露的原因, 模拟了一下情况, 写了一点简单的测试 代码

@Test
public void test03ResourceLeak() {
for (int i = 0; i < 100; i++) {
consumeRedisResourceWithLeak();
// consumeRedisResource();
}

ShardedJedis shardedJedis = (ShardedJedis) redisBasedCacheService.getResource();
System.out.println(" end ... ");
}

// case of connection leak
private void consumeRedisResourceWithLeak() {
ShardedJedis shardedJedis = (ShardedJedis) redisBasedCacheService.getResource();
}

// case of connection created and closed normally
private void consumeRedisResource() {
try (ShardedJedis shardedJedis = (ShardedJedis) redisBasedCacheService.getResource()) {
}
}

ShardedJedisPool
继承自 Pool, 里面 主要是 重写了一个自定义的 ShardedJedisFactory 来创建 Jedis 连接 以及一个连接生命周期的业务处理

其业务方法 主要是继承自 Pool, Pool 内部委托给 GenericObjectPool

GenericObjectPool

1. borrowObject(long borrowMaxWaitMillis)

主要供能为 从连接池里面获取一个连接

1. 如果配置了 AbandonedConfig, 并且当前连接池资源极度紧缺了, 移除 长期没有使用的链接[距离现在已经 removeAbandonedTimeout 秒没有使用的连接]

2. blockWhenExhausted 配置为 获取不到连接是否阻塞线程等待

borrowMaxWaitMillis 配置为获取连接 可以等待的毫秒数, 如果小于0 为阻塞直到获取到连接 [可能被中断]

如果不阻塞, 获取到连接直接返回, 获取不到 抛出异常

如果阻塞, 如果 borrowMaxWaitMillis 小于0, 阻塞直到 获取到连接,

否则 最大等待 borrowMaxWaitMillis 毫秒, 超时抛出异常

3. 获取到连接之后, 进行一些生命周期的操作, activateObject, validateObject 等等, 如果异常 有相应的补偿处理

public T borrowObject(long borrowMaxWaitMillis) throws Exception {
assertOpen();

AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
(getNumIdle() < 2) &&
(getNumActive() > getMaxTotal() - 3) ) {
removeAbandoned(ac);
}

PooledObject<T> p = null;

// Get local copy of current config so it is consistent for entire
// method execution
boolean blockWhenExhausted = getBlockWhenExhausted();

boolean create;
long waitTime = 0;

while (p == null) {
create = false;
if (blockWhenExhausted) {
p = idleObjects.pollFirst();
if (p == null) {
create = true;
p = create();
}
if (p == null) {
if (borrowMaxWaitMillis < 0) {
p = idleObjects.takeFirst();
} else {
waitTime = System.currentTimeMillis();
p = idleObjects.pollFirst(borrowMaxWaitMillis,
TimeUnit.MILLISECONDS);
waitTime = System.currentTimeMillis() - waitTime;
}
}
if (p == null) {
throw new NoSuchElementException(
"Timeout waiting for idle object");
}
if (!p.allocate()) {
p = null;
}
} else {
p = idleObjects.pollFirst();
if (p == null) {
create = true;
p = create();
}
if (p == null) {
throw new NoSuchElementException("Pool exhausted");
}
if (!p.allocate()) {
p = null;
}
}

if (p != null) {
try {
factory.activateObject(p);
} catch (Exception e) {
try {
destroy(p);
} catch (Exception e1) {
// Ignore - activation failure is more important
}
// 省略部分代码
}
if (p != null && getTestOnBorrow()) {
boolean validate = false;
Throwable validationThrowable = null;
try {
validate = factory.validateObject(p);
} catch (Throwable t) {
PoolUtils.checkRethrow(t);
validationThrowable = t;
}
// 省略部分代码
}
}
}

updateStatsBorrow(p, waitTime);

return p.getObject();
}

2. returnObject(T obj)
回收给定的链接, 或者清理掉给定的链接

1. 首先校验 需要回收的链接是否正常

2. 执行生命周期相关操作 validateObject, passivateObject, deallocate 等等

3. 如果连接池正常运行, 并且 空闲的链接数量 大于 maxIdle, 取消注册 连接 并关闭这个 连接

否则, 根据 last in first out 的策略, 将连接 回收到 idleeObjects 队列

public void returnObject(T obj) {
PooledObject<T> p =www.michenggw.com allObjects.get(obj);

if (!isAbandonedConfig(www.meiwanyule.cn)) {
if (p == null) {
throw new IllegalStateException(
"Returned object not www.dasheng178.com currently part of this pool");
}
} else {
if (p == null) {
return; // Object was abandoned and removed
} else {
// Make sure object is not being reclaimed
synchronized(p) {
final PooledObjectState state = p.getState();
if (state == PooledObjectState.ABANDONED ||
state == PooledObjectState.INVALID) {
return;
} else {
p.markReturning(); // Keep from being marked abandoned
}
}
}
}

long activeTime = p.getActiveTimeMillis();

if (getTestOnReturn()) {
if (!factory.validateObject(p)) {
try {
destroy(p);
} catch (Exception e) {
swallowException(e);
}
updateStatsReturn(activeTime);
return;
}
}

try {
factory.passivateObject(p);
} catch (Exception e1) {
swallowException(e1);
try {
destroy(p);
} catch (Exception e) {
swallowException(e);
}
updateStatsReturn(activeTime);
return;
}

if (!p.deallocate()) {
throw new IllegalStateException(
"Object has already been retured to this pool or is invalid");
}

int maxIdleSave = getMaxIdle();
if (isClosed() |www.gcyL157.com | maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
try {
destroy(p);
} catch (Exception e) {
swallowException(e);
}
} else {
if (getLifo()) {
idleObjects.addFirst(p);
} else {
idleObjects.addLast(p);
}
}
updateStatsReturn(activeTime);
}

3. evict()

在 GenericObjectPool 的通用构造方法 GenericObjectPool(PooledObjectFactory<T> factory, GenericObjectPoolConfig config)

里面, 启动了一个 timer, 来定时清理, 校验 idleObjects 队列里面的连接, 并且需要 确保连接池 最少保持minIdle 个连接

numTestsPerEvictionRun 大于等于0表示, 最多清理, 校验 numTestsPerEvictionRun  个连接, 小于0 表示 最多清理校验 (idleObjeects.size / (-numTestsPerEvictionRun) ) 个连接

每次定时任务 处理[清理, 校验] numTestsPerEvictionRun 个连接

1. 初始化迭代器, 如果 迭代器为空 或者迭代到了最后, 重新初始化迭代器

2. 获取下一个待处理的链接, 如果状态不对 说明被其他的线程取走了, continue

3. 先根据策略 判断当前连接是否应该被 evict, 如果应该 那就清理当前连接

否则 对当前连接进行校验, 处理生命周期相关操作 activateObject, validateObject, passivateObject

4. 最后, 如果 配置了 AbandonedConfig, 并且有 removeAbandonedOnMaintenance 配置, 移除 长期没有使用的链接[距离现在已经 removeAbandonedTimeout 秒没有使用的连接]

public void evict() throws Exception {
assertOpen();

if (idleObjects.size() > 0) {
PooledObject<T> underTest = null;
EvictionPolicy<T> evictionPolicy = getEvictionPolicy();

synchronized (evictionLock) {
EvictionConfig evictionConfig = new EvictionConfig(
getMinEvictableIdleTimeMillis(),
getSoftMinEvictableIdleTimeMillis(),
getMinIdle());

boolean testWhileIdle = getTestWhileIdle();

for (int i = 0, m =www.mhylpt.com getNumTests(); i < m; i++) {
if (evictionIterator == null || !evictionIterator.hasNext()) {
if (getLifo()) {
evictionIterator = idleObjects.descendingIterator();
} else {
evictionIterator = idleObjects.iterator();
}
}
if (!evictionIterator.hasNext()) {
// Pool exhausted, nothing to do here
return;
}

try {
underTest = evictionIterator.next();
} catch (NoSuchElementException nsee) {
// Object was borrowed in another thread
// Don't count this as an eviction test so reduce i;
i--;
evictionIterator = null;
continue;
}
if (!underTest.startEvictionTest()) {
// Object was borrowed in another thread
// Don't count this as an eviction test so reduce i;
i--;
continue;
}

if (evictionPolicy.evict(evictionConfig, underTest,
idleObjects.size())) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
} else {
if (testWhileIdle) {
boolean active = false;
try {
factory.activateObject(underTest);
active = true;
} catch (Exception e) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
}
if (active) {
if (!factory.validateObject(underTest)) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
} else {
try {
factory.passivateObject(underTest);
} catch (Exception e) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
}
}
}
}
if (!underTest.endEvictionTest(idleObjects)) {
// TODO - May need to add code here once additional
// states are used
}
}
}
}
}

AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getRemoveAbandonedOnMaintenance()) {
removeAbandoned(ac);
}
}
void ensureMinIdle() throws Exception {
int minIdleSave = getMinIdle();
if (minIdleSave < 1) {
return;
}

while (idleObjects.size() < minIdleSave) {
PooledObject<T> p = create();
if (p == null) {
// Can't create objects, no reason to think another call to
// create will work. Give up.
break;
}
if (getLifo()) {
idleObjects.addFirst(p);
} else {
idleObjects.addLast(p);
}
}
}

4. 默认的 evict 策略

还是比较好理解, 两个维度, idleEvictTime, idleSoftEvictTime 的判断处理

public boolean evict(EvictionConfig config, PooledObject<T> underTest,
int idleCount) {

if ((config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis() &&
config.getMinIdle() < idleCount) ||
config.getIdleEvictTime() < underTest.getIdleTimeMillis()) {
return true;
}
return false;
}

5. getMinIdle

不仅仅是简单的 return, 还有一些逻辑

public int getMinIdle() {
int maxIdleSave = getMaxIdle();
if (this.minIdle > maxIdleSave) {
return maxIdleSave;
} else {
return minIdle;
}
}

38 一次 redis 连接泄露的原因 以及 ShardedJedisPool的更多相关文章

  1. redis连接超时问题排查

    连接池无法获取到连接或获取连接超时redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource f ...

  2. go的mgo,连接未释放问题,连接泄露。

    api启动几天后,卡住(连接失败,超时) 异常原因 mongo连接被占满,无法建立mgo连接,返回信息 查询点用端口可知,97%的连接被api项目占用. api项目的mongodb连接“泄露”,某处的 ...

  3. Java内存泄露的原因

    Java内存泄露的原因 1.静态集合类像HashMap.Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector ...

  4. 牛客网Java刷题知识点之内存溢出和内存泄漏的概念、区别、内存泄露产生原因、内存溢出产生原因、内存泄露解决方案、内存溢出解决方案

    不多说,直接上干货! 福利 => 每天都推送 欢迎大家,关注微信扫码并加入我的4个微信公众号:   大数据躺过的坑      Java从入门到架构师      人工智能躺过的坑          ...

  5. java原生程序redis连接(连接池/长连接和短连接)选择问题

    最近遇到的连接问题我准备从重构的几个程序(redis和mysql)长连接和短连接,以及连接池和单连接等问题用几篇博客来总结下. 这个问题的具体发生在java原生程序和redis的交互中.这个问题对我最 ...

  6. 【Redis连接超时】记录线上RedisConnectionFailureException异常排查过程

    项目架构: 部分组件如下: SpringCloudAlibaba(Nacos+Gateway+OpenFeign)+SpringBoot2.x+Redis 问题背景: 最近由于用户量增大,在高峰时期, ...

  7. Redis 连接问题

    .NET 中使用 StackExchange.Redis 我为什么想写这个,总感觉很多介绍相应技术的博客,只是把内容从官网搬到自己的博客中,没有任何的实践,这样会给想学的人,没有任何好处,也可能我是自 ...

  8. Redis 连接池的问题

      目录 Redis 连接池的问题    1 1.    前言    1 2.解决方法    1     前言 问题描述:Redis跑了一段时间之后,出现了以下异常. Redis Timeout ex ...

  9. PHP- 深入PHP、Redis连接

    pconnect, phpredis中用于client连接server的api. The connection will not be closed on close or end of reques ...

随机推荐

  1. oradebug 的学习 一

        说明 oradebug主要是给oracle支持人员使用的,尽管很早便有,但oracle官网很少有记载.他是个sql*plus命令行工具,有sysdba的权限就可以登入,无需特别设置.他可以被用 ...

  2. 获取附加在方法上的Attribute

    如下: class Program { static void Main(string[] args) { var methodInfo = typeof(Program).GetMethod(&qu ...

  3. Linux系统中ElasticSearch搜索引擎安装配置Head插件

    近几篇ElasticSearch系列: 1.阿里云服务器Linux系统安装配置ElasticSearch搜索引擎 2.Linux系统中ElasticSearch搜索引擎安装配置Head插件 3.Ela ...

  4. 微信小程序—day04

    元素水平+垂直居中 昨天的用户页的用户头像,是根据已知的像素大小,设置固定的值,达到居中的效果. 今日切换机型进行适配,发现对不同尺寸大小的屏幕不匹配.所以对wxss进行修改,真正达到水平+垂直居中. ...

  5. GameplayKit的GKStateMachine用法与实例

    GKStateMachine 玩家进入GameScene场景中 -> 通过GKStateMachine进入到指定的游戏状态GKState 在GameScene场景中 -> 根据不同的逻辑调 ...

  6. 树莓派怎么连接无线网wifi?

    没有显示器的同学,想要连接无线网,一定非常苦恼,前面教会了大家远程登录图形界面,下面我将教会大家:在没有图形界面的情况下,怎么连接树莓派WiFi.同样还是利用putty远程访问软件登录,但这次不需要登 ...

  7. Vue 兄弟组件通信(不使用Vuex)

    Vue 兄弟组件通信(不使用Vuex) 项目中,我们经常会遇到兄弟组件通信的情况.在大型项目中我们可以通过引入vuex轻松管理各组件之间通信问题,但在一些小型的项目中,我们就没有必要去引入vuex.下 ...

  8. Vuejs 实现简易 todoList 功能 与 组件

    todoList 结合之前 Vuejs 基础与语法 使用 v-model 双向绑定 input 输入内容与数据 data 使用 @click 和 methods 关联事件 使用 v-for 进行数据循 ...

  9. 出现java.lang.Exception: java.lang.RuntimeException: java.lang.NoSuchMethodException: com.web.visit.main.ClickVist$VisitMapper.<init>()的问题

    执行mapreduce报错java.lang.Exception: java.lang.RuntimeException: java.lang.NoSuchMethodException: com.w ...

  10. 1053 Path of Equal Weight (30 分)(树的遍历)

    题目大意:给出树的结构和权值,找从根结点到叶子结点的路径上的权值相加之和等于给定目标数的路径,并且从大到小输出路径 #include<bits/stdc++.h> using namesp ...