Jedis连接问题

一、现状

  • 项目实施中生产环境获取redis缓存报错。
Unexpected end of stream.

二、复现 - Unexpected end of stream

  • 环境版本信息
应用版本数据来源
jdk1.8.0_231
redis2.8.19./redis-cli info server | grep redis_version
jedis2.7.3
ss-redis1.2.6.1
  • 设置redis服务端超时时间或者在redis.conf修改

    ./redis-cli -p 7379 config set timeout 30
    
  • 客户端验证超时(伪代码)

    /** 重要参数
    MinIdle=2
    MaxIdle = 2
    TestOnBorrow = false
    TestOnReturn = false;
    MaxTotal = -1;
    MaxWaitMillis = 100L;
    TestWhileIdle = false;
    **/
    initPool();						//初始化连接池
    Thread.sleep(1000*40);			//线程阻塞40s(大于服务端超时时间)
    Jedis jedis = getResource();	//从连接池获取连接
    jedis.set(k, v);				//报错 Unexpected end of stream
    
  • 模拟报错结果如下
    在这里插入图片描述

三、分析 - Unexpected end of stream

  • 设置服务端超时30s,每隔2秒采集redis client连接情况

    while true ;do ./redis-cli  -p 7379 info Clients  | grep connected_clients && sleep 2; done
    
    • 连接数监测结果

      • 如下图中可以看出,在redis服务端30s超时之后,服务端断开连接,连接数变成7。当Jedis客户端再次操作的时候就会报错_Unexpected end of stream_
        在这里插入图片描述
    • 对初始及剩下的7个连接分析结果

      • 6个连接是sentinel与redis-server建立的连接
      • 1个是当前执行采集脚本时候,与redis-server建立的连接
        在这里插入图片描述

四、方案 - Unexpected end of stream

方案一:设置服务端不超时 timeout=0

  • 修改redis-server配置

    ./redis-cli -p 7379 config set timeout 0
    
  • 结果验证

    服务端连接一直保持,不断开。客户端操作正常。
    

方案二:设置TestOnBorrow = true,服务端超时30s

  • 修改Jedis连接池配置

    setTestOnBorrow(true)
    
  • 修改redis-server配置

    ./redis-cli -p 7379 config set timeout 30
    
  • 结果验证

    服务端连接超时断开,客户端重新建立连接获取数据。见下图
    

    在这里插入图片描述

  • 源码分析

    • redis pool将连接对象保存在队列里面,每次获取连接时从队列中取 pollFirst,拿到连接对象执行ping操作。如果正常返回则使用该连接。如果报错则重建连接。
    • testOnBorrow=true保证了连接的可用性。
    public boolean validateObject(PooledObject<Jedis> pooledJedis) {
            BinaryJedis jedis = (BinaryJedis)pooledJedis.getObject();
    
            try {
                HostAndPort hostAndPort = (HostAndPort)this.hostAndPort.get();
                String connectionHost = jedis.getClient().getHost();
                int connectionPort = jedis.getClient().getPort();
                return hostAndPort.getHost().equals(connectionHost) && hostAndPort.getPort() == connectionPort && jedis.isConnected() && jedis.ping().equals("PONG");
            } catch (Exception var6) {
                return false;
            }
        }
    

方案三:设置最大空闲连接为0

设置最大空闲连接为0,每一次重新建立连接,业务操作正常。

方案四:设置Jedis驱逐策略

  • redis pool驱逐参数

    minEvictableIdleTimeMillis:硬闲置时间,连接多久没有使用设置为闲置,检测线程直接剔除闲置
    softMinEvictableIdleTimeMillis:软闲置时间,连接多久没有使用设置为闲置,当空闲连接 > MinIdle,才执行剔除闲置,否则维持最小空闲数,即使闲置了也不会剔除
    timeBetweenEvictionRunsMillis:逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
    
  • 参数设置如下,将逐出扫描时间设置大于服务端超时时间,并得出结果。

    • 设置服务端超时25s
    • Jedis驱逐间隔35s
    minEvictableIdleTimeMillis = 1000 * 35
    softMinEvictableIdleTimeMillis = 1000 * 60
    timeBetweenEvictionRunsMillis = 1000 * 60
    
  • 默认驱逐策略源码

    public class DefaultEvictionPolicy<T> implements EvictionPolicy<T> {
        public DefaultEvictionPolicy() {
        }
    
        public boolean evict(EvictionConfig config, PooledObject<T> underTest, int idleCount) {
            return config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis() && config.getMinIdle() < idleCount || config.getIdleEvictTime() < underTest.getIdleTimeMillis();
        }
    }
    
  • 结果验证

    如下图,存在连接数为7的时间段,说明在此时间段内,如果发生redis操作,会造成服务端超时报错。
    

在这里插入图片描述

  • 将逐出扫描时间设置小于服务端超时时间,并得出结果。

    minEvictableIdleTimeMillis = 1000 * 25
    softMinEvictableIdleTimeMillis = 1000 * 25
    timeBetweenEvictionRunsMillis = 1000 * 28
    
    • 通过将驱逐间隔以及空闲时间修改为小于服务端超时时间。达到客户端主动超时重建的目的。如下图,不会出现redis连接为7的情况,即不产生服务端超时。

    • 设置服务端超时时间30s,Jedis逐出间隔28s,闲置时间25s。

    • 重建并维护最小连接源码

      private void ensureIdle(int idleCount, boolean always) throws Exception {
              if (idleCount >= 1 && !this.isClosed() && (always || this.idleObjects.hasTakeWaiters())) {
                  while(this.idleObjects.size() < idleCount) {
                      PooledObject<T> p = this.create();
                      if (p == null) {
                          break;
                      }
      
                      if (this.getLifo()) {
                          this.idleObjects.addFirst(p);
                      } else {
                          this.idleObjects.addLast(p);
                      }
                  }
      
                  if (this.isClosed()) {
                      this.clear();
                  }
      
              }
          }
      

在这里插入图片描述

方案对比:以上方案均可解决服务端超时报错的问题。

方案修改点优缺点建议
方案一修改服务端超时时间为 0,不超时连接不超时,如果客户端不限制,会打满连接不使用
方案二设置testOnBorrow = true,设置服务端超时30s每次操作之前ping,多一次网络消耗在客户端请求量不大的情况下,可作为临时解决方案
方案三设置最大空闲连接maxIdle为0每次需要重新建立连接,违背了连接池的理念不使用
方案四设置服务端超时时间,设置合理的客户端驱逐策略能很好的使用Jedis连接池设置605s服务端超时时间 > ss-redis源码中定义的驱逐时间间隔600s,让客户端先断开。

五、验证Jedis最大连接参数

  • 验证当从Jedis连接池中获取超过预定参数配置(redis.pool.maxActive)的Jedis最大连接数时,系统的响应情况。下面是简单的模拟这种情况:

    /**
      * 设置maxActive 100, 并持有100连接不释放,验证101请求到来时的情况
      */
    private static void scene2() {
        Jedis jedis = null;
        int count = 0;
        while (count <= 99) {
            for (int i = 0; i < 20; i++) {
                try {
                    jedis = SentinelJedisUtil.getResource();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                count++;
            }
            System.out.println("print redis client: " + jedis.info("Clients").split("\r\n")[1]);
    
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        try {
            System.out.println("start get 101 link");
            jedis = SentinelJedisUtil.getResource();
            System.out.println("print redis client: " + jedis.info("Clients"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
  • 设置 blockWhenExhausted=true 当超过连接池最大连接时,会阻塞等待,超过获取连接等待时间,则抛错。

在这里插入图片描述

  • 设置 blockWhenExhausted=false 当超过连接池最大连接时,直接抛错。
    在这里插入图片描述

  • 获取连接源码

    org.apache.commons.pool2.impl.GenericObjectPool#borrowObject(long)
    
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐