本文摘自:http://my.oschina.net/astute/blog/93492, 感謝作者
一、簡介
spymemcached 是一個 memcache 的客戶端, 使用 NIO 實現。
分析 spymemcached 需要了解 NIO,memcached使用,memcached協議,參考資料中列出了有用的資源連接。
NIO是New I/O的縮寫,Java裏邊大家一般稱爲異步IO,實際上對應Linux系統編程中的事件驅動IO(event-driven IO),是對 epoll 的封裝。其它的IO模型還包括同步,阻塞,非阻塞,多路複用(select,poll)。阻塞/非阻塞是 fd 的屬性,同步會跟阻塞配合,這樣的應用會一直 sleep,直到IO完成被內核喚醒;同步非阻塞的話,第一次讀取時,如果沒有數據,應用線程會立刻返回,但是應用需要確定以什麼樣的策略進行後面的系統調用,如果是簡單的while循環會導致CPU 100%,複雜的類似自旋的策略增加了應用編程的難度,因此同步非阻塞很少使用。多路複用是Linux早期的一個進程監控多個fd的方式,性能比較低,每次調用涉及3次循環遍歷,具體分析見 http://my.oschina.net/astute/blog/92433 。event-driven IO,應用註冊 感興趣的socket IO事件(READ,WRITE),調用wait開始sleep,當條件成立時,如數據到達(可讀),寫緩衝區可用(可寫),內核喚醒應用線程,應用線程根據得到的socket執行同步的調用讀/寫 數據。
二、協議簡介
memcachded服務器和客戶端之間採用 TCP 的方式通信,自定義了一套字節流的格式。本文分析的文本協議的構建和其它文本協議類似,mc裏面分成命令行和數據塊行,命令行裏指明數據塊的字節數目,命令行和數據塊後都跟隨\r\n。重要的一點是服務器在讀取數據塊時是根據命令行裏指定的字節數目,因此數據塊中含有\r或\n並不影響服務器讀塊操作。數據塊後必須跟隨\r\n。
存儲命令
發送
<command name> <key> <flags> <exptime> <bytes> [noreply]\r\n
cas <key> <flags> <exptime> <bytes> <cas unique> [noreply]\r\n
<data block>\r\n
command name = "set", "add", "replace", "append" or "prepend"
flags - 32位整數 server並不操作這個數據 get時返回給客戶端
exptime - 過期時間,可以是unix時間戳或偏移量,偏移量的話最大爲30*24*60*60, 超過這個值,服務器會認爲是unix時間戳
bytes - 數據塊字節的個數
響應
<data block>\r\n
STORED\r\n - 成功
NOT_STORED\r\n - add或replace命令沒有滿足條件
EXISTS\r\n - cas命令 表明item已經被修改
NOT_FOUND\r\n - cas命令 item不存在
獲取命令
發送
get <key>*\r\n
gets <key>*\r\n
<key>* - 空格分割的一個或多個字符串
響應
VALUE <key> <flags> <bytes> [<cas unique>]\r\n
<data block>\r\n
VALUE <key> <flags> <bytes> [<cas unique>]\r\n
<data block>\r\n
END\r\n
本文以 get 操作爲例;key = someKey value=abcdABC中文
以字節流的形式最終發送的數據
[103, 101, 116, 32, 115, 111, 109, 101, 75, 101, 121, 13, 10, 0]
103 101 116 - "get"
32 - "" 空格
115 111 109 101 75 101 121 - someKey
13 10 - \r\n
接收到的數據
VALUE someKey 0 13
61 62 63 64 41 42 43 E4 B8 AD E6 96 87\r\n
END\r\n
刪除命令
發送
delete <key> [noreply]\r\n
響應
DELETED\r\n - 成功刪除
NOT_FOUND\r\n - 刪除的條目不存在
其它命令
詳見參考資料 mc 協議
三、spymemcached中的重要對象
簡介
spy是mc的客戶端,因此spy中所有對象需要基於它要完成的 功能 和 到mc服務器的通信協議來進行設計。最重要的MemcachedClient表示mc集羣的client,應用中單例即可。spy中的每一個mc節點,用MemcachedNode表示,這個對象內部含有一個channel,網絡連接到mc節點。要根據key的哈希值查找某個mc節點,spy中使用NodeLocator,默認locator是ArrayModNodeLocator,這個對象內部含有所有的MemcachedNode,spy使用的hash算法都在對象DefaultHashAlgorithm中,默認使用NATIVE_HASH,也就是String.hashCode()。locator和client中間還有一個對象,叫MemcachedConnection ,它表示到mc集羣的連接,內部持有locator。clent內部持有MemcachedConnection(mconn)。spy使用NIO實現,因此有一個selector,這個對象存在於mconn中。要和服務器進行各種操作的通信,協議數據發送,數據解析,spy中抽象爲Operation,文本協議的get操作最終實現爲net.spy.memcached.protocol.ascii.GetOperationImpl。爲了實現工作線程和IO線程之間的調度,spy抽象出了一個 GetFuture,內部持有一個OperationFuture。
TranscodeService執行字節數據和對象之間的轉換,spy中實現方式爲任務隊列+線程池,這個對象的實例在client中。
對象詳解
SpyObject - spy中的基類 定義 Logger
MemcachedConnection - 表示到多臺 mc 節點的連接
MemcachedConnection - 詳細屬性
shouldOptimize - 是否需要優化多個連續的get操作 --> gets 默認true
addedQueue - 用來記錄排隊到節點的操作
selector - 監控到多個 mc 服務器的讀寫事件
locator - 定位某個 mc 服務器
GetFuture - 前端線程和工作線程交互的對象
--> OperationFuture
ConnectionFactory - 創建 MemcachedConnection 實例;創建操作隊列;創建 OperationFactory;制定 Hash 算法。
DefaultConnectionFactory - 默認連接工廠
DefaultHashAlgorithm - Hash算法的實現類
MemcachedNode - 定義到 單個memcached 服務器的連接
TCPMemcachedNodeImpl -
AsciiMemcachedNodeImpl -
BinaryMemcachedNodeImpl -
TCPMemcachedNodeImpl - 重要屬性
socketAddress - 服務器地址
rbuf - 讀緩衝區 默認大小 16384
wbuf - 寫緩衝區 默認大小 16384
writeQ - 寫隊列
readQ - 讀隊列
inputQueue - 輸入隊列 memcachclient添加操作時先添加到 inputQueue中
opQueueMaxBlockTime - 操作的最大阻塞時間 默認10秒
reconnectAttempt - 重連嘗試次數 volatile
channel - socket 通道
toWrite - 要向socket發送的字節數
optimizedOp - 優化後的Operation 實現類是OptimizedGetImpl
sk - channel註冊到selector後的key
shouldAuth - 是否需要認證 默認 false
authLatch - 認證需要的Latch
reconnectBlocked -
defaultOpTimeout - 操作默認超時時間 默認值 2.5秒
continuousTimeout - 連續超時次數
opFact - 操作工廠
MemcachedClient - 重要屬性
mconn - MemcachedConnection
opFact - 操作工廠
transcoder - 解碼器
tcService - 解碼線程池服務
connFactory - 連接工廠
Operation - 所有操作的基本接口
BaseOperationImpl
OperationImpl
BaseGetOpImpl - initialize 協議解析 構建緩衝區
GetOperationImpl
OperationFactory - 爲協議構建操作 比如生成 GetOperation
BaseOperationFactory
AsciiOperationFactory - 文本協議的操作工廠 默認的操作工廠
BinaryOperationFactory - 二進制協議的操作工廠
OperationFactory - 根據 protocol handlers 構建操作
BaseOperationFactory
AsciiOperationFactory - 支持 ascii protocol
BinaryOperationFactory - 支持 binary operations
NodeLocator - 根據 key hash 值查找節點
ArrayModNodeLocator - hash 值和節點列表長度取模,作爲下標,簡單的數組查詢
KetamaNodeLocator - Ketama一致性hash的實現
Transcoder - 對象和字節數組之間的轉換接口
BaseSerializingTranscoder
SerializingTranscoder - 默認的transcoder
TranscodeService - 異步的解碼服務,含有一個線程池
FailureMode - node失效的模式
Redistribute - 節點失效後移動到下一個有效的節點 默認模式
Retry - 重試失效節點 直至恢復
Cancel - 取消操作
四、整體流程
初始化
客戶端執行new MemcachedClient(new InetSocketAddress("192.168.56.101", 11211))。初始化 MemcachedClient,內部初始化MemcachedConnection,創建selector,註冊channel到selector,啓動IO線程。
線程模型
初始化完成後,把監聽mc節點事件的線程,也就是調用select的線程,稱爲IO線程;應用執行 c.get("someKey"),把應用所在的線程稱爲工作線程。工作線程通常由tomcat啓動,負責創建操作,加入節點的操作隊列,工作線程通常有多個;IO線程負責從隊列中拿到操作,執行操作。
工作線程
工作線程最終會調用asyncGet,方法內部會創建CountDownLatch(1), GetFuture,GetOperationImpl(持有一個內部類,工作線程執行完成後,最終會調用 latch.countDown()),選擇mc節點,操作op初始化(生成寫緩衝區),把op放入節點等待隊列inputQueue中,同時會把當前節點放入mc連接(mconn)的addedQueue屬性中,最後喚醒selector。最終工作線程在latch上等待(默認超時2.5秒)IO線程的執行結果。
IO線程
IO線程被喚醒後
1、handleInputQueue()。移動Operation從inputQueue到writeQ中。對添加到addedQueue中的每一個MemcachedNode分別進行處理。這個函數會處理所有節點上的所有操作,全部發送到mc服務器(之前節點上就有寫操作的才這麼處理,否則只是註冊寫事件)。
2、循環過程中,如果當前node中沒有寫操作,則判斷writeQ,readQ中有操作,在SK上註冊讀/寫事件;如果有寫操作,需要執行handleWrites函數。這個函數內部首先做的是、填充緩衝區fillWriteBuffer():從writeQ中取出一個可寫的操作(remove掉取消的和超時的),改變操作的狀態爲WRITING,把操作的數據複製到寫緩衝區(寫緩衝區默認16K,操作的字節數從十幾字節到1M,這個地方有複雜的處理,後面會詳細分析,現在只考慮簡單情況),複製完成後把操作狀態變爲READING,從writeQ中remove當前操作,把操作add到readQ當中,這個地方會再去複製pending的操作;‚、發送寫緩衝區的內容,全部發送完成後,會再次去填充緩衝區fillWriteBuffer()(比如說一個大的命令,一個緩衝區不夠)。循環,直到所有的寫操作都處理完。ƒ、判斷writeQ,readQ是否有操作,更新sk註冊的讀寫事件。get操作的話,現在已經註冊了讀事件。
3、selector.select()
4、數據到達時,執行handleIO(sk),處理讀事件;執行channel.read(rbuf);執行readFromBuffer(),解析數據,讀取到END\r\n將操作狀態置爲COMPLETE。
五、初始化詳細流程
1、默認連接工廠爲 DefaultConnectionFactory。接着創建TranscodeService(解碼的線程池,默認線程最多爲10),創建AsciiOperationFactory(支持ascii協議的操作工廠,負責生成各種操作,比如 GetOperationImpl),創建MemcachedConnection,設置操作超時時間(默認2.5秒)。
2、DefaultConnectionFactory創建MemcachedConnection詳細過程:創建reconnectQueue,addedQueue,設置shouldOptimize爲true,設置maxDelay爲30秒,設置opFact,設置timeoutExceptionThreshold爲1000(超過這個值,關閉到 mc node 的連接),打開 Selector,創建nodesToShutdown,設置bufSize爲16384字節,創建到每個node的 MemcachedNode(默認是AsciiMemcachedNodeImpl,這一步創建SocketChannel,連接到mc節點,註冊到selector,設置sk爲剛註冊得到的SelectionKey),最後啓動 MemcachedConnection 線程,進入事件處理的循環代碼
while(running) handleIO()。
六、核心流程代碼
1、工作線程
一切從工作線程調用 c.get("someKey") 方法開始
基本流程是:創建操作(Operation),操作初始化,查找節點,把操作加入節點的等待隊列,喚醒IO線程,然後工作線程在Future上等待IO線程的執行結果
2 |
return asyncGet(key,
tc).get( 2500 ,
TimeUnit.MILLISECONDS) |
04 |
public <T>
GetFuture<T> asyncGet( final String
key, final Transcoder<T>
tc) { |
05 |
final CountDownLatch
latch = new CountDownLatch( 1 ); |
06 |
final GetFuture<T>
rv = new GetFuture<T>(latch,
operationTimeout, key); |
07 |
Operation
op = opFact.get(key, new GetOperation.Callback()
{ |
08 |
private Future<T>
val = null ; |
09 |
public void receivedStatus(OperationStatus
status) { |
12 |
public void gotData(String
k, int flags, byte []
data) { |
13 |
val
= tcService.decode(tc, new CachedData(flags,
data, tc.getMaxSize())); |
15 |
public void complete()
{ |
20 |
mconn.enqueueOperation(key,
op); |
03 |
protected void addOperation( final String
key, final Operation
o) { |
04 |
MemcachedNode
placeIn = null ; |
05 |
MemcachedNode
primary = locator.getPrimary(key); |
06 |
if (primary.isActive()
|| failureMode == FailureMode.Retry) { |
08 |
} else if (failureMode
== FailureMode.Cancel) { |
11 |
for (Iterator<MemcachedNode>
i = locator.getSequence(key); placeIn == null |
13 |
MemcachedNode
n = i.next(); |
18 |
if (placeIn
== null )
{ |
22 |
if (placeIn
!= null )
{ |
23 |
addOperation(placeIn,
o); |
2 |
protected void addOperation( final MemcachedNode
node, final Operation
o) { |
3 |
o.setHandlingNode(node); |
6 |
addedQueue.offer(node); |
7 |
Selector
s = selector.wakeup(); |
工作線程和IO線程之間傳遞的Future對象,結構如下
GetFuture ---> OperationFuture ---> latch
---> 表示依賴關係
02 |
public T
get( long duration,
TimeUnit units) { |
03 |
if (!latch.await(duration,
units)) { |
04 |
MemcachedConnection.opTimedOut(op); |
2、IO線程
IO線程的操作循環
處理輸入隊列,註冊寫事件;執行寫操作,註冊讀事件;處理讀操作,解析結果。
01 |
public void handleIO() throws IOException
{ |
03 |
int selected
= selector.select(delay); |
04 |
Set<SelectionKey>
selectedKeys = selector.selectedKeys(); |
06 |
if (selectedKeys.isEmpty()
&& !shutDown) { |
09 |
for (SelectionKey
sk : selectedKeys) { |
02 |
處理addedQueue中的所有節點,對每一個節點複製inputQueue中的操作到writeQ中。註冊讀寫事件。 |
03 |
private void handleInputQueue()
{ |
04 |
if (!addedQueue.isEmpty())
{ |
05 |
Collection<MemcachedNode>
toAdd = new HashSet<MemcachedNode>(); |
06 |
Collection<MemcachedNode>
todo = new HashSet<MemcachedNode>(); |
07 |
MemcachedNode
qaNode = null ; |
08 |
while ((qaNode
= addedQueue.poll()) != null )
{ |
11 |
for (MemcachedNode
qa : todo) { |
12 |
boolean readyForIO
= false ; |
14 |
if (qa.getCurrentWriteOp()
!= null )
{ |
23 |
if (qa.getWbuf().hasRemaining())
{ |
24 |
handleWrites(qa.getSk(),
qa); |
26 |
} catch (IOException
e) { |
32 |
addedQueue.addAll(toAdd); |
02 |
readQ不爲空註冊讀事件;writeQ不爲空註冊寫事件;網絡沒有連接上註冊連接事件。 |
03 |
public final void fixupOps()
{ |
05 |
if (s
!= null &&
s.isValid()) { |
06 |
int iops
= getSelectionOps(); |
01 |
public final int getSelectionOps()
{ |
03 |
if (getChannel().isConnected())
{ |
05 |
rv
|= SelectionKey.OP_READ; |
07 |
if (toWrite
> 0 ||
hasWriteOp()) { |
08 |
rv
|= SelectionKey.OP_WRITE; |
11 |
rv
= SelectionKey.OP_CONNECT; |
1 |
public final boolean hasReadOp()
{ |
2 |
return !readQ.isEmpty(); |
5 |
public final boolean hasWriteOp()
{ |
6 |
return !(optimizedOp
== null &&
writeQ.isEmpty()); |
3、handleWrites(SelectionKey sk, MemcachedNode qa)
我能夠想到的一些場景,這個狀態機代碼必須處理的
⑴ 當前隊列中有1個操作,操作要發送的字節數目小於16K
⑵ 當前隊列中有1個操作,操作要發送的字節數目大於16K(很大的set操作)
⑶ 當前隊列中有多個操作,操作要發送的字節數目小於16K
⑷ 當前隊列中有多個操作,操作要發送的字節數目大於16K
⑸ 任意一次寫操作wrote爲0
summary:處理節點中writeQ和inputQueue中的所有操作。每次循環會盡量填滿發送緩衝區,然後將發送緩衝區的內容全部發送到網絡上,循環往復,沒有異常的情況下,直至發送完數據。操作中發送的內容只要放入到發送緩衝區後,就把操作加入到readQ(spy中根據writeQ和readQ中有沒有數據,來註冊讀寫事件)。
執行時機:IO線程在select上休眠,被工作線程喚醒後,處理輸入隊列,把操作複製到writeQ 中,註冊寫事件;再次調用select,返回後,就會調用handleWrites(),數據全部發送後,會註冊讀事件。處理輸入隊列時,如果wbuf還有東西沒有發送,那麼會在select調用前,調用handleWrites函數。
01 |
private void handleWrites(SelectionKey
sk, MemcachedNode qa) throws IOException
{ |
02 |
qa.fillWriteBuffer(shouldOptimize);
---> |
03 |
boolean canWriteMore
= qa.getBytesRemainingToWrite() > 0 ; |
04 |
while (canWriteMore)
{ |
05 |
int wrote
= qa.writeSome(); ---> |
06 |
qa.fillWriteBuffer(shouldOptimize); |
07 |
canWriteMore
= wrote > 0 &&
qa.getBytesRemainingToWrite() > 0 ; |
11 |
--
發送數據;執行一次後,wbuf可能還有數據未寫完 |
12 |
public final int writeSome() throws IOException
{ |
13 |
int wrote
= channel.write(wbuf); |
02 |
toWrite= 0 表明
寫緩衝區以前的內容已經全部寫入到網絡中,這樣纔會進行下一次的填充寫緩衝區 |
03 |
操作會盡量填滿16K的緩衝區(單一操作數據量很大比如500K;或多個操作數據量500K) |
04 |
當一個操作中的數據完全寫入緩衝區後,操作的狀態變成READING,從writeQ中移除當前操作。 |
05 |
public final void fillWriteBuffer( boolean shouldOptimize)
{ |
06 |
if (toWrite
== 0 &&
readQ.remainingCapacity() > 0 )
{ |
08 |
Operation
o=getNextWritableOp(); ---> |
10 |
while (o
!= null &&
toWrite < getWbuf().capacity()) { |
12 |
ByteBuffer
obuf = o.getBuffer(); |
13 |
int bytesToCopy
= Math.min(getWbuf().remaining(), obuf.remaining()); |
14 |
byte []
b = new byte [bytesToCopy]; |
17 |
if (!o.getBuffer().hasRemaining())
{ |
19 |
transitionWriteItem(); |
20 |
preparePending();
-- copyInputQueue() |
24 |
o=getNextWritableOp(); |
26 |
toWrite
+= bytesToCopy; |
02 |
如果操作已經取消(前端線程等待超時,取消操作),或超時(IO線程沒有來得及執行操作,操作超時),那麼把操作從隊列中移除,繼續查找下一個操作。把可寫的操作的狀態從WRITE_QUEUED變成WRITING,同時把操作放入讀隊列中。 |
03 |
private Operation
getNextWritableOp() { |
04 |
Operation
o = getCurrentWriteOp(); --->④ |
05 |
while (o
!= null &&
o.getState() == OperationState.WRITE_QUEUED) { |
07 |
if (o.isCancelled())
{ |
08 |
Operation
cancelledOp = removeCurrentWriteOp();--->⑤ |
09 |
} else if (o.isTimedOut(defaultOpTimeout))
{ |
10 |
Operation
timedOutOp = removeCurrentWriteOp(); |
13 |
if (!(o instanceof TapAckOperationImpl))
{ |
18 |
o
= getCurrentWriteOp(); |
01 |
④
-- 拿到當前寫操作(並不從隊列中移除) |
02 |
public final Operation
getCurrentWriteOp() { |
03 |
return optimizedOp
== null ?
writeQ.peek() : optimizedOp; |
07 |
public final Operation
removeCurrentWriteOp() { |
08 |
Operation
rv = optimizedOp; |
02 |
handleReads(SelectionKey
sk, MemcachedNode qa) |
03 |
從網絡中讀取數據,放入rbuf。解析rbuf,得到結果; |
04 |
private void handleReads(SelectionKey
sk, MemcachedNode qa) throws IOException
{ |
05 |
Operation
currentOp = qa.getCurrentReadOp(); |
06 |
if (currentOp instanceof TapAckOperationImpl)
{ |
07 |
qa.removeCurrentReadOp(); |
10 |
ByteBuffer
rbuf = qa.getRbuf(); |
11 |
final SocketChannel
channel = qa.getChannel(); |
12 |
int read
= channel.read(rbuf); |
18 |
while (rbuf.remaining()
> 0 )
{ |
19 |
synchronized (currentOp)
{ |
20 |
currentOp.readFromBuffer(rbuf); |
21 |
if (currentOp.getState()
== OperationState.COMPLETE) { |
22 |
Operation
op = qa.removeCurrentReadOp(); |
23 |
} else if (currentOp.getState()
== OperationState.RETRY) { |
24 |
((VBucketAware)
currentOp).addNotMyVbucketNode(currentOp.getHandlingNode()); |
25 |
Operation
op = qa.removeCurrentReadOp(); |
26 |
retryOps.add(currentOp); |
29 |
currentOp=qa.getCurrentReadOp(); |
32 |
read
= channel.read(rbuf); |
02 |
public void readFromBuffer(ByteBuffer
data) throws IOException
{ |
03 |
while (getState()
!= OperationState.COMPLETE && data.remaining() > 0 )
{ |
04 |
if (readType
== OperationReadType.DATA) { |
08 |
for ( int i
= 0 ;
data.remaining() > 0 ;
i++) { |
12 |
} else if (b
== '\n' )
{ |
13 |
assert foundCr
: "got
a \\n without a \\r" ; |
18 |
assert !foundCr
: "got
a \\r without a \\n" ; |
23 |
String
line = new String(byteBuffer.toByteArray(),
CHARSET); |
25 |
OperationErrorType
eType = classifyError(line); |
27 |
handleError(eType,
line); |
03 |
public final void handleLine(String
line) { |
04 |
if (line.equals( "END" ))
{ |
06 |
getCallback().receivedStatus(END); |
08 |
getCallback().receivedStatus(NOT_FOUND); |
10 |
transitionState(OperationState.COMPLETE); |
12 |
} else if (line.startsWith( "VALUE
" ))
{ |
13 |
String[]
stuff = line.split( "
" ); |
14 |
currentKey
= stuff[ 1 ]; |
15 |
currentFlags
= Integer.parseInt(stuff[ 2 ]); |
16 |
data
= new byte [Integer.parseInt(stuff[ 3 ])]; |
17 |
if (stuff.length
> 4 )
{ |
18 |
casValue
= Long.parseLong(stuff[ 4 ]); |
22 |
setReadType(OperationReadType.DATA); |
23 |
} else if (line.equals( "LOCK_ERROR" ))
{ |
24 |
getCallback().receivedStatus(LOCK_ERROR); |
25 |
transitionState(OperationState.COMPLETE); |
27 |
assert false : "Unknown
line type: " +
line; |
5、那個著名的bug
JAVA NIO bug 會導致 CPU 100%
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6403933
int selected = selector.select(delay);
Set<SelectionKey> selectedKeys = selector.selectedKeys();
if (selectedKeys.isEmpty() && !shutDown) {
if (++emptySelects > DOUBLE_CHECK_EMPTY) {
for (SelectionKey sk : selector.keys()) {
if (sk.readyOps() != 0) {
handleIO(sk);
} else {
lostConnection((MemcachedNode) sk.attachment());
}
DOUBLE_CHECK_EMPTY = 256,當連續的select返回爲空時,++emptySelects,超過256,連接到當前mc節點的socket channel關閉,放入重連隊列。
七、調試 spymemcached
調試 spymemcached IO線程的過程中,工作線程放入到節點隊列的操作很容易超時,因此需要繼承DefaultConnectionFactory 複寫相關方法。
01 |
public class AstuteConnectionFactory extends DefaultConnectionFactory
{ |
03 |
public boolean shouldOptimize()
{ |
07 |
public long getOperationTimeout()
{ |
八、參考資料
NIO:http://www.ibm.com/developerworks/cn/education/java/j-nio/index.html
memcached:http://memcached.org/
protocol:https://github.com/memcached/memcached/blob/master/doc/protocol.txt