一次線上Redis類轉換異常排查引起的思考

以前同事反饋說線上遇到Redis反序列化異常問題,異常以下:redis

XxxClass1 cannot be cast to XxxClass2

已知信息以下:json

  • 該異常不是必現的,偶爾纔會出現;
  • 出現該異常後重啓應用或者過一會就行了;
  • 序列化協議使用了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" ]的列表,你能夠根據前一項識別出這是你發送的哪一個請求。lua

爲何Redis通訊方式並無採用相似於dubbo這種通訊方式呢,我的認爲有如下幾點:spa

  • 使用停等這種通訊方式實現簡單,而且協議字段儘量緊湊;
  • Redis都是內存操做,處理性能較強,停等協議不會形成客戶端等待時間較長;
  • 目前來看,通訊方式這塊不是Redis使用上的性能瓶頸,這一點很重要。

 

推薦閱讀:

 歡迎小夥伴掃描如下二維碼閱讀更多精彩好文。

相關文章
相關標籤/搜索