引子
打從去年一路北漂,進入無人貨架行業,業務需求漫天飄,最近總算把工作都規劃齊整。回望過去一年多的時間裡,諸多東西值得整理,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;
}
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);
結語
分析到此,問題明了,方案明确,水到渠成,問題解決了。在不修改第三方源碼的基礎上,通過擴充解決了,也不用擔心第三方更新的問題了,這樣就比第一種别扭的方案舒服多了。
第一次感受到閱讀源碼,與深究一個問題的帶來的收獲 -- 杠杠的
成為一名優秀的程式員!
版權聲明:
作者:J²
編輯:
妞妞 妞妞首頁出處:http://www.cnblogs.com/jijunjian/
本文版權歸作者和部落格園共有,歡迎轉載,大家好,才是真的好!
#top