在項目中用到了memcached 做緩存,在實際應用中發現spymemcached 客戶端在網絡狀態比較差是(大概延遲60ms左右)時的讀取超時現象比較嚴重,而且造成應用的內存被消耗盡了(用jmap 查看過,發現memcache 的客戶端用到的異步線程類佔用很大的內存,估計是由於超時不斷創建的緣故)。於是便換了http://www.whalin.com/memcached 這個的客戶端。。在更換過程中也出現了一些問題,由於是晚上才發現,害得我晚上因爲這事睡都睡不着。。。想好一些調試的辦法,然後在明早進行測試。。
問題一:當key 爲中文的時候,value取錯了。
遇到這個問題時,由於對業務的理解,排除了是鍵重複的問題。由於是更換客戶端後纔出現的問題,也首選反應到是客戶端的問題,但爲了確定下,想在服務器端進行驗證,然而在SecureCRT終端中輸入不了中文,沒辦法在memcache服務器端進行測試。。在上線過程中也在測試機上測試,發現測試機上不會出現這個問題。。隨後也反應到,中文經常是會涉及編碼問題。。然後便查看了兩邊的操作系統默認編碼,發現確實不一樣。。線上服務器是默認的LANG=C,測試機是LANG=en_US.UTF-8。然後在測試機上也調成LANG=C,發現問題重現,大喜。。之後更加確定這個問題出現的原因。。然後便是對whalin memcache客戶端源碼進行研究了。
private Object get(String cmd, String key, Integer hashCode, boolean asString) {
if (key == null) {
log.error("key is null for get()");
return null;
}
try {
//注意這裏,可以對key進行URLEncode
key = sanitizeKey(key);
} catch (UnsupportedEncodingException e) {
log.error("failed to sanitize your key!", e);
return null;
}
// get SockIO obj using cache key
SchoonerSockIO sock = pool.getSock(key, hashCode);
if (sock == null) {
if (errorHandler != null)
errorHandler.handleErrorOnGet(this, new IOException("no socket to server available"), key);
return null;
}
String cmdLine = cmd + " " + key;
try {
sock.writeBuf.clear();
//cmdLine.getBytes() 這個是出錯的關鍵
sock.writeBuf.put(cmdLine.getBytes());
sock.writeBuf.put(B_RETURN);
// write buffer to server
sock.flush();
//......
}
//可以看出這裏對key做了URLEncode ,當然這裏要進行設定纔會
private String sanitizeKey(String key) throws UnsupportedEncodingException {
return (sanitizeKeys) ? URLEncoder.encode(key, "UTF-8") : key;
}
由於我把key的編碼給關了mcc1.setSanitizeKeys(false); 所以對中文不會進行URLEncode編碼。。然後查看了Java API 發現了 cmdLine.getBytes()方法的描述是:
使用平臺的默認字符集將此 String 編碼爲 byte 序列,並將結果存儲到一個新的 byte 數組中。
當此字符串不能使用默認的字符集編碼時,此方法的行爲沒有指定。如果需要對編碼過程進行更多控制,則應該使用 CharsetEncoder 類。
關鍵字在於平臺默認編碼。。假如當用戶輸入中文時,是utf8編碼,然後在getBytes方法的時候,不是用utf8解碼,那就會出現問題了。我也在memcache 客戶端中加入了一些調試代碼後,再進行測試,發現中文打印的是“??”,每個中文解碼由於解碼不對稱問題都統一轉成一樣的二進制編碼。。。這就是原因所在了。。
解決辦法:mcc1.setSanitizeKeys(true).但這種解決辦法的缺點是由於對key做了URLEncode編碼,在memecache 服務器中測試就比較困難了,因爲我們也要首先把key轉成URLEncode編碼,然後在測試。
問題二:key中間出現空字符串,客戶端一直未結束
這個問題是在問題一的測試中突然發現的。。經代碼調試發現阻塞在下面的方法中。。
/** * Constructor. * * @param sock * {@link SchoonerSockIO}, read from this socket. * @param limit * limited length to read from specified socket. * @throws IOException * error happened in reading. */ public SockInputStream(final SchoonerSockIO sock, int limit) throws IOException { this.sock = sock; willRead(limit); sock.readBuf.clear(); //阻塞在這裏。。這個通道處於阻塞模式 sock.getChannel().read(sock.readBuf); sock.readBuf.flip(); }
SocketChannel.read(ByteBuffer des)的JavaDoc的描述是
public abstract int read(ByteBuffer dst)
throws IOException
將字節序列從此通道中讀入給定的緩衝區。
嘗試最多從該通道中讀取 r 個字節,其中 r 是調用此方法時緩衝區中剩餘的字節數,即 dst.remaining()。
假定讀取的字節序列長度爲 n,其中 0 <= n <= r。此字節序列將被傳輸到緩衝區中,序列中的第一個字節位於索引 p 處,最後一個字節則位於索引 p + n - 1 處,其中 p 是調用此方法時緩衝區的位置。返回時,該緩衝區的位置將等於 p + n;其限制不會更改。
讀取操作可能不填充緩衝區,實際上它可能根本不讀取任何字節。是否如此執行取決於通道的性質和狀態。例如,處於非阻塞模式的套接字通道只能從該套接字的輸入緩衝區中讀取立即可用的字節;類似地,文件通道只能讀取文件中剩餘的字節。但是可以保證,如果某個通道處於阻塞模式,並且緩衝區中至少剩餘一個字節,則在讀取至少一個字節之前將阻塞此方法。
可在任意時間調用此方法。但是如果另一個線程已經在此通道上發起了一個讀取操作,則在該操作完成前此方法的調用被阻塞。
然後我在初始化memcache 這個客戶端的時候已經設定了超時時間爲3秒pool.setSocketTO(3000),但爲什麼到了3秒後依然沒有報超時錯誤,這個我也納悶,暫時也想不到原因。。不知道誰知道不。。
在調試中也發現了出現這個的讀取一直阻塞的原因是memcache 命令的組裝未檢查key中帶有特殊字符,下面是源碼中key的組裝
// build command
StringBuilder command = new StringBuilder("sync ").append(key);
command.append("\r\n");
memcache 服務器端的協議也表明了,key 中不能有製表符和空白字符,並且長度不能超高250個字符。不然服務器端不會響應任何數據。。這個我也確實試了下,發現真沒反應。。對memcache 服務器端表示不解。。
解決辦法:和問題一的一樣mcc1.setSanitizeKeys(true),對key進行URLEncode編碼。。
問題三:批量獲取接口取值和單個接口取值不一致
這個問題還是上去後不就發現的。。批量接口(getMutil())的應用比較少,所以才遲幾天發現。。初始以爲是我的程序問題,擔心了一場,後來慢慢調試發現一些規律,就是key的Encoder的問題的,在getMutil()裏竟然不會去判斷key是否要編碼,都按照未編碼的key進行獲取,當然獲取不了了,暈死。。源碼就不發了,想看的自己可以去下載看下,另外批量獲取接口是用NIO來實現的和單個獲取的方法不一樣,暫時也沒去深究了。。。沒想到的是用了這麼久的客戶端,竟然有這樣的bug存在。。不過在官網上還是挺給力的,最近才更新了2.5.3版本解決了這個問題。。
解決辦法:更換2.5.3版本包。