Jedis源码阅读之底层交互

1. host和port的设置

众所周知, 和一个网络服务建立连接的关键参数就是服务的地址和端口号, 与redis server建立连接也不例外.

在Jedis中, 使用HostAndPortUtil来管理和redis server的host和port. 默认情况下可以使用localhost这个host和6379这个port

源码:

public final class HostAndPortUtil {
  
  ...
  other code
  ...
  
  static {
        // host: localhost, port: 6379
    redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT));
    redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT + 1));
    redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT + 2));
    redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT + 3));
    redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT + 4));
    redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT + 5));
    redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT + 6));

        // 哨兵模式下的地址和端口
    sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT));
    sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 1));
    sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 2));
    sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 3));

        // 集群模式下的地址和端口
    clusterHostAndPortList.add(new HostAndPort("localhost", 7379));
    clusterHostAndPortList.add(new HostAndPort("localhost", 7380));
    clusterHostAndPortList.add(new HostAndPort("localhost", 7381));
    clusterHostAndPortList.add(new HostAndPort("localhost", 7382));
    clusterHostAndPortList.add(new HostAndPort("localhost", 7383));
    clusterHostAndPortList.add(new HostAndPort("localhost", 7384));

   }
  
  ...
  other code
  ...
}

当然, 如果不想使用Jedis提供的这些默认的host和port, 可以通过HostAndPortUtil.parseHosts()这个方法来设置自己的host和port

2. 建立连接

在设置完成redis server的host和port之后, 自然而然的就是和redis server建立连接.
在阅读连接的源代码之前, 先看一下和连接相关的UML图

Jedis源码阅读之底层交互_第1张图片
REDIS-CONNECTION UML

可以看到 BinaryJedis中含有 Client的引用, 而 Client又实现了 Connection, 而实际上与redis server建立连接的是 Connection, 所以只要创建 Jedis对象, 然后使用 Client与redis server建立连接就可以了.

接下来看下源码:

  1. 初始化Jedis对象
// 创建Jedis对象, 实际上创建Jedis对象的过程中最重要的是初始化Client对象
Jedis jedis = new Jedis(hnp.getHost(), hnp.getPort(), 500);

// Jedis构造函数中调用父类的构造函数, 在父类的构造函数中创建Client对象
public BinaryJedis(final String host, final int port, final int timeout) {
    client = new Client(host, port);
    client.setConnectionTimeout(timeout);
    client.setSoTimeout(timeout);
  }
  
 // 同样初始化Client时会调用Connection的构造函数, 初始化Connection对象, 这是最关键的一步, 因为后面会使用Connection对象与redis server真正建立连接
 public Connection(final String host, final int port) {
    this.host = host;
    this.port = port;
  }
  1. 与redis server建立连接

前面提到与redis server建立连接实际上是由Connection完成的, 因此把重点放到Connection的连接方法上.

// 通过jedis对象调用connect()
jedis.connect();

// 经过一系列的内部调用最终会调用Connection的Connection方法

public void connect() {
    if (!isConnected()) {
      try {
        // 创建Socket对象以及设置socket的一些参数
        socket = new Socket();
        // ->@wjw_add
        socket.setReuseAddress(true);
        socket.setKeepAlive(true);         
        socket.setTcpNoDelay(true);         
        socket.setSoLinger(true, 0); 
        socket.connect(new InetSocketAddress(host, port), connectionTimeout);
        socket.setSoTimeout(soTimeout);
            
            ...
            other code
            ...
            
            
        // 这里是关键的地方, 初始化与redis通信的input stream和output stream, 之后的将参数发送给redis server和接受redis server的命令执行结果就是通过这两个输入输出流
        outputStream = new RedisOutputStream(socket.getOutputStream());
        inputStream = new RedisInputStream(socket.getInputStream());
      } catch (IOException ex) {
        broken = true;
        throw new JedisConnectionException("Failed connecting to host " 
            + host + ":" + port, ex);
      }
    }
  }

3. 与redis进行交互

与redis server建立连接之后, 就可以与redis server进行交互了, 也就是可以向redis server发送指令了. 与redis server进行交互实际上可以通过Connection这个类进行控制

为了避免不必要的干扰, 这里采用较为简单的String的getSet命令说明与redis server的交互, 其他的命令的交互也相识.

通过之前的UML图可以知道Client还实现了Commands接口, 这个接口中定义了redis中支持的所有的命令, 由此可知jedis对象所有的命令调用在内部都是通过client对象进行调用的.

// Jedis.java
@Override
public String getSet(final String key, final String value) {
    ...
    other code
    ...
    // 调用client的getSet命令
    client.getSet(key, value);
    // 获取redis server的响应
    return client.getBulkReply();
}

在client的getSet方法中, 首先会先将命令进行编码, 也就是将字符串类型的命令转成字节形式(因为和redis server的交互是通过socket的, 也就是说是通过字节的形式在网络中进行传输的, 因此要先把命令转成字节形式)


// Client.java
@Override
 public void getSet(final String key, final String value) {

      // 对命令进行编码(转成字节形式)
    getSet(SafeEncoder.encode(key), SafeEncoder.encode(value));
 }

最后会调用ConnectionsendCommand()方法发送命令给redis server

// Connection.java
// 真正向redis-server发送请求
  public Connection sendCommand(final ProtocolCommand cmd, final byte[]... args) {
    try {
        // 这里会再次试着连接redis server, 也是调用之前的connect方法
      connect();
      // 通过Protocol发送命令, 可以看见这里使用的是之前创建的outputStream
      Protocol.sendCommand(outputStream, cmd, args);
      return this;
    } catch (JedisConnectionException ex) {
            ...
            错误处理
            ...         
    }
  }
// Protocol.java
// 实际上就是和其他的网络通信一样通过流向socket中写入数据, 这样就把数据发送给redis server了
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);
    }
  }

前面就是发送命令给redis server的流程, 但是细心的同学有没有发现, outputStream没有flush方法. 我们都知道像RedisOutputStream(继承了FilterOutputStream)这种高级输出流都是有缓存的, 所以如果没有调用flush之类的方法数据是不会发送的, 所以这里只是把命令放到了输出流的缓存中, 但是并没有发送给远程的redis server. 那具体是在什么时候将数据发送到redis server呢, 肯定是在调用flush的时候, 那什么时候调用flush方法呢, 让我们看一下从redis server获取命令执行结果的方法.

之前的Jedis.getSet()方法中发送完命令之后, 紧接着就调用了client.getBulkReply()方法, 不出意外, 这个方法是从Connection中继承的一个方法.

// Connection.java
public String getBulkReply() {
    final byte[] result = getBinaryBulkReply();
    if (null != result) {
      return SafeEncoder.encode(result);
    } else {
      return null;
    }
  }

  public byte[] getBinaryBulkReply() {
      // 就是在这里调用了flush()方法
    flush();
    // 读入redis server的响应结果
    return (byte[]) readProtocolWithCheckingBroken();
  }
  
  protected void flush() {
    try {
        // 发送数据给redis server
      outputStream.flush();
    } catch (IOException ex) {
      broken = true;
      throw new JedisConnectionException(ex);
    }
  }

上面可以看到, Jedis实际上是在读取redis server返回的结果之前才将命令发送给redis server, 至于为什么要这么做, 我想可能是与redis的multipipeline有关.

最后再来看一下如何从redis server读取响应结果的. 从上面可以看见读取redis server的数据是调用了readProtocolWithCheckingBroken方法

protected Object readProtocolWithCheckingBroken() {
    try {
        // 调用Protocol的read方法, 可以看见这里使用的是之前创建的inputStream, 也就是redis server会将响应结果打到inputStream, 然后Jedis从inputStream读取数据就可以了
      return Protocol.read(inputStream);
    } catch (JedisConnectionException exc) {
      broken = true;
      throw exc;
    }
  }

读取响应结果的过程就是如上所述, 但是读取的一些细节这里没有指明, 可以通过阅读Protocol.read()Protocol.process()方法了解.

4. 总结

可以看见Jedis底层和redis server的交互还是通过基本的socket连接实现的, 忽略实现的细节其实Jedis和redis server就是一个网络应答系统. 所以说看一个框架表面上功能有多强大有多灵活, 其实具体实现的原理都是一些基础的东西, 所以说基础这个东西还是很重要的, 越往底层研究越重要.

你可能感兴趣的:(Jedis源码阅读之底层交互)