引子
打從去年一路北漂,進入無人貨架行業,業務需求漫天飄,最近總算把工作都規划齊整。回望過去一年多的時間里,諸多東西值得整理,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 } }
根據之前得到的結果,要把 .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; }
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; }
既然問題出在“解壓”這里,那為什么我把 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);
結語
分析到此,問題明了,方案明確,水到渠成,問題解決了。在不修改第三方源碼的基礎上,通過擴展解決了,也不用擔心第三方升級的問題了,這樣就比第一種別扭的方案舒服多了。
第一次感受到閱讀源碼,與深究一個問題的帶來的收獲 -- 杠杠的
成為一名優秀的程序員!