一次线上Redis类转换异常排查引发的思考
之前同事反饋說線上遇到Redis反序列化異常問題,異常如下:
XxxClass1 cannot be cast to XxxClass2已知信息如下:
- 該異常不是必現(xiàn)的,偶爾才會出現(xiàn);
- 出現(xiàn)該異常后重啟應用或者過一會就好了;
- 序列化協(xié)議使用了hessian。
因為偶爾出現(xiàn),首先看了報異常那塊業(yè)務邏輯是不是有問題,看了一遍也發(fā)現(xiàn)什么問題。看了下對應日志,發(fā)現(xiàn)是在Redis讀超時之后才出現(xiàn)的該異常,因此懷疑redis client操作邏輯那塊導致的(公司架構組對redis做了一層封裝),發(fā)現(xiàn)獲取/釋放redis連接如下代碼:
1 try { 2 jedis = jedisPool.getResource(); 3 // jedis業(yè)務讀寫操作 4 } catch (Exception e) { 5 // 異常處理 6 } finally { 7 if (jedis != null) { 8 // 歸還給連接池 9 jedisPool.returnResourceObject(jedis); 10 } 11 }初步認定原因為:發(fā)生了讀寫超時的連接,直接歸還給連接池,下次使用該連接時讀取到了上一次Redis返回的數(shù)據(jù)。因此本地驗證下,示例代碼如下:
1 @Data 2 @NoArgsConstructor 3 @AllArgsConstructor 4 static class Person implements Serializable { 5 private String name; 6 private int age; 7 } 8 @Data 9 @NoArgsConstructor 10 @AllArgsConstructor 11 static class Dog implements Serializable { 12 private String name; 13 } 14 15 public static void main(String[] args) throws Exception { 16 JedisPoolConfig config = new JedisPoolConfig(); 17 config.setMaxTotal(1); 18 JedisPool jedisPool = new JedisPool(config, "192.168.193.133", 6379, 2000, "123456"); 19 20 Jedis jedis = jedisPool.getResource(); 21 jedis.set("key1".getBytes(), serialize(new Person("luoxn28", 26))); 22 jedis.set("key2".getBytes(), serialize(new Dog("tom"))); 23 jedisPool.returnResourceObject(jedis); 24 25 try { 26 jedis = jedisPool.getResource(); 27 Person person = deserialize(jedis.get("key1".getBytes()), Person.class); 28 System.out.println(person); 29 } catch (Exception e) { 30 // 發(fā)生了異常之后,未對該連接做任何處理 31 System.out.println(e.getMessage()); 32 } finally { 33 if (jedis != null) { 34 jedisPool.returnResourceObject(jedis); 35 } 36 } 37 38 try { 39 jedis = jedisPool.getResource(); 40 Dog dog = deserialize(jedis.get("key2".getBytes()), Dog.class); 41 System.out.println(dog); 42 } catch (Exception e) { 43 System.out.println(e.getMessage()); 44 } finally { 45 if (jedis != null) { 46 jedisPool.returnResourceObject(jedis); 47 } 48 } 49 }連接超時時間設置2000ms,為了方便測試,可以在redis服務器上使用gdb命令斷住redis進程(如果redis部署在Linux系統(tǒng)上的話,還可以使用iptable命令在防火墻禁止某個回包),比如在執(zhí)行?jedis.get("key1".getBytes()?代碼前,對redis進程使用gdb命令斷住,那么就會導致讀取超時,然后就會觸發(fā)如下異常:
Person cannot be cast to Dog既然已經知道了該問題原因并且本地復現(xiàn)了該問題,對應解決方案是,在發(fā)生異常時歸還給連接池時關閉該連接即可(jedis.close內部已經做了判斷),代碼如下:
1 try { 2 jedis = jedisPool.getResource(); 3 // jedis業(yè)務讀寫操作 4 } catch (Exception e) { 5 // 異常處理 6 } finally { 7 if (jedis != null) { 8 // 歸還給連接池 9 jedis.close(); 10 } 11 }至此,該問題解決。注意,因為使用了hessian序列化(其包含了類型信息,類似的有Java本身序列化機制),所有會報類轉換異常;如果使用了json序列化(其只包含對象屬性信息),反序列化時不會報異常,只不過因為不同類的屬性不同,會導致反序列化后的對象屬性為空或者屬性值混亂,使用時會導致問題,并且這種問題因為沒有報異常所以更不容易發(fā)現(xiàn)。
?
既然說到了Redis的連接,要知道的是,Redis基于RESP(Redis Serialization Protocol)協(xié)議來通信,并且通信方式是停等方式,也就說一次通信獨占一個連接直到client讀取到返回結果之后才能釋放該連接讓其他線程使用。小伙伴們可以思考一下,Redis通信能否像dubbo那樣使用單連接+序列號(標識單次通信)通信方式呢?理論上是可以的,不過由于RESP協(xié)議中并沒有一個"序列號"的字段,所以直接靠原生的通信方法來實現(xiàn)是不現(xiàn)實的。不過我們可以通過echo命令傳遞并返回"序列號"+正常的讀寫方式來實現(xiàn),這里要保證二者執(zhí)行的原子性,可以通過lua腳本或者事務來實現(xiàn),事務方式如下:
MULTI ECHO "唯一序列號" GET key1 EXEC然后客戶端收到的結果是一個?[ "唯一序列號", "value1" ]的列表,你可以根據(jù)前一項識別出這是你發(fā)送的哪個請求。
為什么Redis通信方式并沒有采用類似于dubbo這種通信方式呢,個人認為有以下幾點:
- 使用停等這種通信方式實現(xiàn)簡單,并且協(xié)議字段盡可能緊湊;
- Redis都是內存操作,處理性能較強,停等協(xié)議不會造成客戶端等待時間較長;
- 目前來看,通信方式這塊不是Redis使用上的性能瓶頸,這一點很重要。
?
推薦閱讀:
- 別再問我ConcurrentHashMap了
-
分布式鎖設計與實現(xiàn)
-
ConcurrentHashMap竟然也有死循環(huán)問題?
-
你的ThreadLocal線程安全么
?歡迎小伙伴掃描以下二維碼精彩好文。
?
轉載于:https://www.cnblogs.com/luoxn28/p/11075958.html
總結
以上是生活随笔為你收集整理的一次线上Redis类转换异常排查引发的思考的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GridSearchCV和Randomi
- 下一篇: 安全测试的一些漏洞和测试方法