专栏名称: 石杉的架构笔记
专注原创、用心雕琢!十余年BAT一线大厂架构经验倾囊相授
目录
相关文章推荐
Wind万得  ·  小米SU7 Ultra曝光,多款新品同台发布 ·  11 小时前  
法询金融固收组  ·  财政部11号文 ·  3 天前  
深圳市中级人民法院  ·  盗用身份证,“贷”价惨重! ·  3 天前  
深圳市中级人民法院  ·  盗用身份证,“贷”价惨重! ·  3 天前  
金融早实习  ·  麦星投资2025年实习生招聘 ·  3 天前  
51好读  ›  专栏  ›  石杉的架构笔记

一次线上 Jedis(Redis 客户端)异常的排查、定位、分析、解决!

石杉的架构笔记  · 公众号  ·  · 2020-02-13 08:59

正文

公众号后台回复“ 面试 ”,获取精品学习资料

扫描下方海报 了解 专栏详情

本文来源:Java爱好者社区


本文导读:

  1. 应用异常监控

  2. Redis客户端异常分析

  3. Redis客户端问题引导分析

  4. 站在Redis客户端视角分析

  5. 站在Redis服务端视角分析

  6. 资源池生产配置合理性分析

  7. 本文总结


今天我们来聊聊线上环境遇到的一个问题以及分析过程。


1
应用异常监控


这不,项目中有一个Redis客户端的异常在疫情期间,出现在了你的面前,虽然该异常是偶发,有必要仔细分析下该异常出现的原由。

具体异常信息如下所示:

大家看截图展示的异常信息,是不是很想问,这个异常显示怎么这么「友好」?

没错,是通过一款非常好用的 实时异常监控工具:Sentry 来监控到的,这款工具在我们的项目中已经接入并使用了很长一段时间了,对异常的监控非常到位。

比如针对发生的异常,将具体访问的整个URL、客户端上报的信息、设备型号等信息作为TAGS收集上来,尽情的展示给你,让你尽快结合这些信息快速定位问题。

该服务部署在k8s容器环境下,在截图中TAGS中,也能够看到 server_name 代表的是Pod的 hostname ,这样便能快速知道是哪个Pod出现的问题,进入容器平台直接进入到Pod内部进一步详细分析。

强烈推荐大家项目中接入 Sentry ,因为它不但有很好用的异常治理平台,更为重要的是Sentry支持跨语言客户端,比如支持Java、Andriod、C++、Python、Go等大部分语言,现成的客户端易于接入和使用。

我想只要你的服务不卡死,如果出现问题,项目里输出的日志中总会有一些 ERROR 级别的日志出现的,那么此时就交给Sentry,它会及时向你发出告警(邮件...)通知你。


2
Redis客户端异常分析


本项目中使用的Jedis(Redis的Java客户端),提示异常信息 JedisConnectionException Unexpected end of stream ,在使用Redis过程中我还很少遇到这个问题,既然遇到了,这是不是缘分啊 :)

其实异常栈中已经给出了详细的调用过程,在哪里出现的问题,顺藤摸瓜根据这个堆栈去查找 线索

如何找到更为详细的堆栈?别担心,在上图中点击下 raw 会出现完整的异常堆栈的文本信息,也方便复制拷贝出来分析。

如下所示:

redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199)
at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40)
at redis.clients.jedis.Protocol.process(Protocol.java:151)
at redis.clients.jedis.Protocol.read(Protocol.java:215)
at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:340)
at redis.clients.jedis.Connection.getStatusCodeReply(Connection.java:239)
at redis.clients.jedis.BinaryJedis.auth(BinaryJedis.java:2139)
at redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:108)
at org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:888)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:432)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:361)
...

根据以上信息,发现是调用到 BinaryJedis.auth 验证Redis密码时出错的,而且有 GenericObjectPool.borrowObject 表示借用对象的方法,GenericObjectPool是Apache开源项目的线程池,在很多开源项目中都能看到它的身影。

说明是在伸手向资源池索要对象时,在资源池里没有拿到对象,那就只能创建一个,调用了 GenericObjectPool.create ,调用具体实现方法 JedisFactory.makeObject 创建Jedis对象时出错的。

哦?这么一看,简单一想猜测下,在创建新的对象时验证密码时,可能因网络不稳定,Redis-Server没有正常返回异常信息导致的。

3
Redis客户端问题引导分析


在上文中,我们在异常堆栈中发现使用了线程池,如果不使用资源池管理这些对象,会发生什么情况?

如下所示,每次使用Redis连接都会在客户端重新创建Jedis对象,创建Jedis对象后,连接Redis Server,这个过程会建立TCP连接(三次握手),完成操作后,断开TCP连接(四次挥手),当遇到并发量稍大的请求,就会吃不消了,消耗资源的同时,无法满足应用性能上的要求。

如果使用了线程池,如下图所示的样子:

按需在资源池中初始化一定数量的对象,当有客户端请求到达时,从资源池里获取对象,对象使用完成,再将对象丢回到资源池里,给其他客户端使用。

这就是所谓的 「池化技术」 ,相信在你的项目中一定会用到的,比如数据库连接池、应用服务器的线程池等等。

池化技术的优势就是能够复用池中的对象,比如上述图示中,避免了分配内存和创建堆中对象的开销;避免了因对象重复创建,进而能避免了TCP连接的建立和断开的资源开销;避免了释放内存和销毁堆中对象的开销,进而减少垃圾收集器的负担;避免内存抖动,不必重复初始化对象状态。

当然,我们也可以自己来实现,但是如果想写出比较完善的对象池的资源管理功能,也需要花费不少的精力,考虑的细节也是非常多的。

站在巨人的肩膀上,在前文中提到的Jedis内部是由 Apache Common Pool2 开源工具包来实现的,很多开源项目中应用也是很广泛的。

而Jedis客户端的很多参数都是来源于Apache Common Pool2的底层实现过程所需要的参数。

这也是Jedis或者说一些Redis客户端给用户使用简单的原因,但是简单的同时,我们也要根据不同场景去合理配置好连接池的参数,不合理的配置加上不合理的功能使用,可能会引起很多的问题。

在回归到前文的最开始的异常,这些异常跟什么有关系呢?

从图示中,我们能知道客户端使用了线程池,可能跟线程池有关系;创建对象时,auth 验证密码时出现了问题,而验证密码前已经发起了 connect 连接了,说明连接到了Redis Server,所以 Redis Server 也脱离不了干系的。

跟 Redis Client 有关系,我们就要进一步分析客户端的参数,连接池的参数是否合理。

跟 Redis Server 有关系,就要结合问题分析下服务端的参数,相关配置参数是否合理。

4
站在Redis客户端视角分析


既然讲到了Redis客户端,首先想到的是从客户端配置的参数入手。

直接从参数入手,不如我们可以先接着对异常栈的分析,从对象资源池入手去分析,看看这个对象池到底是怎样管理的?

1、资源池对象管理

资源池中创建对象的过程如上图所示。

Apache Common Pool2 既然是一个通用的资源池管理框架,内部会定义好资源池的接口和规范,具体创建对象实现交由具体框架来实现。

1)从资源池获取对象,会调用ObjectPool#borrowObject,如果没有空闲对象,则调用PooledObjectFactory#makeObject创建对象,JedisFactory是具体的实现类。

2)创建完对象放到资源池中,返回给客户端使用。

3)使用完对象会调用ObjectPool#returnObject,其内部会校验一些条件是否满足,验证通过,对象归还给资源池。

4)条件验证不通过,比如资源池已关闭、对象状态不正确(Jedis连接失效)、已超出最大空闲资源数,则会调用 PooledObjectFactory#destoryObject从资源池中销毁对象。

ObjectPool 和 KeyedObjectPool 是两个基础接口。从定义的接口名上也能做下区分,ObjectPool 接口资源池列表里存储都是对象,默认实现类GenericObjectPool,KeyedObjectPool 接口用键值对的方式维护对象,默认实现类是GenericKeyedObjectPool。在实现过程会有很多公共的功能实现,放在了BaseGenericObjectPool基础实现类当中。

SoftReferenceObjectPool 是一个比较特殊的实现,在这个对象池实现中,每个对象都会被包装到一个 SoftReference 中。SoftReference 软引用,能够在JVM GC过程中当内存不足时,允许垃圾回收机制在需要释放内存时回收对象池中的对象,避免内存泄露的问题

PooledObject 是池化对象的接口定义,池化的对象都会封装在这里。DefaultPooledObject 是PooledObject 接口缺省实现类,PooledSoftReference 使用 SoftReference 封装了对象,供SoftReferenceObjectPool 使用。

2、对象池参数详解

查看对象池的参数配置,一种方式是直接查找代码或者官网文档中的说明去查看,另外介绍一种更为直观的方式,因为 Common Pool2 工具资源池的管理都接入到 JMX 中,所以可以通过如 Jconsole 等工具去查看暴露的属性和操作。

第一种方式:

查找对应配置类:

在 GenericObjectPoolConfig 和 BaseObjectPoolConfig 配置类对外提供的 setter 方法便是配置参数,并且代码里都有详细的注释说明。

第二种方式:

前提是你的应用暴露了 JMX 的端口和IP,允许外部连接。

JVM 参数如下所示:

-Dcom.sun.management.jmxremote
-Djava.rmi.server.hostname=IP地址
-Dcom.sun.management.jmxremote.port=端口
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

以上使用的是 Jconsole 工具类,点击 MBean 在左侧找到 org.apache.commons.pool2#GenericObjectPool#pool2 点击属性可以看到该类的所有属性信息,其中除包括核心的配置属性之外,还包括一些资源池的统计属性。

核心配置属性:

这些都是 重点关注 的属性,也是对外提供的可配置参数。

1) minIdle 资源池确保最少空闲的连接数,默认值:0

2) maxIdle 资源池允许最大空闲的连接数,默认值:8

3) maxTotal 资源池中最大连接数,默认值:8

4) maxWaitMillis 当资源池连接用尽后,调用者的最大等待时间,单位是毫秒,默认值:-1,建议设置合理的值

5) testOnBorrow 向资源池借用连接时,是否做连接有效性检测,无效连接会被移除,默认值:false ,业务量很大时建议为false,因为会多一次ping的开销

6) testOnCreate 创建新的资源连接后,是否做连接有效性检测,无效连接会被移除,默认值:false ,业务量很大时建议为false,因为会多一次ping的开销

7) testOnReturn 向资源池归还连接时,是否做连接有效性检测,无效连接会被移除,默认值:false,业务量很大时建议为false,因为会多一次ping的开销

8) testWhileIdle 是否开启空闲资源监测,默认值:false

9) blockWhenExhausted 当资源池用尽后,调用者是否要等待。默认值:true,当为true时,maxWaitMillis参数才会生效,建议使用默认值

10) lifo 资源池里放池对象的方式, LIFO Last In First Out 后进先出,true(默认值),表示放在空闲队列最前面,false:放在空闲队列最后面

空闲资源监测配置属性

当需要对空闲资源进行监测时, testWhileIdle 参数开启后与下列几个参数组合完成监测任务。

1) timeBetweenEvictionRunsMillis 空闲资源的检测周期,单位为毫秒,默认值:-1,表示不检测,建议设置一个合理的值,周期性运行监测任务

2) minEvictableIdleTimeMillis 资源池中资源最小空闲时间,单位为毫秒,默认值:30分钟(1000 60L 30L),当达到该值后空闲资源将被移除,建议根据业务自身设定

3) numTestsPerEvictionRun 做空闲资源检测时,每次的采样数,默认值:3,可根据自身应用连接数进行微调,如果 设置为 -1 ,表示对所有连接做空闲监测

3、空闲资源监测源码剖析

在资源池初始化之后,有个空闲资源监测任务流程如下:

对应源代码:

创建资源池对象时,在构造函数中初始化配合和任务的。

this.internalPool




    
 = new GenericObjectPool(factory, poolConfig);
public GenericObjectPool(final PooledObjectFactoryfactory,
final GenericObjectPoolConfig config) {

super(config, ONAME_BASE, config.getJmxNamePrefix());

if (factory == null) {
jmxUnregister(); // tidy up
throw new IllegalArgumentException("factory may not be null");
}
this.factory = factory;
// 创建空闲资源链表
idleObjects = new LinkedBlockingDeque>(config.getFairness());
// 初始化配置
setConfig(config);

// 开启资源监测任务
startEvictor(getTimeBetweenEvictionRunsMillis());
}

final void startEvictor(final long delay) {
synchronized (evictionLock) {
// 当资源池关闭时会触发,取消evictor任务
if (null != evictor) {
EvictionTimer.cancel(evictor, evictorShutdownTimeoutMillis, TimeUnit.MILLISECONDS);
evictor = null;
evictionIterator = null;
}
if (delay > 0) {
// 启动evictor任务
evictor = new Evictor();
// 开启定时任务
EvictionTimer.schedule(evictor, delay, delay);
}
}
}

Eviector 是个TimerTask,通过启用的调度器,每间隔 timeBetweenEvictionRunsMillis 运行一次。

class Evictor extends TimerTask {
@Override
public void run() {
final ClassLoader savedClassLoader =
Thread.currentThread().getContextClassLoader();
try {
...

// Evict from the pool
evict();

// Ensure min idle num
ensureMinIdle();

} finally {
// Restore the previous CCL
Thread.currentThread().setContextClassLoader(savedClassLoader);
}
}
}

evict() 移除方法源码:

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

if (idleObjects.size() > 0) {

PooledObjectunderTest = null;
// 获取清除策略
final EvictionPolicyevictionPolicy = getEvictionPolicy();

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

final boolean testWhileIdle = getTestWhileIdle();

for (int i = 0, m = getNumTests(); i < m; i++) {
// ... 省略部分代码
// underTest 代表每一个资源
boolean evict;

evict = evictionPolicy.evict(evictionConfig, underTest,
idleObjects.size());
// evict为true,销毁对象
if (evict) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
} else {
// testWhileIdle为true校验资源有效性
if (testWhileIdle) {
boolean active = false;
try {
factory.activateObject(underTest);
active = true;
} catch (final Exception e) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
}
if (active) {
if (!factory.validateObject(underTest)) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
} else {
try {
factory.passivateObject(underTest);
} catch (final Exception e) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
}
}
}
}
//...
}
}
}
}
// ...
}

代码里的默认策略 evictionPolicy,由 org.apache.commons.pool2.impl.DefaultEvictionPolicy 提供默认实现。

// DefaultEvictionPolicy#evict()
@Override
public boolean evict(final EvictionConfig config, final PooledObjectunderTest,
final int idleCount) {

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

1) 当空闲资源列表大小超过 minIdle 最小空闲资源数时,并且资源配置的 idleSoftEvictTime 小于资源空闲时间,返回 true。

EvictionConfig 配置初始化时,idleSoftEvictTime 如果使用的默认值 -1 < 0,则赋予值为 Long.MAX_VALUE。

2) 当检测的资源空闲时间过期后,即大于资源池配置的最小空闲时间,返回true。表示这些资源处于空闲状态,该时间段内一直未被使用到。

以上两个满足其中任一条件,则会销毁资源对象。

ensureIdle() 方法源代码:

private void ensureIdle(final int idleCount, final boolean always) throws Exception {
if (idleCount < 1 || isClosed() || (!always && !idleObjects.hasTakeWaiters())) {
return;
}
// 资源池里保留idleCount(minIdle)最小资源数量
while (idleObjects.size() < idleCount) {
final PooledObjectp = 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);
}
}
if (isClosed()) {
// Pool closed while object was being added to idle objects.
// Make sure the returned object is destroyed rather than left
// in the idle object pool (which would effectively be a leak)
clear();
}
}

以上就是对线程池的基本原理和参数的分析。

4、线程池对象状态

线程池对象的状态定义在 PooledObjectState ,是个枚举类型,有以下值:

IDLE 处于空闲状态

ALLOCATED 被使用中

EVICTION 正在被Evictor驱逐器验证

VALIDATION 正在验证

INVALID 驱逐测试或验证失败并将被销毁

ABANDONED 被抛弃状态,对象取出后,很久未归还

RETURNING 归还到对象池中

一张图来了解下线程池状态机转换:

5、对象池初始化时机

思考个问题,资源池里对象什么时候初始化进去的?这里的资源池就是指上文图中的 idleObjects 空闲资源对象缓存列表。是在创建对象时还是归还对象时?

答案是 归还对象的时候

某些场景,启动后可能会出现超时现象,因为每次请求都会创建新的资源,这个过程会有一定的开销。

应用启动后我们可以提前做下线程池资源的预热,示例代码如下:

ListminIdleList = new ArrayList(jedisPoolConfig.getMinIdle());

for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = pool.getResource();
minIdleList.add(jedis);
jedis.ping();
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
}
}

for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = minIdleList.get(i);
jedis.close();
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
}
}

如果不了解原理,可能以为上面的预热代码不大对吧,怎么获取后又调用了 jedis.close() 呢?字面上理解是把资源关闭了嘛。

一起看下线程池资源归还对象的源码就明白了。

GenericObjectPool#returnObject() 归还对象方法源码:

// GenericObjectPool#returnObject() 归还方法
public void returnObject(final T obj) {
// allObjects是存储所有对象资源的地方
final PooledObjectp = allObjects.get(new IdentityWrapper(obj));
// ...
// 变更对象状态
synchronized(p) {
final PooledObjectState state = p.getState();
if (state != PooledObjectState.ALLOCATED) {
throw new IllegalStateException(
"Object has already been returned to this pool or is invalid");
}
p.markReturning(); // Keep from being marked abandoned
}

final long activeTime = p.getActiveTimeMillis();
// testOnReturn为true,返还时验证资源有效性
if (getTestOnReturn()) {
if (!factory.validateObject(p)) {
try {
destroy(p);
} catch (final Exception e) {
swallowException(e);
}
try {
ensureIdle(1, false);
} catch (final Exception e) {
swallowException(e);
}
updateStatsReturn(activeTime);
return ;
}
}
// ...

if (!p.deallocate()) {
throw new IllegalStateException(
"Object has already been returned to this pool or is invalid");
}
// 获取maxIdle,限制空闲资源保留的上限数量
final int maxIdleSave = getMaxIdle();
if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
try {
destroy(p);
} catch (final Exception e) {
swallowException(e);
}
} else {
// 重点在这里,如果没有超过maxIdle,则会将归还的对象添加到 idleObjects 中
if (getLifo()) {
idleObjects.addFirst(p);
} else {
idleObjects.addLast(p);
}
if (isClosed()) {
// Pool closed while object was being added to idle objects.
// Make sure the returned object is destroyed rather than left
// in the idle object pool (which would effectively be a leak)
clear();
}
}
updateStatsReturn(activeTime);
}

归还对象时,首先会变更对象状态从 ALLOCATED 到 RETURNING,如果 testOnReturn参数 为true,校验资源有效性(Jedis连接的有效性),如果无效,则调用 destroy() 方法销毁对象,当 maxIdle 未超过 idleObjects 资源列表大小时,则会将归还的对象添加到 idleObjects 中。

而在 borrorObject() 的借出对象方法中就是从 idleObjects#pollFirst() 获取对象的,没有的话就会去创建,对象最多不能超过 maxTotal 数量。

6、Jedis客户端线程池参数

我们了解完 Apache Common Pool2 框架的线程池原理之后,接下来看看 Jedis 里是如何包装的。

线程池里的参数都是基于 JedisPoolConfig 来构建的。

JedisPoolConfig Jedis资源池配置类默认构造函数:

public class JedisPoolConfig extends GenericObjectPoolConfig {
public JedisPoolConfig() {
// defaults to make your life with connection pool easier :)
setTestWhileIdle(true);
setMinEvictableIdleTimeMillis(60000);
setTimeBetweenEvictionRunsMillis(30000);
setNumTestsPerEvictionRun(-1);
}
}

JedisPoolConfig 继承了 GenericObjectPoolConfig,JedisPoolConfig 默认构造函数中会将 testWhileIdle 参数设置为true(默认为false),minEvictableIdleTimeMillis设置为60秒(默认为30分钟),timeBetweenEvictionRunsMillis设置为30秒(默认为-1),numTestsPerEvictionRun设置为-1(默认为3)。

每个30秒执行一次空闲资源监测,发现空闲资源超过60秒未被使用,从资源池中移除。

创建 JedisPoolConfig 对象后,设置一些参数:

 // 创建 JedisPoolConfig 对象,设置参数
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig()
jedisPoolConfig.setMaxTotal(100);
jedisPoolConfig.setMaxIdle(60);
jedisPoolConfig.setMaxWaitMillis(1000 );
jedisPoolConfig.setTestOnBorrow(false);
jedisPoolConfig.setTestOnReturn(true);

JedisPool 管理了Jedis 的线程池:

// JedisPool 构造函数
public JedisPool(final GenericObjectPoolConfig poolConfig, final String host, int port,
int timeout, final String password) {
this(poolConfig, host, port, timeout, password, Protocol.DEFAULT_DATABASE, null);
}

public abstract class Pool<T> implements Closeable {
protected GenericObjectPoolinternalPool;

// 抽象 Pool 构造函数
public Pool(final GenericObjectPoolConfig poolConfig, PooledObjectFactoryfactory) {
initPool(poolConfig, factory);
}
}

5
站在Redis服务端视角分析


既然猜测可能跟 Redis 服务端有关系,就需要从跟客户端的参数配置去分析下,是否会有所影响。

1、Redis客户端缓冲区满了

Redis有三种客户端缓冲区:

普通客户端缓冲区(normal):

用于接受普通的命令,例如get、set、mset、hgetall等

slave客户端缓冲区(slave):

用于同步master节点的写命令,完成复制。

发布订阅缓冲区(pubsub):

pubsub不是普通的命令,因此有单独的缓冲区。

Redis的客户端缓冲区配置具体格式是:

client-output-buffer-limit <class>limit>limit>seconds>

(1)class: 客户端类型:normal、 slave、 pubsub

(2)hard limit: 如果客户端使用的输出缓冲区大于hard limit,客户端会被立即关闭。

(3)soft limit和soft seconds: 如果客户端使用的输出缓冲区超过了soft limit并且持续了soft limit秒,客户端会被立即关闭

连接 Redis 查看 client-output-buffer-limit:

127.0.0.1:6379> config get client-output-buffer-limit
1) "client-output-buffer-limit"
2) "normal 0 0 0 slave 21474836480 16106127360 60 pubsub 33554432 8388608 60"

普通客户端缓冲区normal类型的class、hard limit、soft limit 都是 0,表示关闭缓冲区的限制。

如果缓冲期过小的,就可能会导致的 Unexpected end of stream 异常。

2、Redis服务器 timeout 设置不合理

Redis服务器会将超过 timeout 时间的闲置连接主动断开。

查看服务器的timeout配置:

127.0.0.1:6379> config get timeout
1) "timeout"
2) "600"

timeout 配置为 600 秒,同一个连接等待闲置 10 分钟后,发现还没有被使用,Redis 就将该连接中断掉了。

所以这里就会有个问题,这里的 timeout 时间是要与上文中的 Jedis 线程池里的 空闲资源监测任务 有关系的。

假设 JedisPoolConfig 里的 timeBetweenEvictionRunsMillis 不设置,会使用默认值 -1,不会启动 Evictor 空闲监测任务了。

当从资源池借出 Jedis 连接后,注意此时,如果过了 10 分钟,Redis 服务端已将这根连接给中断了。

而客户端还拿着这个 Jedis 连接去继续操作 set、get 之类的命令,就会出现 Unexpected end of stream 异常了。

示例演示:

为了方便演示,如下参数调整。

1)Redis服务器 timeout 初始化为 10秒

2)Java 测试代码如下所示

new Thread(new Runnable() {
public void run() {
for (int






请到「今天看啥」查看全文