這個是今天發現一個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很棒的!