之前同事反饋說線上遇到Redis反序列化異常問題,異常如下:
XxxClass1 cannot be cast to XxxClass2
已知資訊如下:
- 該異常不是必現的,偶爾才會出現;
- 出現該異常後重啟應用或者過一會就好了;
- 序列化協議使用了hessian。
因為偶爾出現,首先看了報異常那塊業務邏輯是不是有問題,看了一遍也發現什麼問題。看了下對應日誌,發現是在Redis讀超時之後才出現的該異常,因此懷疑redis client操作邏輯那塊導致的(公司架構組對redis做了一層封裝),發現獲取/釋放redis連線如下程式碼:
1 try { 2 jedis = jedisPool.getResource(); 3 // jedis業務讀寫操作 4 } catch (Exception e) { 5 // 異常處理 6 } finally { 7 if (jedis != null) { 8 // 歸還給連線池 9 jedisPool.returnResourceObject(jedis); 10 } 11 }
初步認定原因為:發生了讀寫超時的連線,直接歸還給連線池,下次使用該連線時讀取到了上一次Redis返回的資料。因此本地驗證下,示例程式碼如下:
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 // 發生了異常之後,未對該連線做任何處理 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系統上的話,還可以使用iptable命令在防火牆禁止某個回包),比如在執行 jedis.get("key1".getBytes()
程式碼前,對redis程式使用gdb命令斷住,那麼就會導致讀取超時,然後就會觸發如下異常:
Person cannot be cast to Dog
既然已經知道了該問題原因並且本地復現了該問題,對應解決方案是,在發生異常時歸還給連線池時關閉該連線即可(jedis.close內部已經做了判斷),程式碼如下:
1 try { 2 jedis = jedisPool.getResource(); 3 // jedis業務讀寫操作 4 } catch (Exception e) { 5 // 異常處理 6 } finally { 7 if (jedis != null) { 8 // 歸還給連線池 9 jedis.close(); 10 } 11 }
至此,該問題解決。注意,因為使用了hessian序列化(其包含了型別資訊,類似的有Java本身序列化機制),所有會報類轉換異常;如果使用了json序列化(其只包含物件屬性資訊),反序列化時不會報異常,只不過因為不同類的屬性不同,會導致反序列化後的物件屬性為空或者屬性值混亂,使用時會導致問題,並且這種問題因為沒有報異常所以更不容易發現。
既然說到了Redis的連線,要知道的是,Redis基於RESP(Redis Serialization Protocol)
協議來通訊,並且通訊方式是停等方式,也就說一次通訊獨佔一個連線直到client讀取到返回結果之後才能釋放該連線讓其他執行緒使用。小夥伴們可以思考一下,Redis通訊能否像dubbo那樣使用單連線+序列號(標識單次通訊)
通訊方式呢?理論上是可以的,不過由於RESP協議中並沒有一個"序列號"的欄位,所以直接靠原生的通訊方法來實現是不現實的。不過我們可以通過echo命令傳遞並返回"序列號"+正常的讀寫方式來實現,這裡要保證二者執行的原子性,可以通過lua指令碼或者事務來實現,事務方式如下:
MULTI ECHO "唯一序列號" GET key1 EXEC
然後客戶端收到的結果是一個 [ "唯一序列號", "value1" ]
的列表,你可以根據前一項識別出這是你傳送的哪個請求。
為什麼Redis通訊方式並沒有採用類似於dubbo這種通訊方式呢,個人認為有以下幾點:
- 使用停等這種通訊方式實現簡單,並且協議欄位儘可能緊湊;
- Redis都是記憶體操作,處理效能較強,停等協議不會造成客戶端等待時間較長;
- 目前來看,通訊方式這塊不是Redis使用上的效能瓶頸,這一點很重要。
推薦閱讀:
歡迎小夥伴掃描以下二維碼閱讀更多精彩好文。