Redis簡單介紹-管道

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的管道實現,假如有不對的地方,歡迎前來指正。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章