Redis是一個使用客戶機-服務器模型和所謂的請求/響應協議的TCP服務器。這意味着請求通常通過以下步驟完成:
- 客戶端向服務器發送一個請求,並從套接字讀取服務器響應,通常是以阻塞的方式。
- 服務端處理命令併發送響應給客戶端。
客戶端和服務器通過網絡連接。這樣的鏈接可以是非常快(環回接口)或非常慢(在Internet上建立的連接,兩臺主機之間有許多跳)。無論網絡延遲是什麼,數據包都有一段時間從客戶機傳輸到服務器,然後從服務器返回到客戶機以攜帶應答。
這個時間叫做RTT(往返時間)。當客戶機需要在一行中執行多個請求時(例如向同一列表中添加多個元素,或用多個鍵填充數據庫),很容易看出這會如何影響性能。例如,如果RTT時間是250毫秒(在Internet上的鏈接非常慢的情況下),即使服務器能夠每秒處理10萬個請求,我們也將能夠每秒最多處理四個請求。
一、什麼是Redis管道
Redis提供了一種區別於上述響應模式的方式,客戶端無需等待單個請求響應再進行下一次請求,而是可以一次發送多個命令給服務器,服務器處理完後一次性將結果返回給客戶端,這種工作方式在Redis中被稱作管道。可以可出,通過管道的方式,能夠明顯的減少請求的次數,在於網絡狀況不好的環境中,能明顯的提生處理的速度。
二、Jedis中的管道實現
在Jedis中,客戶端通過與服務器建立Socket長連接來進行通信,主要通過一個Connection對象進行維護,可以看下這個類的定義:
public class Connection implements Closeable {
private static final byte[][] EMPTY_ARGS = new byte[0][];
private String host = Protocol.DEFAULT_HOST;
private int port = Protocol.DEFAULT_PORT;
private Socket socket;
private RedisOutputStream outputStream;
private RedisInputStream inputStream;
private int connectionTimeout = Protocol.DEFAULT_TIMEOUT;
private int soTimeout = Protocol.DEFAULT_TIMEOUT;
private boolean broken = false;
private boolean ssl;
private SSLSocketFactory sslSocketFactory;
private SSLParameters sslParameters;
private HostnameVerifier hostnameVerifier;
public Connection() {
}
public Connection(final String host) {
this.host = host;
}
public Connection(final String host, final int port) {
this.host = host;
this.port = port;
}
//...
}
Connection對象通過outputStream發送redis命令,通過inputStream讀取服務器的響應,對與發送redis命令,Jedis中分爲兩個動作:
- 向outputStream中寫入命令
- flush outputStream,發送網絡套接字
我們通過一個查看set命令的源碼驗證一下:
@Override
public String set(final String key, final String value) {
checkIsInMultiOrPipeline();
client.set(key, value);
return client.getStatusCodeReply();
}
進入Client.set方法:
@Override
public void set(final String key, final String value) {
set(SafeEncoder.encode(key), SafeEncoder.encode(value));
}
//...
public void set(final byte[] key, final byte[] value) {
sendCommand(SET, key, value);
}
//...
public void sendCommand(final ProtocolCommand cmd, final byte[]... args) {
try {
connect();
Protocol.sendCommand(outputStream, cmd, args);
} catch (JedisConnectionException ex) {
/*
* When client send request which formed by invalid protocol, Redis send back error message
* before close connection. We try to read it to provide reason of failure.
*/
try {
String errorMessage = Protocol.readErrorLineIfPossible(inputStream);
if (errorMessage != null && errorMessage.length() > 0) {
ex = new JedisConnectionException(errorMessage, ex.getCause());
}
} catch (Exception e) {
/*
* Catch any IOException or JedisConnectionException occurred from InputStream#read and just
* ignore. This approach is safe because reading error message is optional and connection
* will eventually be closed.
*/
}
// Any other exceptions related to connection?
broken = true;
throw ex;
}
}
再看下Protocol.sendCommand:
public static void sendCommand(final RedisOutputStream os, final ProtocolCommand command,
final byte[]... args) {
sendCommand(os, command.getRaw(), args);
}
private static void sendCommand(final RedisOutputStream os, final byte[] command,
final byte[]... args) {
try {
os.write(ASTERISK_BYTE);
os.writeIntCrLf(args.length + 1);
os.write(DOLLAR_BYTE);
os.writeIntCrLf(command.length);
os.write(command);
os.writeCrLf();
for (final byte[] arg : args) {
os.write(DOLLAR_BYTE);
os.writeIntCrLf(arg.length);
os.write(arg);
os.writeCrLf();
}
} catch (IOException e) {
throw new JedisConnectionException(e);
}
}
到這一步,客戶端的set命令就完成了,但是這只是命令寫入到了outputStream緩衝區中,還沒有向Redis服務器實際的發送Redis命令,
我們再來看一下 client.getStatusCodeReply():
public String getStatusCodeReply() {
flush();
final byte[] resp = (byte[]) readProtocolWithCheckingBroken();
if (null == resp) {
return null;
} else {
return SafeEncoder.encode(resp);
}
}
該方法會觸發flush,outputStream緩衝區中的Redis命令會真正的發送到Redis服務器上,我們理解上述過程之後,再回過頭來看下管道。
在Redis客戶端中,客戶端只能處於一種模式下,要麼是普通的模式,一次發送一個請求,要麼處於管道模式,二者不能混用。在普通模式下,命令寫入到outputStream緩衝區後會立即觸發flush,而在管道模式模式下,flush的觸發需要顯示調用Pipeline.sync()函數,因此可以實現一次發送多個命令。
public void sync() {
if (getPipelinedResponseLength() > 0) {
List<Object> unformatted = client.getMany(getPipelinedResponseLength());
for (Object o : unformatted) {
generateResponse(o);
}
}
}
public List<Object> getMany(final int count) {
flush();
final List<Object> responses = new ArrayList<Object>(count);
for (int i = 0; i < count; i++) {
try {
responses.add(readProtocolWithCheckingBroken());
} catch (JedisDataException e) {
responses.add(e);
}
}
return responses;
}
三、Jedis管道的數據接收
Jedis中通過一次性將outputStream中的命令發送到Redis中來實現管道的特性,但這裏有一個問題需要解決,客戶端如何接收服務器響應。在普通模式下,客戶端發送請求,然後同步的接收響應。在管道模式下,需要批量的接收響應,然後分發的對應的請求中去,Jedis通過Builder接口實現這個過程:
public abstract class Builder<T> {
public abstract T build(Object data);
}
在Jedis的BuilderFactory中,對Builder接口進行多種實現,主要包括將data轉換爲String,int,long,float,double和boolean類型。
在通過pipeline發送命令命令時,pipeline會返回一個Response對象代表一個服務器響應,可以看下這個類的實現:
public class Response<T> {
protected T response = null;
protected JedisDataException exception = null;
private boolean building = false;
private boolean built = false;
private boolean set = false;
private Builder<T> builder;
private Object data;
private Response<?> dependency = null;
public Response(Builder<T> b) {
this.builder = b;
}
public void set(Object data) {
this.data = data;
set = true;
}
public T get() {
// if response has dependency response and dependency is not built,
// build it first and no more!!
if (dependency != null && dependency.set && !dependency.built) {
dependency.build();
}
if (!set) {
throw new JedisDataException(
"Please close pipeline or multi block before calling this method.");
}
if (!built) {
build();
}
if (exception != null) {
throw exception;
}
return response;
}
//...
}
可以看到,Response對象的創建需要一個Builder, 表示如何構造響應數據。
我們還是以set命令爲例子:
@Override
public Response<String> set(final String key, final String value) {
getClient(key).set(key, value);
return getResponse(BuilderFactory.STRING);
}
//...
protected <T> Response<T> getResponse(Builder<T> builder) {
Response<T> lr = new Response<T>(builder);
pipelinedResponses.add(lr);
return lr;
}
可以看到,調用set命令時,Pipeline會將命令的實現委託給Client對象,並返回一個指定Builder的Response,表示將來有數據了,通過指定的Builder去構造數據。並且也會將Response加入到管道維護的一個隊列中。這個主要是爲了調用sync()方法時,將服務器的響應分發到每一個response中。
public void sync() {
if (getPipelinedResponseLength() > 0) {
List<Object> unformatted = client.getMany(getPipelinedResponseLength());
for (Object o : unformatted) {
generateResponse(o);
}
}
}
protected Response<?> generateResponse(Object data) {
Response<?> response = pipelinedResponses.poll();
if (response != null) {
response.set(data);
}
return response;
}
這個時候 Response中就有了數據data,此時就可以調用builder去獲取實際類型的數據了。
本人對Redis的管道做了簡單介紹,也簡單說了一下jedis的管道實現,假如有不對的地方,歡迎前來指正。