性能文章>一个NullPointerException,竟然有这么多花样!>

一个NullPointerException,竟然有这么多花样!原创

1年前
544608

案发现场

我们先看一下给出的异常栈

java.lang.NullPointerException
 at org.springframework.data.redis.cache.RedisCache.get(RedisCache.java:180)
 at org.springframework.data.redis.cache.RedisCache.get(RedisCache.java:133)
 at org.springframework.cache.transaction.TransactionAwareCacheDecorator.get(TransactionAwareCacheDecorator.java:69)
 at org.springframework.cache.interceptor.AbstractCacheInvoker.doGet(AbstractCacheInvoker.java:71)
 at org.springframework.cache.interceptor.CacheAspectSupport.findInCaches(CacheAspectSupport.java:537)
 at org.springframework.cache.interceptor.CacheAspectSupport.findCachedItem(CacheAspectSupport.java:503)
 at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:389)
 at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:327)
 at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:61)

根据异常栈我们很轻松就能定位到源码位置

image.png

很明显existsnull,就会出现本篇的NullPointerException。当然如果这样三分钟没到本篇就草率结束,明显有失肥朝的男儿本色!另外若当真如此草率,那么关注肥朝公众号的意义何在?肥朝公众号的老粉丝们都知道,肥朝的海量源码实战类文章,都有三个特点

  • 从源码原理角度分析,为什么会出现这个问题?

  • 如何解决掉这个问题?

  • 我们如何深度思考,不断从这次经历中压榨出最大价值。(非常重点!)

比如就拿这个问题来说,按照我们正常的思维惯性,我们看到176行有个return,又知道existsnull,那么我们点进connection.exists(cacheKey.getKeyBytes())这个方法一探究竟。

image.png

结果发现果然如我们所料,这里竟然有两个return null的情况,这个时候就有粉丝把持不住,要九浅一深直入源码分析,看这两个条件什么时候满足。但是肥朝想说的是,且慢动手!

你注意看这段代码

Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() {

        @Override
        public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
            return connection.exists(cacheKey.getKeyBytes());
        }
    });


public interface RedisOperations<K, V> {
    <T> T execute(RedisCallback<T> action);
}

我们现在已知execute的返回值T(exists)为null,T和action的返回值的关系,要看execute代码里面的具体实现的!这点很重要,也很容易被疏忽!由于文中提到了随机出现,那么这里就涉及到一个排查的技巧。一般和随机出现有关的,根据经验,主要从两个方向入手

1.根据多个异常的情况的入参数据来分析,看看多个异常情况的入参都有什么特点。

2.模拟并发量,因为很多问题本地重现不了是因为并发量不够。

该粉丝根据方式二,将自己的项目代码中的Redis代码抽取了一个最简模型,将问题重现。但是由于该模型是ssm项目,为了方便大家都能重现并参与其中,我用springboot抽取了相关模型,如下图:

image.png

真相大白

将上面的代码运行,果不其然出现了我们想要找的异常栈。

image.png

我们把断点打在了上面说的那两个可能返回null的地方,可能你却惊奇的发现,断点根本没有进去。那就只剩下一个可能了。那就是redisOperations.execute()方法。我们发现,demo中继承RedisTemplate自定义了RedisOperations,名为RedisCustomTemplate。然后我们在catch处打上断点。

image.png

从这里我们就知道了。连接池参数blockWhenExhausted = true(默认),如果连接池没有可用Jedis连接,会等待maxWaitMillis(毫秒),依然没有获取到可用Jedis连接,会抛出这个异常。

根据沟通了解到,该同学生产代码中,catch住后是没有任何日志输出,直接返回了null,自然就导致了NullPointerException。

如何解决

肥朝认为解决问题要从两个方面入手。

1.连接池为什么不够?连接数不够时,盲目调大连接数是最常见的错误做法,根本解决办法还是要挖掘背后不够的原因,一般连接数不够,根据《Redis开发与运维》作者付磊在书中总结出无非是以下几点。

  • 1.1 连接泄漏(较为常见)

  • 1.2 业务并发量大,maxTotal确实设置小了。

  • 1.3 Jedis连接还的太慢(Redis发生了阻塞,例如慢查询等原因)。

  • 1.4 其他问题(例如丢包、DNS、客户端TCP参数配置)。

具体是属于上述哪个情况,自己对症下药。

2.自定义RedisCache

我们知道RedisCallback确实会存在两个返回null的情况,根据

if (!exists) {
    return null;
}

这样的判断方式,存在很大的空指针异常隐患,我们可以继承RedisCache,然后重写该方法,将这个判断的bug改掉,也可以去最大同性交友网站github上,查看最新版本的bug修复情况,当然很多公司更换jar版本都要走流程,所以具体处理方式,酌情处理。

问题复盘

在真相大白和给出解决方案后,那么我们就将整件事进行复盘。

提问的艺术

我们再回顾一下该粉丝的提问:“你们有没有遇到xxx问题”,其实显而易见,这样的提问方式,相信和他一样因为这个不规范,并且公司还有一定流量触发出问题的概率,小之又小。如果把“你们有没有遇到过xxx问题”换成“我遇到了一个xxx问题”,这样可能回复率还会多一丢丢。当然,问人毕竟是最低效的解决方式,最重要的还是,要掌握分析问题的技巧,以及源码实战的经验。当然很多同学反馈,公司根本没有源码实战经验的机会,因此,关注肥朝公众号,积累源码实战经验就显得格外重要了。

编码规范

如果同学没有吞掉异常,日志输出了真实异常

org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; 

nested exception is redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool

那么这个问题简直是随便搜索一下都秒解决。但是关键就在于吞掉了真实异常,并且返回null,非常碰巧的是

public Boolean exists(byte[] key) {
    try {
        if (isPipelined()) {
            pipeline(new JedisResult(pipeline.exists(key)));
            return null;
        }
        if (isQueueing()) {
            transaction(new JedisResult(transaction.exists(key)));
            return null;
        }
        return jedis.exists(key);
    } catch (Exception ex) {
        throw convertJedisAccessException(ex);
    }
}

该方法还有两个返回null的情况,给排查时造成了极大的迷惑性,容易让我们把精力放在了这里。

命名不规范。

在沟通中,我发现demo中RedisCustomTemplate这个成为问题最关键突破口的地方,在他们公司,竟然被起了一个欺骗性的命名。

image.png

异常输出。

从该粉丝的demo中,我们看到了这样的代码。

image.png

别以为这只是个demo无所谓,细节往往能看出编码的意识! 大家可以检查一下自己的项目。

ex.printStackTrace();

的方式。这样的方式存在两个非常大的隐患。

4.1 该方式用到了synchronized,当然这个还是小问题,毕竟synchronized在jdk 1.6做了很多优化,性能提升了很大。(这个很多优化到底是啥优化,后续肥朝再讲解)

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

4.2 这样的输出方式,无法将异常输出到日志文件。因为这个是jdk的方法,人家怎么可能输出到你要的文件路径。然后我们生产分析问题,都是要看日志,你通过这种方式输出,自然就会导致关键信息丢失!

4.3 ex.getMessage()的方式,是无法输出异常栈信息的。我们来看一下阿里规范手册提到的输出方式:

image.png

请先登录,再评论

暂无回复,快来写下第一个回复吧~

为你推荐

字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
据说99.99%的人都会答错的类加载的问题
概述首先还是把问题抛给大家,这个问题也是我厂同学在做一个性能分析产品的时候碰到的一个问题。 同一个类加载器对象是否可以加载同一个类文件多次并且得到多个Class对象而都可以被java层使用吗请仔细注意
Java多线程——并发测试
编写并发程序时候,可以采取和串行程序相同的编程方式。唯一的难点在于,并发程序存在不确定性,这种不确定性会令程序出错的地方远比串行程序多,出现的方式也没有固定规则。那么如何在测试中,尽可能的暴露出这些问
Java多线程知识小抄集(一)
本文主要整理笔者遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子,习题方便理解和记忆。 1.interr