以前同事反饋說線上遇到Redis反序列化異常問題,異常以下:redis
XxxClass1 cannot be cast to XxxClass2
已知信息以下:json
由於偶爾出現,首先看了報異常那塊業務邏輯是否是有問題,看了一遍也發現什麼問題。看了下對應日誌,發現是在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" ]
的列表,你能夠根據前一項識別出這是你發送的哪一個請求。lua
爲何Redis通訊方式並無採用相似於dubbo這種通訊方式呢,我的認爲有如下幾點:spa
推薦閱讀:
歡迎小夥伴掃描如下二維碼閱讀更多精彩好文。