flag -- 詭異的memcache標記


引子   

     打從去年一路北漂,進入無人貨架行業,業務需求漫天飄,最近總算把工作都規划齊整。回望過去一年多的時間里,諸多東西值得整理,memcache就是其中一個。

   看到java的工資高些,隊伍中好些人都想學習java,美其名曰:技術多元化。奈何團隊中並沒有相關經驗的人,也深知大家殷切的期盼,所以,只能先擼起袖子自己干,看看書、看看博客、看看視頻,兩個小項目就上線了,除memcache以外,過程還算順利,於是就有了這篇文章。

       正值高考,突然感懷,當年的失利,讓自己更加堅強。

                                 

 

  

 

背景

  因為目前大部分項目都是.net core ,使用了memcache做為緩存服務器,首先就是 spring boot 里集成 memcache(使用 spymemcached 客戶端),集成過程就不說了,添加依賴,編寫幫助類,通過 @Configuration 注入就可以了。

      如果以為這樣就完了,那就沒有這個文章了,真正的故事才剛剛開始.....

問題

   配置完成后,就開始讀取已經有緩存,然后就提示:Failed to decompress data,如下圖,返回的內容就是null,但是在命令行能讀出來。另外,我們緩存的都是string,不會存在序列化的問題(一開始還真懷疑過java與.net  string 序列化,好傻好天真)

        

        因為一開始看上圖是 warn,就沒在意,於是開始了排除方法:

    1、java緩存,java 讀取正常。

    2、java緩存,.net 讀取正常。

        3、直接控制添加, java 讀取正常。

    4、更換java 客戶端為xmemcached

    5、還嘗試了很多.....甚至自己又部署幾個memcache 環境

        最后,得到一個結論:.net 緩存(使用的是 Enyim.Caching 客戶端),java 無法正常獲取。

    一個詭異的結論,咨詢別人時,都說:memcache 與語言無關!

   

失落的解決方案

   嘗試了很多次失敗后,決定讓他涼一涼。終究還是過不了內心的坎,感覺心中有一個東西,不得踏實,又不停的搜索,甚至還在阿里雲里發了工單,一開始也懷疑是阿里雲的服務器有問題(直接用的阿里的memcache),后來他們技術給我說了一堆

聽不太明白的內容,大概是要用 string 開頭的接口去讀取。這時已經明白,不是讀取不到,而是解碼出錯,返回null而已。

  再后來,就是一個叫flag 的參數引起了我的注意, 大意是說,不同客戶端在緩存時,用了不同的flag 來標記,說什么 java 的是flag 32,.net 的是2之類的,只要修改.net 為32就可以了。 反正聽起來就不靠譜,又到茫茫網絡中去搜索.....

  又過了兩天,感覺不能這么耗下去了,沒有其他方案,想着,還是修改下 Enyim.Caching 源碼試試看。接着 git clone 源碼,很快定位到 flag 的地方 在 DefaultTranscoder.cs  74行左右,生成flag的代碼如下

        public static uint TypeCodeToFlag(TypeCode code)
        {
            return 32;

            //return (uint)((int)code | 0x0100);  //修改前
        }

 

     其中,TypeCode 是系統中數據類型對應一個 enum,源碼如下,其中 String的值為 18,

namespace System
{
    //
    // Summary:
    //     Specifies the type of an object.
    [ComVisible(true)]
    public enum TypeCode
    {
        //
        // Summary:
        //     A null reference.
        Empty = 0,
        //
        // Summary:
        //     A general type representing any reference or value type not explicitly represented
        //     by another TypeCode.
        Object = 1,
        //
        // Summary:
        //     A database null (column) value.
        DBNull = 2,
        //
        // Summary:
        //     A simple type representing Boolean values of true or false.
        Boolean = 3,
        //
        // Summary:
        //     An integral type representing unsigned 16-bit integers with values between 0
        //     and 65535. The set of possible values for the System.TypeCode.Char type corresponds
        //     to the Unicode character set.
        Char = 4,
        //
        // Summary:
        //     An integral type representing signed 8-bit integers with values between -128
        //     and 127.
        SByte = 5,
        //
        // Summary:
        //     An integral type representing unsigned 8-bit integers with values between 0 and
        //     255.
        Byte = 6,
        //
        // Summary:
        //     An integral type representing signed 16-bit integers with values between -32768
        //     and 32767.
        Int16 = 7,
        //
        // Summary:
        //     An integral type representing unsigned 16-bit integers with values between 0
        //     and 65535.
        UInt16 = 8,
        //
        // Summary:
        //     An integral type representing signed 32-bit integers with values between -2147483648
        //     and 2147483647.
        Int32 = 9,
        //
        // Summary:
        //     An integral type representing unsigned 32-bit integers with values between 0
        //     and 4294967295.
        UInt32 = 10,
        //
        // Summary:
        //     An integral type representing signed 64-bit integers with values between -9223372036854775808
        //     and 9223372036854775807.
        Int64 = 11,
        //
        // Summary:
        //     An integral type representing unsigned 64-bit integers with values between 0
        //     and 18446744073709551615.
        UInt64 = 12,
        //
        // Summary:
        //     A floating point type representing values ranging from approximately 1.5 x 10
        //     -45 to 3.4 x 10 38 with a precision of 7 digits.
        Single = 13,
        //
        // Summary:
        //     A floating point type representing values ranging from approximately 5.0 x 10
        //     -324 to 1.7 x 10 308 with a precision of 15-16 digits.
        Double = 14,
        //
        // Summary:
        //     A simple type representing values ranging from 1.0 x 10 -28 to approximately
        //     7.9 x 10 28 with 28-29 significant digits.
        Decimal = 15,
        //
        // Summary:
        //     A type representing a date and time value.
        DateTime = 16,
        //
        // Summary:
        //     A sealed class type representing Unicode character strings.
        String = 18
    }
}
View Code

 

         

  根據之前得到的結果,要把 .net 客戶端的flag 設置成32,於是,直接返回32,代碼生成上傳,不試不知道,一試嚇一跳,竟然正常了。java 能正常返回緩存內容了,如下圖,正常打印了

  

       剛開始真是高興了足足10秒中,畢竟嘗試了很多次失敗,但轉念一想,現在所有的項目,都得去引用自己編譯的這個版本,以后如果 Enyim.Caching 升級了,我還得去重新下載、編譯,所有項目又要重新引用,想想就后怕!

       於是,第一次有了這樣的感覺:問題解決了,但是很多失落!弄完回到家,看我一臉無趣,媳婦還安慰說:“今天沒解決,明天再來,明天不行,后天再來,總會撥雲見日的!”

 

升級版解決方案

  缺陷的解決方案,一直縈繞心頭,揮之不去,於是,還是忍不住去查詢新的方案,還特意發起了一個博問,不過就 dudu 回復了,雖然沒有直接解決,也給了一些新的提示,並順利的看到了 spymemcached 的源碼。找到了

  解碼的類 SerializingTranscoder.java ,對於 String 並未做處理,也沒有解碼的問題。 解碼部分源碼如下,可以看到,對於 String是直接調用  decodeString

public Object decode(CachedData d) {
    byte[] data = d.getData();
    Object rv = null;
    if ((d.getFlags() & COMPRESSED) != 0) {
      data = decompress(d.getData());
    }
    int flags = d.getFlags() & SPECIAL_MASK;
    if ((d.getFlags() & SERIALIZED) != 0 && data != null) {
      rv = deserialize(data);
    } else if (flags != 0 && data != null) {
      switch (flags) {
      case SPECIAL_BOOLEAN:
        rv = Boolean.valueOf(tu.decodeBoolean(data));
        break;
      case SPECIAL_INT:
        rv = Integer.valueOf(tu.decodeInt(data));
        break;
      case SPECIAL_LONG:
        rv = Long.valueOf(tu.decodeLong(data));
        break;
      case SPECIAL_DATE:
        rv = new Date(tu.decodeLong(data));
        break;
      case SPECIAL_BYTE:
        rv = Byte.valueOf(tu.decodeByte(data));
        break;
      case SPECIAL_FLOAT:
        rv = new Float(Float.intBitsToFloat(tu.decodeInt(data)));
        break;
      case SPECIAL_DOUBLE:
        rv = new Double(Double.longBitsToDouble(tu.decodeLong(data)));
        break;
      case SPECIAL_BYTEARRAY:
        rv = data;
        break;
      default:
        getLogger().warn("Undecodeable with flags %x", flags);
      }
    } else {
      rv = decodeString(data);
    }
    return rv;
  }
View Code

 

 

     decodeString 代碼如下,可見並無特殊處理

  /**
   * Decode the string with the current character set.
   */
  protected String decodeString(byte[] data) {
    String rv = null;
    try {
      if (data != null) {
        rv = new String(data, charset);
      }
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
    return rv;
  }

 

     

      再細看 SerializingTranscoder.java 的處理邏輯,在解碼之前,有壓縮標志,以及 decompress() 方法, 這個方法在 BaseSerializingTranscoder.java 中,源代碼如下,正好有,有一個 catch 會輸出,最早看到的錯誤信息:Failed to decompress data

  getLogger().warn("Failed to decompress data", e); 找到了問題的發生地兒,離解決方案就不遠了。 第一現場很重要。

  /**
   * Get the object represented by the given serialized bytes.
   */
  protected Object deserialize(byte[] in) {
    Object rv=null;
    ByteArrayInputStream bis = null;
    ObjectInputStream is = null;
    try {
      if(in != null) {
        bis=new ByteArrayInputStream(in);
        is=new ObjectInputStream(bis);
        rv=is.readObject();
        is.close();
        bis.close();
      }
    } catch (IOException e) {
      getLogger().warn("Caught IOException decoding %d bytes of data",
          in == null ? 0 : in.length, e);
    } catch (ClassNotFoundException e) {
      getLogger().warn("Caught CNFE decoding %d bytes of data",
          in == null ? 0 : in.length, e);
    } finally {
      CloseUtil.close(is);
      CloseUtil.close(bis);
    }
    return rv;
  }
View Code

 

     

      既然問題出在“解壓”這里,那為什么我把 flag 設置成32就可以了呢,再看源碼,判斷是否解壓的如下:

      static final int COMPRESSED = 2;

 if ((d.getFlags() & COMPRESSED) != 0) {
  data = decompress(d.getData());
 }

.net 里默認是 18 | 0x0100 = 274
 274 &  2 = 2  不等於0,會去解壓,然后出錯了。

32 & 2 =0, 不解壓,正常。

這里其實驗證了,flag與客戶端無關。壓縮標志與數據類型有關。

   問題已經明確了,只要程序不走解壓就是正常的,並且,這些參數,都是類內部的狀態,外面無法修改,那可以擴展嗎?使用自己的解碼類來實現,肯定是可以的,看 SerializingTranscoder 與 BaseSerializingTranscoder 的繼承關系就知道,

     再看  get 方法 memcachedClient.get(String key, Transcoder<T> tc),支持自定義  Transcoder, 接下來,問題就簡單了,自定義一個 Transcoder 繼承  BaseSerializingTranscoder 實現 Transcoder,不用解壓,直接解碼。

     最后,其實,我只是在  SerializingTranscoder  基礎上,把 static final int COMPRESSED = 0,就可以了,都不解壓。 獲取代碼如下

HMSerializingTranscoder transcoder = new HMSerializingTranscoder();
return memcachedClient.get(key,transcoder);

 

 

結語

  分析到此,問題明了,方案明確,水到渠成,問題解決了。在不修改第三方源碼的基礎上,通過擴展解決了,也不用擔心第三方升級的問題了,這樣就比第一種別扭的方案舒服多了。

  第一次感受到閱讀源碼,與深究一個問題的帶來的收獲 -- 杠杠的

 

   成為一名優秀的程序員!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM