这一天风和日丽,我很荣幸的参加进入组织的活动,这个组织依然是一群闷骚的少年,热火朝天的甩着膀子,写着神圣的 Java 代码,偌大的办公室,只能听见噼里啪啦的敲击键盘声!
好骚气的组织!!!
------------------------------------------------------------------------------------------------------------------------
进入组织后,给我随手就撩过来一个 git 地址,习惯性的通过 git clone <git-url> ,将代码 dang 下来!
我的 IDEA 已经饥渴难耐,说时迟那时快。鼠标飞快的飞到桌面的 , 以盲狙的速度进行了双击!经过 10s 的长达等待。主界面终于展示在我的眼前。仿佛看到了我梦中的女神——怦然心动,无法形容我的处境。
作为一个不会技术的二溜子,岂能就此作罢,扫视一眼后发现,原来是仰慕已久的 Web 项目基佬(这么说主要是因为培训机构 3 个月能生产一批,而且都是各行各业的男性同胞),瞬间心情大落!吾等倒要看看你是什么妖孽,没有妖孽也要给你制造一批出来。
拉出我的汤姆猫( tomcat ), 将它迅速加载进来。紧接着一个飘逸的 “Shift + F10” (IDEA 的 快捷键)闪过。
一个 http://localhost:8080 的界面自动打开在我的眼前。
哎呀,好帅气的界面……
输入测试账号、测试密码、登录验证码……一波骚操作之后,功能都可以正常使用!
待我休息三秒后,挠了挠头,对 组织成员 A 说:嗨,帅哥,发 50 个请求过来玩玩!
哈……果然,出现了骚气的问题,页面请求处于 pending 状态,过 10s 报 timeout , 后台日志报错:
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
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:204)
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:348)
at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:129)
at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:92)
at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:79)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:194)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:169)
at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:91)
at org.springframework.data.redis.core.DefaultValueOperations.increment(DefaultValueOperations.java:63)
注:说明一下,我们得 redis 环境配置主要为:
<property name="maxIdle" value="50"/>
<property name="minIdle" value="20"/>
<property name="usePool" value="true" />
----------------------------------------------------------------------------------------------------------------------------------
看到如此之问题,岂不是很蛋疼,立即跳起来,左手叉腰,右手指向天花板。大喊一声:拿我的 5 米长刀来。
我跟着异常中提示的异常堆栈信息,我打开代码,并定位到异常的行数位置,查看代码。大多都是通过 redisTemplate 来与 Redis 交互。redis 的连接池是通过 common-pools 来管理的,redisTemplate 之前我在其他项目也使用过,不应该会出现泄漏的问题。
怀着激动不安的心情,我进去到如下代码中进行了代码跟踪:
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
这里我以 valueOperations.set()进行了代码跟踪, set 方法的实现如下:
public void set(K key, V value) {
final byte[] rawValue = rawValue(value);
execute(new ValueDeserializingRedisCallback(key) {
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
connection.set(rawKey, rawValue);
return null;
}
}, true);
}
追踪代码,set 方法调用内部,我很能确定的是,调用后,链接进行了关闭操作(代码如下)!其实,严格来说,使用连接池,通过 borrowObject()方法获取的,最终当然是通过 returnObject()! 读者有兴趣可以直接了解 : common-pools2.jar 源代码了解。
/**
* Executes the given action object within a connection that can be exposed or not. Additionally, the connection can
* be pipelined. Note the results of the pipeline are discarded (making it suitable for write-only scenarios).
*
* @param <T> return type
* @param action callback object to execute
* @param exposeConnection whether to enforce exposure of the native Redis Connection to callback code
* @param pipeline whether to pipeline or not the connection for the execution
* @return object returned by the action
*/
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
Assert.notNull(action, "Callback object must not be null");
RedisConnectionFactory factory = getConnectionFactory();
RedisConnection conn = null;
try {
if (enableTransactionSupport) {
// only bind resources in case of potential transaction synchronization
conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
} else {
conn = RedisConnectionUtils.getConnection(factory);
}
boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
RedisConnection connToUse = preProcessConnection(conn, existingConnection);
boolean pipelineStatus = connToUse.isPipelined();
if (pipeline && !pipelineStatus) {
connToUse.openPipeline();
}
RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
T result = action.doInRedis(connToExpose);
// close pipeline
if (pipeline && !pipelineStatus) {
connToUse.closePipeline();
}
// TODO: any other connection processing?
return postProcessResult(result, connToUse, existingConnection);
} finally {
RedisConnectionUtils.releaseConnection(conn, factory);
}
}
看了看我的 5 米长刀,再看看这个异常,虎躯一震:难不成真要让我的大刀上场?!
干!!!
天下大事,要干成功,一般需要三步:
1. 千军易找,一将难求!(打开终端,找到程序的 pid )
2. 招兵买马、屯田生产(获取应用堆栈信息,命令为: jmap -dump,format=b,file=/home/hadoop/my-dump.hprof <pid> )
3. 拿下城池,弑帝称王!(使用 Mat 分析定位、解决问题)
注: MAT 全称为:Eclipse Memory Analyzer, 下载地址为:http://www.eclipse.org/mat/ , 读者下载完后,可以考虑修改一下 MemoryAnalyzer.ini 文件中 -Xmx 的大小( 如果你的 hprof 很大的话,会造成 OOM,导致无法继续分析 )
--------------------------------------------------------------------------------------------------------------------------
我的文件打开后,如下图,占用内存并不大。我们主要是分析链接泄漏。既然是泄漏,肯定就存在有内存不能释放,并且可 DUMP 。
点击内存占用最高的饼图位置,会出现如下图示的菜单可选择。选择 "show objects by class " -> “ by incoming references ” !
随后会打开 "class references" 窗口, 果断的在 ClassName 顶部的搜索框中输入 “com” ( 我们只关注我们关注的内容),由于是连接池泄漏,所以其他的内容我们可以不用理会了哈,直接看 org.apache.commons.pool2.impl.GenericObjectPool 即可。
在该类上面右击,选择 “Java Basics” ——> "Open In Dominator Tree" ,打开 !
一步步的展开 org.apache.commons.pool2.impl.GenericObjectPool 我们可以看到如下图。哈,,,大大的 hscan !
到这一步,我们已经很清晰了。 在 IDEA 中搜索 hscan 相关的代码。果然,找到了一些使用 scan 命令的地方,再细细端详才发现, 罪魁祸首为:
Cursor<Map.Entry<String, Bean>> cursor = hashOperations.scan(scanOption);
cursor 使用完毕后,没有看到调用 .close() 的地方。
果断加上,再测一把!!!顺利通过。
结论:
在平时的代码中,一定要记得在使用链接、游标、流等位置,记得关闭!否则会造成不可预料的问题。
问题顺利解决,收起我的 5 米大刀!
端起我的碧螺春,轻抿一口!
窗外不知何时漂起了小雨!