1

前言

Jedis 是 java 应用访问 Redis 服务的首选客户端,本文通过分析 jedis 客户端源代码,扒一扒客户端设计与实现的常用套路

连接(Connection)

要访问(Redis)服务,首先需要与服务建立连接,因此客户端库首先需要对连接进行抽象和封装,Jedis 使用 Connection 类来封装与服务器的一个 socket 连接:

public class Connection implements Closable {
    private Socket socket;
    private connectionTimeout = Protocol.DEFAULT_TIMEOUT;
    private int soTimeout = Protocol.DEFAULT_TIMEOUT;
    ...
    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 类还会对 socket 的读写进行一层简单的封装,对于 Redis 客户端就是发送命令以及获取结果,这里的 Command 类是一个命令的枚举类型,args 是可变长参数,表示命令参数

protected Connection sendCommand(final Command cmd, final byte[]... args) {
    // 见下文
}

由于网络通信的不稳定,客户端与服务器的通信通常都需要 捕获 各种异常并进行恢复(重试,重连)

// sendCommand 实现
    try {
      connect();
      Protocol.sendCommand(outputStream, cmd, args);
      pipelinedCommands++;
      return this;
    } catch (JedisConnectionException ex) {
      try {
        String errorMessage = Protocol.readErrorLineIfPossible(inputStream);
        if (errorMessage != null && errorMessage.length() > 0) {
          ex = new JedisConnectionException(errorMessage, ex.getCause());
        }
      } catch (Exception e) {
      }
      // Any other exceptions related to connection?
      broken = true;
      throw ex;
    }

connect 方法建立连接,如果已经建立连接当然就不需要,这里又涉及到 socket 的一些常用参数

  • reuse address

  • keep alive

  • tcp no delay

  • so linger

  • so timeout

  public void connect() {
    if (!isConnected()) {
      try {
        socket = new Socket();
        // ->@wjw_add
        socket.setReuseAddress(true);
        socket.setKeepAlive(true); // Will monitor the TCP connection is
        // valid
        socket.setTcpNoDelay(true); // Socket buffer Whetherclosed, to
        // ensure timely delivery of data
        socket.setSoLinger(true, 0); // Control calls close () method,
        // the underlying socket is closed
        // immediately
        // <-@wjw_add

        socket.connect(new InetSocketAddress(host, port), connectionTimeout);
        socket.setSoTimeout(soTimeout);
        outputStream = new RedisOutputStream(socket.getOutputStream());
        inputStream = new RedisInputStream(socket.getInputStream());
      } catch (IOException ex) {
        broken = true;
        throw new JedisConnectionException(ex);
      }
    }
  }

输入输出流(Input, output stream)

通常客户端都会自定义输入输出流来封装协议格式以及对传输内容进行缓存(预读)来提高效率,Jedis 使用 RedisInputStream 和 RedisOutputStream 类类封装输入输出流

RedisInputStream

RedisInputStream 类中定义了一个 byte 类型的字节数组 buf 来保存从 socket inputstream 读到的数据,limit 字段表示实际读到的数据大小,count 字段表示当前已经读取的数据(偏移量),

public class RedisInputStream extends FilterInputStream {
    protected final byte[] buf;
    protected int count, limit;
    public RedisInputStream(InputStream  in, int size) {
        super(int);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
}

我们来看看 readLine 方法,它用于从 socket input stream 中读取一行

  public String readLine() {
    final StringBuilder sb = new StringBuilder();
    while (true) {
      // 预读
      ensureFill();
      byte b = buf[count++];
      if (b == '\r') {
        // 按照协议 \r\n 必定连续出现,所以读到 \r 后再预读一次确保数据完整性
        ensureFill(); // Must be one more byte
        byte c = buf[count++];
        if (c == '\n') {
          break;
        }
        sb.append((char) b);
        sb.append((char) c);
      } else {
        sb.append((char) b);
      }
    }
    final String reply = sb.toString();
    if (reply.length() == 0) {
      throw new JedisConnectionException("It seems like server has closed the
          connection.");
    }
    return reply;
  }

RedisOutputStream

协议(Protocol)

Socket 连接在客户端和服务器之间建立了一个通信通道,协议(Protocol)规定了数据传输格式,对于 Redis 这种使用 socket 长连接的服务,一般都会自定义 协议,所以接下来要对 协议 进行抽象和封装.

通常有两种做法:

  • 使用面向对象的分析与设计,将每个协议单元抽象成一个类

  • 将所有与协议相关的字段和方法封装到一个工具类里头

Jedis 使用了后者,可能是因为 Redis 命令本身比较简单,没必要过度设计.

Protocol 类包含了 Jedis 和 Redis 服务通信协议相关的代码,比如上文提到的 Protocol.sendCommand 方法

  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);
    }
  }

从 sendCommand 方法可以看出 Redis 发送命令协议,完整的协议可以参考 Redis 官网文档,或者使用 tcpdump 这样的抓包工具来观察 Redis 协议

Facade 设计模式

使用 Connection, Command, Protocol 等类就可以直接和 Redis 服务通信,但是客户端库通常会再做一层封装供调用者使用,类似设计模式中的 Facade 模式,这也就是为什么在很多客户端中会有各种各样的 XXXClient, XXXManager 等

在 Jedis 中这个 Facade(门面)就是 Jedis 和 Client

Jedis 类层次结构:

BinaryJedis ---> Client
    Jedis

BinaryJedis 使用 Client 类和 Redis 服务通信

Client 类层次结构:

Connection
    BinaryClient
        Client

以 Redis set 命令的实现为例:

  public String set(final byte[] key, final byte[] value) {
    checkIsInMultiOrPipeline();
    // 发送命令
    client.set(key, value);
    // 读取响应
    return client.getStatusCodeReply();
  }

性能优化

连接池

如果只有一条路,车比较多的时候就会造成阻塞,因此直观的方案是多修几条(道)路,所以客户端和服务器之间的连接普遍都会用到 连接池,除了上述效率的考虑外,使用连接池还可以增加容错能力,比如一个连接挂了,系统可以从连接池中选取其它的连接进行服务

Jedis 通过 JedisPool 来抽象和封装 连接池,向用户隐藏了实现细节:实例化 JedisPool 的一个实例并调用 getResource 方法就可以获取一个 Jedis 实例,使用完成后调用 Jedis.close 方法,就这么简单

public JedisPool(String host, int port) {
    ...
}

@Override
public Jedis getResource() {
    Jedis jedis = super.getResource();
    jedis.setDataSource(this);
    return jedis;
}

总结


xingpingz
122 声望64 粉丝

博学,审问,慎思,明辨,力行