记一次使用Jedis客户端获取不到资源(Could not get a resource from the pool)的填坑经历

经历描述:本人联合一狗蛋开发一个小APP,本人负责服务器的开发,狗蛋负责APP的开发。登录验证使用到了Redis做Token缓存,于是使用了Jedis库来操作阿里云服务器上的Redis。项目一开始,我便随便从网上找了别人封装好了的Jedis的工具类,想着无非就是往Redis中做一些增删查操作,网上关于Redis+Token的资源很多,直接从网上下载后整合到自己项目中了,并没有想到以后会有什么问题。在程序中修改了一下别人的IP为自己服务器的IP后(恰恰是这个操作,奠定了我今后两天的痛苦填坑经历),便使用本地IDEA开发测试,没毛病,可以正常在Redis中增删查Token,于是就把项目打包上传到了云服务器中的Tomcat中,使用Postman测试,同时使用tail -F /root/tomcat/logs/catalina.out 命令查看Tomcat输出的日志信息,,一切都是那么的顺理成章,一切都又那么的安静祥和。日志信息开始缓慢又优雅的匀速在控制台打印,突然,不知怎的,一行行带有error的日志信息如山洪一般,扑面而来,歇斯底里地在屏幕上打印了出来,此时的我不由得倒一了一口凉气。What the hell?

redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
        at redis.clients.util.Pool.getResource(Pool.java:53)
        at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226)
                ...................
Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
                ...................
Caused by: java.net.SocketTimeoutException: connect timed out

大概就是这些重要的错误信息吧。通过打印的日志,我清晰的看到了——无法从资源池中获取资源(后面的连接超时当时没太在意)!!

无法获取资源??这是怎么回事??马上,把错误信息复制到百度、谷歌,一看,呦,这个问题很大众啊,看来,不是啥大问题嘛,接了杯水,想着三两分钟差不多可以解决。

网上关于这个问题的解决我大致看了看,无非分为以下几个原因:

  1. Redis没有启动:我的不是这个原因,使用RedisDesktopManager工具连接正常。
  2. 防火墙、安全组:防火墙压根没开过、安全组早已经把6379端口开放了。
  3. IP地址或端口错误:查看以下程序中写的自己云服务器的IP和端口,没问题。
  4. Jedis 对象用完以后未释放掉,池中无可用资源,所以会出现无法获取新的资源。

看来看去,仿佛也就第四种可能与我的错误有关系啊。于是我便仔细得看了一下,网上说,客户端去redis服务器拿连接的时候,池中无可用连接,即池中所有连接被占用,且在等待时候设定的超时时间后还没拿到时,报出此异常。 解决办法:调整JedisPoolConfig中maxActive为适合自己系统的阀值。 按照网上说的,调整了maxActive的值,一遍又一遍往云服务器打包、上传、测试,一遍又一遍的出现相同的错误,本以为很快就会解决的问题,转眼已经拖了半天了,问题依然存在!!

下午,午休过后,继续!

网上一篇博客中提到:如果你用的jedis 2.4.2以及以前版本,用完之后别忘了return连接到资源池。不过2.5.0版本之后,jedis使用了try-with-resource,jedis用完了就会自动归还了,不用每次都自己return了。相应的他还给出了一个完整的,带有return资源到连接池的Jedis工具类,我一想,我使用的Jedis版本是2.8.0啊,按他说的讲道理jedis用完了就会自动归还了,没办法,病急乱投医吧,虽然2.8.0自带释放资源的操作了,我再释放一遍,礼多人不怪嘛,没问题吧,我就把我之前的工具类给替换掉了。不过他写的这个工具类确实挺好的,后面一直使用的也是这个,我贴到下面哈:


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @说明: Redis操作工具类
 * @return
 * @author:Jeremiah Yu
 * @date 2019/3/5 11:36
 */
public class JedisUtil {

    private static final Log logger = LogFactory.getLog(JedisUtil.class);

    //Redis服务器IP
    private static String IP = "127.0.0.1";

    //Redis的端口号
    private static int PORT = 6379;

    //Redis服务密码
    private static String password = "******";

    //可用连接实例的最大数目,默认值为8;
    //如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
    private static int MAX_ACTIVE = 64;

    //控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。
    private static int MAX_IDLE = 20;

    //等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException;
    private static int MAX_WAIT = 3000;

    private static int TIMEOUT = 3000;

    //在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
    private static boolean TEST_ON_BORROW = true;

    //在return给pool时,是否提前进行validate操作;
    private static boolean TEST_ON_RETURN = true;

    private static Map<String, JedisPool> maps = new ConcurrentHashMap<String, JedisPool>();

    private JedisUtil() {
    }

    /**
     * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例 没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载。
     */
    private static class RedisUtilHolder {
        private static JedisUtil instance = new JedisUtil();
    }

    /**
     * 当getInstance方法第一次被调用的时候,它第一次读取 RedisUtilHolder.instance,导致RedisUtilHolder类得到初始化;
     * 而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建RedisUtil的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,
     * 并由虚拟机来保证它的线程安全性。 这个模式的优势在于,getInstance方法并没有被同步,
     * 并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。
     */
    public static JedisUtil getInstance() {
        return RedisUtilHolder.instance;
    }

    /**
     * 获取连接池.
     */
    private JedisPool getPool(String ip, int port) {
        String key = ip + ":" + port;
        JedisPool pool = null;
        if (!maps.containsKey(key)) {//根据ip和端口判断连接池是否存在.
            JedisPoolConfig config = new JedisPoolConfig();
            config.setMaxTotal(MAX_ACTIVE);
            config.setMaxIdle(MAX_IDLE);
            config.setMaxWaitMillis(MAX_WAIT);
            config.setTestOnBorrow(TEST_ON_BORROW);
            config.setTestOnReturn(TEST_ON_RETURN);

            //false:如果连接池没有可用Jedis连接,立即抛出异常;默认true时:如果连接池没有可用Jedis连接,
            //会等待maxWaitMillis(毫秒),依然没有获取到可用Jedis连接
            //config.setBlockWhenExhausted(false);

            try {
                pool = new JedisPool(config, ip, port, TIMEOUT,password);
                maps.put(key, pool);
            } catch (Exception e) {
                logger.error("初始化Redis连接池异常:", e);
            }
        } else {
            pool = maps.get(key);
        }
        return pool;
    }

    /**
     * 获取Jedis实例
     */
    public Jedis getJedis() {
        Jedis jedis = null;
        try {
            jedis = getPool(IP, PORT).getResource();
        } catch (Exception e) {
            logger.error("获取Jedis实例异常:", e);
            // 销毁对象
            getPool(IP, PORT).returnBrokenResource(jedis);
        }
        return jedis;
    }

    /**
     * 释放jedis资源到连接池
     */
    public void returnResource(final Jedis jedis) {
        if (jedis != null) {
            getPool(IP, PORT).returnResource(jedis);
        }
    }

    /**
     * 获取数据
     */
    public Object get(String key) {
        Object value = null;
        Jedis jedis = null;
        try {
            jedis = getJedis();
            value = jedis.get(key);
        } catch (Exception e) {
            logger.warn("获取数据异常:", e);
        } finally {
            //返还到连接池
            returnResource(jedis);
        }
        return value;
    }

    //设置数据
    public boolean set(String key,String value){
        Jedis jedis = null;
        try {
            jedis = getJedis();
            jedis.set(key,value);
            return true;
        }catch (Exception e){
            logger.warn("设置数据异常",e);
            return false;
        }finally {
            //返还到连接池
            returnResource(jedis);
        }
    }

    //删除数据
    public void delete(String key){
        Jedis jedis = null;
        try {
            jedis = getJedis();
            jedis.del(key);
        }catch (Exception e){
            logger.warn("删除数据异常",e);
        }finally {
            //返还到连接池
            returnResource(jedis);
        }
    }

    public static void main(String[] args) {
        JedisUtil.getInstance().set("lp","yuzhsaneh");
        Object val = JedisUtil.getInstance().get("lp");
        System.out.println(val);
    }
}

又一次激动地打包、上传、测试。WTF!!还是一样的问题。

想啊,查啊,调啊,重装Redis啊,重装Tomcat啊,重启服务器啊,可以想到的招式全部都用过了,一半天又过去了!问题还是没有解决。我真的快要哭了。

时间来到了第二天。

看到了阿里云社区的一篇博客详细的介绍了这个问题出现的根源,我开始一个字一个字的研读,希望可以解决我的问题。博客内容

 连接池参数blockWhenExhausted = true(默认)如果连接池没有可用Jedis连接,会等待maxWaitMillis(毫秒),如果依然没有获取到可用Jedis连接,会抛出如下异常:

redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
    …
Caused by: java.util.NoSuchElementException: Timeout waiting for idle object
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:449)

连接池参数blockWhenExhausted = false;设置该参数,如果连接池没有可用的Jedis连接,立即抛出异常:

redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
    …
Caused by: java.util.NoSuchElementException: Pool exhausted
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:464)

上述异常是客户端没有从连接池(最大数量maxTotal)获得可用的Jedis连接造成的,可能有如下原因:

(1) 连接泄露:JedisPool默认的maxTotal=8,下面的代码从JedisPool中获取了8个Jedis资源,但是没有归还资源,当第9次尝试获取Jedis资源的时候,无法获得(jedisPool.getResource().ping())(我已经换成了别人写好的Redis工具类,别人封装的很好了啊,用完就放回资源池里,这种情况可以排除)

(2) 业务并发量大,maxTotal设置得过小了。(我的项目压根就没有上线,不存在什么并发)

(3) Jedis连接阻塞:例如Redis发生了阻塞(例如慢查询等原因),所有连接在超时时间范围内等待,并发量较大时,会造成连接池资源不足。(我重新安装过Redis,第一次连接也是同样的问题,所以这种情况也可以排除)

检查是否DNS问题、检查是否TIME_WAIT问题、检查是否发生nf_conntrack丢包.......

网上可以找到的解决办法,我都试了一遍,毫无效果!难道这个问题就无解了吗???

时间一分一秒的过去了,问题还是没有解决,整整两天时间,什么事情都没有做,就在处理这个bug了。

休息一下吧,想着Redis在云服务器上安装以来,还没有使用过redis-cli客户端,玩一下吧。

进入bin目录下,使用命令:redis-cli -h 39.105.1*4.** -p 6379,回车。

嗯???怎么回事?怎么连接不上啊???服务器IP是这个啊,难道云服务器不知道自己的公网IP吗??

我抱着试一试的态度,使用了:redis-cli -h 127.0.0.1 -p 6379,回车,唰一下子,说时迟那时快,连接上了!!

我仿佛知道了些什么。

我抓紧时间修改程序:

将程序中Jedis参数中的IP设置为127.0.0.1

把程序打包上传,激动人心的时候到了。手颤颤抖抖的打开postman,输出URL、参数,点击send。

啊!!期待已久的画面终于出现!!!

问题就这么解决了。

我知道现在还没有搞明白,为什么服务器自己使用自己的公网IP连接自己的Redis服务会连接不上,换成127.0.0.1就可以了。

  • 21
    点赞
  • 65
    收藏
    觉得还不错? 一键收藏
  • 24
    评论
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值