记一次误用JedisPool引起的系统假死问题排查

最近一直在搞公司Ai云平台的服务网关,项目涉及到了一个Oauth2的认证系统。实现认证系统的时候,用了Spring Security Oauth2框架,然后通过redis来实现用户的token等信息的存储。
在进行压测的时候,发现系统经常会出现假死状态,出现假死状态的时候,所有请求都会被挂起不返回。发现这种情况时,我起初猜测是死锁引起的,就用jconsole连到测试服务器来检测死锁,不过并没有检测到死锁。
发现没有死锁,我就登上服务器,用jstack命令dump下当前线程的堆栈信息。拿到堆栈信息之后,我发现大量的线程都被阻塞在从JedisPool获取Jedis资源上,具体堆栈信息贴在下面:


记一次误用JedisPool引起的系统假死问题排查_第1张图片
部分堆栈信息

再结合JedisPool的源码发现,线程都阻塞在从资源队列中获取资源这步。这是什么鬼?怎么会这样?起初我怀疑是Jedis对象创建失败了,所以资源队列中没有Jedis对象,于是我dump下了堆信息,使用VisualVM分析堆信息,但是我发现,堆中是有Jedis对象的,而且Jedis对象个数正好和JedisPool设置的最大对象个数一致。看来又猜错了!!!难道是使用JedisPool的时候有地方忘记归还资源了???我检查了一遍代码,使用jedispoll的时候清一色的try()语句:

  try (Jedis jedis = jedisPool.getResource()){
  ...
  }

我这就纳闷了,这是怎么回事,资源也都释放了,为什么会这样?静下心来再去看线程堆栈信息,我发现一个问题:


记一次误用JedisPool引起的系统假死问题排查_第2张图片
堆信息

这两个方法,在一个线程调用链中,方法实现如下:

    @Override
    public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
        String key = ACCESS_KEY + authentication.getOAuth2Request().getClientId();
        try (Jedis jedis = redisSource.getConnect();) {
            String accessToken = jedis.get(key);
            if (accessToken != null) {
                return readAccessToken(accessToken);
            }
        }
        return null;

    }

    @Override
    public OAuth2AccessToken readAccessToken(String tokenValue) {
        String key = TOKEN_PREFIX + tokenValue;
        try (Jedis jedis = redisSource.getConnect();) {
            List result = jedis.hmget(key, "access_token", "access_key_id", "access_key", "refresh_token", "user_id");
            if (result != null && result.size() > 0 && result.get(0) != null) {
                DefaultOAuth2AccessToken auth2AccessToken = new DefaultOAuth2AccessToken(tokenValue);
                auth2AccessToken.setRefreshToken(new DefaultOAuth2RefreshToken(result.get(3)));
                Map map = new HashMap<>();
                map.put("accessKeyId", result.get(1));
                map.put("userId", result.get(4));
                long expire = jedis.ttl(key);
                auth2AccessToken.setExpiration(new Date(System.currentTimeMillis() + expire * 1000));
                auth2AccessToken.setAdditionalInformation(map);
                if (logger.isDebugEnabled()) {
                    logger.debug("读取到accessToken:" + MoreObjects.toStringHelper(auth2AccessToken).toString());
                }
                return auth2AccessToken;
            }
        }
        return null;
    }

细心的朋友肯定能发现,在获取Jedis对象的时候有一个问题:重入了!!!没错,就是重入了。当并发高的时候,请求一起打过来,多个线程同时执行getAccessToken(OAuth2Authentication authentication)方法的时候,获取了Jedis对象,然后进入readAccessToken(String tokenValue)方法,这个时候,Jedis对象都被外部的getAccessToken(OAuth2Authentication authentication)方法持有,所以就被阻塞了,而外部的getAccessToken(OAuth2Authentication authentication)等不到readAccessToken(String tokenValue)执行完成,所以永远都不会释放自己持有的Jedis对象!!!而这种情况是检测不到死锁的。竟然是因为这个原因!!!能发现这个错误也是运气好呀。

你可能感兴趣的:(记一次误用JedisPool引起的系统假死问题排查)