版权声明:本文为博主原创文章,未经博主允许不得转载。 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. python的阶段复习

    1.ABCD乘于9 = DCBA,求ABCD的值,且ABCD均互不相等 #!/usr/bin/env python # -*- coding:utf-8 -*- # @Time :2017/12/26 ...

  2. Updating Homebrew... 长时间不动解决方法

    确保你已安装Homebrew 依次输入下面的命令(注意:不要管重置部分的命令,这里原作者贴出来.我也贴出来是以防需要重置的时候有参考操作命令) 替换brew.git: cd "$(brew ...

  3. Python对象引用问题总结

    对于对象引用问题,一直是一知半解的状态,现整理以备使用. 操作不可变对象进行加减运算时,会在内存中创建新的不可变实例,不会影响原来的引用>>> c=12>>> d= ...

  4. 2019年猪年海报PSD模板-第一部分

    14套精美猪年海报,免费猪年海报,下载地址:百度网盘,https://pan.baidu.com/s/1i7bIzPRTX0OMbHFWnqURWQ                        

  5. 初学Direct X(9) ——文字的显示

    初学Direct X(9) --文字的显示 本次学习如何使用ID3DXFont创建字体,使得我们可以在任何安装了Windows系统中TrueType字体来打印文字,不过最好使用标准字体,这样文字在每一 ...

  6. leetcode合并区间

    合并区间     给出一个区间的集合,请合并所有重叠的区间. 示例 1: 输入: [[1,3],[2,6],[8,10],[15,18]] 输出: [[1,6],[8,10],[15,18]] 解释: ...

  7. leetcode-位1的个数(位与运算)

    位1的个数 编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量). 示例 : 输入: 11 输出: 3 解释: 整数 11 的二进制表示为 00000 ...

  8. Java学习 · 初识 IO流

    IO流   1. 原理与概念 a)     流 i.           流动,流向 ii.           从一端移动到另一端 源头到目的地 iii.           抽象.动态概念,是一连 ...

  9. Maven编译Java项目

    Spring在线参考文档: http://spring.io/guides/gs/maven/ 下载安装 Downloadand unzip the source repository for thi ...

  10. Java进阶知识点:不要只会写synchronized - JDK十大并发编程组件总结

    一.背景 提到Java中的并发编程,首先想到的便是使用synchronized代码块,保证代码块在并发环境下有序执行,从而避免冲突.如果涉及多线程间通信,可以再在synchronized代码块中使用w ...