这个是今天发现一个bug:在测试redis并发读写的时候(jedis做为客户端,并使用了链接池),老是报 java.lang.ClassCastException: java.util.ArrayList cannot be cast to java.lang.Long
at redis.clients.jedis.Connection.getIntegerReply(Connection.java:161)
at redis.clients.jedis.Jedis.del(Jedis.java:108)
相似的错误,就是返回值类型和文档上的返回值类型不相符,感受很不该该;开始怀疑是jedis实现的一个bug,后来发现一个现象,当抛一个超时异常的时候,后面就连续的出现一个相似上面的错误,最后终于发现了问题所在。
原先的代码是这样的:
public long del(String key) {
long rt = 0L;
Jedis jedis = null;
try {
jedis = getJedis();
rt = jedis.del(key);
}
finally
{
releaseJedisInstance(jedis);
}
return rt;
}
这样写貌似OK,但实际上有问题,假设jedis在执行这个命令的时候,由于redis超负荷,jedis可能返回超时的异常,这个时候发生了什么,没有处理这个异常,直接将这个jedis的连接返回到了链接池,这样有没有问题呢?
查看jedis源码发现他的connection中对网络输出流作了一个封装,其中自建了一个buffer,因此当发生异常的时候,这个buffer里还残存着上次没有发送或者发送不完整的命令,这个时候没有作处理,直接将该链接返回到链接池,那么重用该链接执行下次命令的时候,就会将上次没有发送的命令一块儿发送过去,因此才会出现上面的错误“返回值类型不对”;
因此正确的写法应该是在发送异常的时候,销毁这个链接,不能再重用!
正确的写法以下:
public long del(String key) {
long rt = 0L;
Jedis jedis = null;
try {
jedis = getJedis();
rt = jedis.del(key);
releaseNormalResource(jedis);
} catch (Exception e) {
returnBrokenResource(jedis);
throw Exception x;
}
return rt;
}
从上面的分析来看,我更认为是jedis实现的一个bug,当链接出现异常的时候,应该对该链接的buffer进行清空的,你认为呢?
咱们在线上也遇到了这个问题,也使用相同的处理方法,但本质上该问题是没有解决的。
由于在 ShardedJedisFactory.destroyObject(pooledShardedJedis) 中经过捕获全部的Exception,同时把异常忽略了,并未warn日志。经过这种方式虽然屏蔽了该问题,仅仅是忽略了异常,其实并未根本解决。(从下面日志就能看出来)
“从上面的分析来看,我更认为是jedis实现的一个bug,当链接出现异常的时候,应该对该链接的buffer进行清空的,你认为呢?” 赞同
Java代码
- [2015-02-07 09:17:47] WARN c.f.f.b.s.r.i.CustomShardedJedisFactory -quit jedis connection for server fail: xxx.xxx.xxx.xxx:xxx
-
- java.lang.ClassCastException: java.lang.Long cannot be cast to [B (强制类型转换异常)
- at redis.clients.jedis.Connection.getStatusCodeReply(Connection.java:181) ~[jedis-2.6.2.jar:na]
- at redis.clients.jedis.BinaryJedis.quit(BinaryJedis.java:136) ~[jedis-2.6.2.jar:na]
- at cn.fraudmetrix.forseti.biz.service.redis.impl.CustomShardedJedisFactory.destroyObject(CustomShardedJedisFactory.java:116) ~[forseti-biz-service-1.0-SNAPSHOT.jar:na]
- at org.apache.commons.pool2.impl.GenericObjectPool.destroy(GenericObjectPool.java:848) [commons-pool2-2.0.jar:2.0]
- at org.apache.commons.pool2.impl.GenericObjectPool.invalidateObject(GenericObjectPool.java:626) [commons-pool2-2.0.jar:2.0]
- at redis.clients.util.Pool.returnBrokenResourceObject(Pool.java:83) [jedis-2.6.2.jar:na]
- at cn.fraudmetrix.forseti.biz.service.redis.impl.CustomShardedJedisPool.returnBrokenResource(CustomShardedJedisPool.java:121) [forseti-biz-service-1.0-SNAPSHOT.jar:na]
- at cn.fraudmetrix.forseti.biz.service.redis.impl.RedisServiceImpl.zadd(RedisServiceImpl.java:337) [forseti-biz-service-1.0-SNAPSHOT.jar:na]
- at cn.fraudmetrix.forseti.biz.service.redis.impl.RedisServiceImpl.zadd(RedisServiceImpl.java:319) [forseti-biz-service-1.0-SNAPSHOT.jar:na]
- ...
- [2015-02-07 09:17:47] ERROR c.f.f.b.s.r.i.RedisServiceImpl -'zadd' key fail, key: xxx, score: xxx, member: xxx
-
- [2015-02-07 09:17:47] ERROR c.f.f.b.s.r.i.RedisServiceImpl -java.net.SocketTimeoutException: Read timed out
-
- redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out (Socket读取超时异常)
- at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:201) ~[jedis-2.6.2.jar:na] ('limit = in.read(buf);' at java.io.InputStream.read(InputStream.java:100) - 这里出现阻塞致使"Socket读取超时"!)
- at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40) ~[jedis-2.6.2.jar:na]
- at redis.clients.jedis.Protocol.process(Protocol.java:128) ~[jedis-2.6.2.jar:na]
- at redis.clients.jedis.Protocol.read(Protocol.java:192) ~[jedis-2.6.2.jar:na]
- at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:282) ~[jedis-2.6.2.jar:na]
- at redis.clients.jedis.Connection.getIntegerReply(Connection.java:207) ~[jedis-2.6.2.jar:na]
- at redis.clients.jedis.Jedis.zadd(Jedis.java:1293) ~[jedis-2.6.2.jar:na]
- at redis.clients.jedis.ShardedJedis.zadd(ShardedJedis.java:364) ~[jedis-2.6.2.jar:na]
- at cn.fraudmetrix.forseti.biz.service.redis.impl.RedisServiceImpl.zadd(RedisServiceImpl.java:328) [forseti-biz-service-1.0-SNAPSHOT.jar:na]
- at cn.fraudmetrix.forseti.biz.service.redis.impl.RedisServiceImpl.zadd(RedisServiceImpl.java:319) [forseti-biz-service-1.0-SNAPSHOT.jar:na]
- ...
- Caused by: java.net.SocketTimeoutException: Read timed out
- at java.net.SocketInputStream.socketRead0(Native Method) ~[na:1.7.0_51]
- at java.net.SocketInputStream.read(SocketInputStream.java:152) ~[na:1.7.0_51]
- at java.net.SocketInputStream.read(SocketInputStream.java:122) ~[na:1.7.0_51]
- at java.net.SocketInputStream.read(SocketInputStream.java:108) ~[na:1.7.0_51]
- at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:195) ~[jedis-2.6.2.jar:na]
- ... 38 common frames omitted
HelloJimmy 写道
关注
谢谢
bert82503 写道
HelloJimmy 写道
bert82503 写道
javaeyes 写道
这个太悲剧了,每一个请求都要try catch finally
Java代码
- poolConfig.setTestOnBorrow(true);
- poolConfig.setTestOnReturn(true);
这样就搞定问题了~
这样是OK,可是性能降低的厉害,每一个验证就是一个网络操做(ping/pong),效率低,并且对redis服务仍是一个负担,十分不建议!
你说得对,咱们在实际生产环境中使用时也是关闭了这两个属性,在外层统一包装一层 RedisService。
咱们基于 Jedis 定制实现的"Redis服务器异常(宕机)时自动摘除,恢复正常时自动添加"的功能。
https://github.com/EdwardLee03/jedis-x
关注
HelloJimmy 写道
bert82503 写道
javaeyes 写道
这个太悲剧了,每一个请求都要try catch finally
Java代码
- poolConfig.setTestOnBorrow(true);
- poolConfig.setTestOnReturn(true);
这样就搞定问题了~
这样是OK,可是性能降低的厉害,每一个验证就是一个网络操做(ping/pong),效率低,并且对redis服务仍是一个负担,十分不建议!
你说得对,咱们在实际生产环境中使用时也是关闭了这两个属性,在外层统一包装一层 RedisService。
咱们基于 Jedis 定制实现的"Redis服务器异常(宕机)时自动摘除,恢复正常时自动添加"的功能。
https://github.com/EdwardLee03/jedis-x
bert82503 写道
javaeyes 写道
这个太悲剧了,每一个请求都要try catch finally
Java代码
- poolConfig.setTestOnBorrow(true);
- poolConfig.setTestOnReturn(true);
这样就搞定问题了~
这样是OK,可是性能降低的厉害,每一个验证就是一个网络操做(ping/pong),效率低,并且对redis服务仍是一个负担,十分不建议!
javaeyes 写道
这个太悲剧了,每一个请求都要try catch finally
Java代码
- poolConfig.setTestOnBorrow(true);
- poolConfig.setTestOnReturn(true);
这样就搞定问题了~
这实际上是jedis不支持多线程的缘由,在你调用del等方法时,在调用方法申明前加上关键字 synchronized就能够解决问题。好比delkey调用del方法,在delkey方法前加synchronized就能够了
你这个是什么版本的?2 .1.0没有你里面的代码了
这个太悲剧了,每一个请求都要try catch finally
Sweblish 写道
no,no,no.这跟redis没有关系,是jedis的问题。redis很棒的!