Redis 系列--Jedis 源码解读

Jedis 作为 Redis 官方推荐的三个客户端之一,相比于其他两个(Lettuce,Redisson)而言,它提供最基本的客户端功能,也因此使得它具备简单和轻量的特性。此外,它也是线程不安全的客户端,因此不适用在多线程场景中。本篇我们通过对其源码的阅读来了解 Redis 的客户端是如何进行工作的,同时也去找出它线程不安全的原因。

版本说明:本片源码解读基于 Jedis-3.3.0,请注意区分。

1. 初始化

首先我们从 Jedis 的基本使用说起,最简单的使用方式如下:

Jedis jedis = new Jedis("localhost", 6379);// 初始化客户端
jedis.auth("foobared");// 密码认证
jedis.dbSize();// 查询字典的数据量
jedis.close();// 关闭连接

了解了基本的用法之后,我们就从客户端的初始化开始,在开始之前不妨先了解一下 Jedis 类的结构:
Redis 系列--Jedis 源码解读_第1张图片

  • BinaryJedisCommands: 定义各种数据类型的基本的操作指令;
  • BasicCommands: 定义基本的数据库操作指令;
  • AdvancedBinaryJedisCommands: 定义高级的操作指令;
  • BinaryScriptingCommands: 定义 Lua 脚本相关的操作指令;
  • MultiKeyBinaryCommands: 定义多键值操作的指令;
  • BinaryJedis: 实现了上面这些操作接口;
  • Jedis: 我们所使用的客户端,不同于 BinaryJedis 的地方在于其方法参数是字符串,而 BinaryJedis 参数是字节数组。
    上面这些带 Binary 的接口或类,其参数均为字节数组,这正是其使用 Binary 修饰的原因。

Jedis 的初始化:

public BinaryJedis(final String host, final int port) {
  client = new Client(host, port);
}

Client 的类图如下:
Redis 系列--Jedis 源码解读_第2张图片

public Client(final String host, final int port) {
  // 调用父类 BinaryClient 的初始化
  super(host, port);
}

public BinaryClient(final String host, final int port) {
  // 调用父类 Connection 的初始化
  super(host, port);
}

public Connection(final String host, final int port) {
  this(host, port, false);
}

public Connection(final String host, final int port, final boolean ssl) {
  this(host, port, ssl, null, null, null);
}

public Connection(final String host, final int port, final boolean ssl,
    SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
    HostnameVerifier hostnameVerifier) {
  this(new DefaultJedisSocketFactory(host, port, Protocol.DEFAULT_TIMEOUT,
      Protocol.DEFAULT_TIMEOUT, ssl, sslSocketFactory, sslParameters, hostnameVerifier));
}

public Connection(final JedisSocketFactory jedisSocketFactory) {
  this.jedisSocketFactory = jedisSocketFactory;
}

DefaultJedisSocketFactory–套接字工厂

public class DefaultJedisSocketFactory implements JedisSocketFactory {

  private String host;
  private int port;
  private int connectionTimeout;
  private int soTimeout;
  private boolean ssl;
  private SSLSocketFactory sslSocketFactory;
  private SSLParameters sslParameters;
  private HostnameVerifier hostnameVerifier;

  public DefaultJedisSocketFactory(String host, int port, int connectionTimeout, int soTimeout,
      boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
      HostnameVerifier hostnameVerifier) {
    this.host = host;
    this.port = port;
    this.connectionTimeout = connectionTimeout;
    this.soTimeout = soTimeout;
    this.ssl = ssl;
    this.sslSocketFactory = sslSocketFactory;
    this.sslParameters = sslParameters;
    this.hostnameVerifier = hostnameVerifier;
  }

  @Override
  public Socket createSocket() throws IOException {
    Socket socket = null;
    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(getHost(), getPort()), getConnectionTimeout());
      socket.setSoTimeout(getSoTimeout());

      if (ssl) {
        if (null == sslSocketFactory) {
          sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
        }
        socket = sslSocketFactory.createSocket(socket, getHost(), getPort(), true);
        if (null != sslParameters) {
          ((SSLSocket) socket).setSSLParameters(sslParameters);
        }
        if ((null != hostnameVerifier)
            && (!hostnameVerifier.verify(getHost(), ((SSLSocket) socket).getSession()))) {
          String message = String.format(
            "The connection to '%s' failed ssl/tls hostname verification.", getHost());
          throw new JedisConnectionException(message);
        }
      }
      return socket;
    } catch (Exception ex) {
      if (socket != null) {
        socket.close();
      }
      throw ex;
    }
  }

  @Override
  public String getDescription() {
    return host + ":" + port;
  }

  @Override
  public String getHost() {
    return host;
  }

  @Override
  public void setHost(String host) {
    this.host = host;
  }

  @Override
  public int getPort() {
    return port;
  }

  @Override
  public void setPort(int port) {
    this.port = port;
  }

  @Override
  public int getConnectionTimeout() {
    return connectionTimeout;
  }

  @Override
  public void setConnectionTimeout(int connectionTimeout) {
    this.connectionTimeout = connectionTimeout;
  }

  @Override
  public int getSoTimeout() {
    return soTimeout;
  }

  @Override
  public void setSoTimeout(int soTimeout) {
    this.soTimeout = soTimeout;
  }
}

2. 执行 command

2.1 发送指令 client.set()

Jedis 中执行命令的过程都实现在 Client 类中,我们以 set 指令为例来进一步讨论:

  public String set(final String key, final String value, final SetParams params) {
    // 判断当前连接是否处于事务中或者管道中,
    // 处于事务中或者管道中,无法使用 Jedis 对象执行任何指令,
    // 底层会向上抛出异常
    checkIsInMultiOrPipeline();
    client.set(key, value, params);
    return client.getStatusCodeReply();
  }

client.set()

  public void set(final String key, final String value, final SetParams params) {
    set(SafeEncoder.encode(key), SafeEncoder.encode(value), params);
  }

BinaryClient.set()

public void set(final byte[] key, final byte[] value, final SetParams params) {
  sendCommand(SET, params.getByteParams(key, value));
}

...

// 发送 Redis 指令的核心方法
public void sendCommand(final ProtocolCommand cmd, final byte[]... args) {
  try {
    // 判断当前是否有可用连接,有则直接使用,否则使用套接字工厂创建
    connect();
    // 使用套接字,按照 Redis 的序列化协议 RESP 进行字节流的发送
    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;
  }
}

...

// 判断当前是否有可用连接,有则直接使用,否则使用套接字工厂创建
public void connect() {
  if (!isConnected()) {
    try {
      socket = jedisSocketFactory.createSocket();

      outputStream = new RedisOutputStream(socket.getOutputStream());
      inputStream = new RedisInputStream(socket.getInputStream());
    } catch (IOException ex) {
      broken = true;
      throw new JedisConnectionException("Failed connecting to "
          + jedisSocketFactory.getDescription(), 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);
  }
}

2.2 接收响应 client.getStatusCodeReply()

  public String getStatusCodeReply() {
    // 刷新输出流
    flush();
    // readProtocolWithCheckingBroken() 方法继承自 Connection
    final byte[] resp = (byte[]) readProtocolWithCheckingBroken();
    if (null == resp) {
      return null;
    } else {
      // 将字节流进行编码得到结果
      return SafeEncoder.encode(resp);
    }
  }

readProtocolWithCheckingBroken()

  protected Object readProtocolWithCheckingBroken() {
    if (broken) {
      throw new JedisConnectionException("Attempting to read from a broken connection");
    }

    try {
      return Protocol.read(inputStream);
    } catch (JedisConnectionException exc) {
      broken = true;
      throw exc;
    }
  }

Protocol.read()

public static Object read(final RedisInputStream is) {
  // 按照 RESP 协议进行解析响应
  return process(is);
}

...

private static Object process(final RedisInputStream is) {
  final byte b = is.readByte();
  switch (b) {
  case PLUS_BYTE:
    return processStatusCodeReply(is);
  case DOLLAR_BYTE:
    return processBulkReply(is);
  case ASTERISK_BYTE:
    return processMultiBulkReply(is);
  case COLON_BYTE:
    return processInteger(is);
  case MINUS_BYTE:
    processError(is);
    return null;
  default:
    throw new JedisConnectionException("Unknown reply: " + (char) b);
  }
}

3. 管道

管道(流水线)的基本使用如下:

Pipeline p = jedis.pipelined();
p.set("foo", "bar");
p.get("foo");
List<Object> results = p.syncAndReturnAll();

System.out.println(results.size());// 2
System.out.println(results.get(0));// "OK"
System.out.println(results.get(1));// "bar"

3.1 jedis.pipelined()

public Pipeline pipelined() {
  pipeline = new Pipeline();
  pipeline.setClient(client);
  return pipeline;
}

3.2 pipeline.set()

public Response<String> set(final String key, final String value) {
  // 通过 client 客户端对象调用操作指令
  getClient(key).set(key, value);
  // 获取应答信息,BuilderFactory 负责类型转换
  return getResponse(BuilderFactory.STRING);
}

getResponse()

protected <T> Response<T> getResponse(Builder<T> builder) {
  Response<T> lr = new Response<T>(builder);
  pipelinedResponses.add(lr);
  return lr;
}

3.3 pipeline.syncAndReturnAll()

public List<Object> syncAndReturnAll() {
  if (getPipelinedResponseLength() > 0) {
    // 从连接中获得多个未格式化的响应信息
    List<Object> unformatted = client.getMany(getPipelinedResponseLength());
    List<Object> formatted = new ArrayList<>();
    for (Object o : unformatted) {
      try {
        formatted.add(generateResponse(o).get());
      } catch (JedisDataException e) {
        formatted.add(e);
      }
    }
    return formatted;
  } else {
    return java.util.Collections.<Object> emptyList();
  }
}

generateResponse()

protected Response<?> generateResponse(Object data) {
  // 指令执行时,向 pipelinedResponses 中添加的 Response 元素
  // 依次取出元素,代表已被服务端处理并收到应答
  // Response 是对应答信息的封装,根据不同的指令,设置对应的应答数据类型
  // 当收到应答后,按照数据类型进行提取转换
  Response<?> response = pipelinedResponses.poll();
  if (response != null) {
    response.set(data);// 设置具体类型的响应值
  }
  return response;
}

response.get()

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

  private void build() {
    // check build state to prevent recursion
    if (building) {
      return;
    }

    building = true;
    try {
      if (data != null) {
        if (data instanceof JedisDataException) {
          exception = (JedisDataException) data;
        } else {
          response = builder.build(data);
        }
      }

      data = null;
    } finally {
      building = false;
      built = true;
    }
  }

BuilderFactory 工厂类中定义了各种数据类型的建造器内部类,对应答数据进行相应格式的转换。

3.4 总结

通过源码的解读,我们了解到,所谓的管道技术就是将多个指令几乎同时发送到服务端,以此来将 N 个 RTT 降低到接近一个 RTT,来提高客户端的吞吐量,效果随着一次发送的指令数的增加而增加。

4. 连接池

4.1 初始化

连接池的基本用法如下:

JedisPool pool = new JedisPool("localhost", 6379, 2000);
Jedis jedis = pool.getResource();
jedis.auth("foobared");
jedis.set("foo", "bar");
System.out.println(jedis.get("foo"));// "bar"
jedis.close();// 将连接归还到连接池

JedisPool 的类图:
Redis 系列--Jedis 源码解读_第3张图片
JedisPool 构造

  public JedisPool(final GenericObjectPoolConfig poolConfig, final String host, int port,
      final int connectionTimeout, final int soTimeout, final String password, final int database,
      final String clientName) {
    // 调用父类 JedisPoolAbstract 的构造器
    // GenericObjectPoolConfig 中封装了连接池的属性信息
    // JedisFactory 作为工厂,用来创建客户端连接 DefaultPooledObject
    super(poolConfig, new JedisFactory(host, port, connectionTimeout, soTimeout, password,
        database, clientName));
  }
public JedisPoolAbstract(GenericObjectPoolConfig poolConfig, PooledObjectFactory<Jedis> factory) {
  // 调用父类 Pool 的构造器
  super(poolConfig, factory);
}
public Pool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {
  initPool(poolConfig, factory);
}

...

public void initPool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {

  if (this.internalPool != null) {
    try {
      // 如果存在则关闭
      closeInternalPool();
    } catch (Exception e) {
    }
  }
  // 初始化池对象
  this.internalPool = new GenericObjectPool<>(factory, poolConfig);
}

4.2 获取连接

public Jedis getResource() {
  // 调用父类的方法获取连接
  Jedis jedis = super.getResource();
  jedis.setDataSource(this);
  return jedis;
}

Pool.getResource()

public T getResource() {
  try {
    // GenericObjectPool 类型的 internalPool 来自 commons-pool 依赖
    // 大概逻辑如下:
    //   首次获取连接时,使用我们初始化时传入的 JedisFactory 工厂创建一个连接
    //   后续如果有空闲的可直接获取到,否则根据配置参数判断能否新建连接
    //     如果可以则新建,否则等待直至超时
    return internalPool.borrowObject();
  } catch (NoSuchElementException nse) {
    if (null == nse.getCause()) { // The exception was caused by an exhausted pool
      throw new JedisExhaustedPoolException(
          "Could not get a resource since the pool is exhausted", nse);
    }
    // Otherwise, the exception was caused by the implemented activateObject() or ValidateObject()
    throw new JedisException("Could not get a resource from the pool", nse);
  } catch (Exception e) {
    throw new JedisConnectionException("Could not get a resource from the pool", e);
  }
}

5. 为何线程不安全

我们再来观察一下发送指令的核心方法——Connection.sendCommand()

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

此方法在发送指令前,会先检查连接是否已经创建,如果已经创建则直接使用,否则创建一个新的连接,问题就在这里:

public void connect() {
  // 这里的 isConnected() 方法存在并发问题,单多个线程并发时会导致 socket 重复创建
  if (!isConnected()) {
    try {
      socket = jedisSocketFactory.createSocket();

      outputStream = new RedisOutputStream(socket.getOutputStream());
      inputStream = new RedisInputStream(socket.getInputStream());
    } catch (IOException ex) {
      broken = true;
      throw new JedisConnectionException("Failed connecting to "
          + jedisSocketFactory.getDescription(), ex);
    }
  }
}

...

public boolean isConnected() {
  return socket != null && socket.isBound() && !socket.isClosed() && socket.isConnected()
      && !socket.isInputShutdown() && !socket.isOutputShutdown();
}

你可能感兴趣的:(技术,菜鸟,微服务)