著名IT作家、译者侯捷老师以前在其著作中有句话,就是我们今天文章的标题「源码面前,了无秘密」。可以说相当精炼。高度概括了从源码中我们可以收获的内容。在源码中,无论是应用的调用逻辑,关系,各种设计都一目了然。
为什么会突然想到这样一个题目呢?
是因为最近一个项目上线,其中有几个功能模块使用了 Redis 进行数据的缓存,包括权限信息也在其中。所以每个请求都会通过 Redis 进行鉴权。这块功能是我指导的一个小同学负责开发的。上线几天后出现了这样的问题:
问题现象是线上出现了大量的错误
- Cannot get Jedis connection; nested exception
- is redis.clients.jedis.exceptions.JedisException:
- Could not get a resource from the pool
项目约等于小流量,上线时 Redis 的***连接数开到了1000,用户数远小于***连接数,Redis 使用的是公司的集群,稳定性也有保障,理论上是不该出现这种取不到资源的情况的。
小同学自行 Google了好久没找到问题具体原因。我提供的几个思路也没本质解决问题。不过好在调整配置可以在线下稳定复现。所以我在小同学的电脑上开始debug,跟源码。
和结对编程类似,我一边Debug,我一边给小同学讲 Redis 这个连接池的实现思路,目前怀疑的问题等等。小同学说自己刚才也在试着跟源码,但跟了两层之后,感觉调用越来越深,有点晕,就放弃了。
然后我继续Debug和讲原理,跟到资源使用完毕,在 finally 进行释放回收的地方,发现有处代码在判断当前的 dataSource是否为空,从而执行两种不同的操作。 如果不为空,则会将资源放回pool中,便于下次继续使用,为空则真正的进行close的操作,直接将Socket关闭了。而这两个if/else的逻辑是封装在一个方法中,不跟进来不会发现区别对待。
而且我们当前在用连接池,预期是按连接池的思路,使用完毕的需要释放回池中继续下次的使用,当前这个现象就比较怪异了。
继续向前,发现整体虽然Connection 使用的是JedisConnection,但在我们出问题的这里,返回的是个其子类JedisWrapper,这个是部门同学开发的一个SpringBoot 的 starter,重写了一部分逻辑,将连接对象作为原始对象持有,我们并没有用到其中的特性。上面判断dataSource是否为空本来是要判断JedisConnection里该属性是否为空,在Wrapper之后,是保存了origin的对象,返回的是个new Wrapper,这样dataSource并没有初始化,就出现了前面dataSource为空的问题。
跟到这里,发现是maven 的依赖不同,切换回标准的 SpringBoot starter,问题解决了。
小同学感叹道,「这不是造孽么,如果不是你帮我,不知道从哪下手分析了。」
我则鼓励小同学没有思路的时候,可以从源码里找找,跟代码的过程,也是学习的过程,比如这次就可以在跟的时候了解连接池具体是怎样实现的。任何问题在源码中都无处遁形,况且这些源码也可能有Bug呢。
回过头来,我们不难发现,可能不是所有的问题都需要跟源码,但阅读源码,带着问题 Debug,本身就像你放大招一样,虽然费时费力,但收获也会很多,一招制敌。这也是开源带来的好处,可能通过源码来分析作者的思路,在问题产生时可以逐行分析,了解细枝末节,这些都是C/C++里dll文件所不具备的优势。:-)
对于新同学,可能对于源码阅读有畏难情绪,感觉这一层层的嵌套,调用有点找不着头绪。但这些都是表象,你试着跟几次就轻车熟路了。
老司机为什么称为老司机,一是车技好,二是熟悉车况。这些也都是练出来的。
所以无论是做为解决问题的***手段,还是学习设计与思想的宝库,源码都是不二之选,有时间的时候,可以多看看,不久之后,你也会是老司机。
***,总结下一般出现问题时,跟源码的思路
- 如果有异常,可以利用IDE的异常断点,这样异常产生时,一个鲜活的异常栈就获取到了,完整的调用链就在你眼前。
- 逐级深入,先分块找到怀疑的目标方法,结合前一步,跟进
- 具体方法时,不要根据表现的方法名,参数来猜方法的意图,必要时进去看一眼,万一有隐藏逻辑呢
- 同样的类,看看包名、类名,是否是加载的类不对导致
【本文为51CTO专栏作者“侯树成”的原创稿件,转载请通过作者微信公众号『Tomcat那些事儿』获取授权】