Redis java客户端 jedis 源码分析系列二:单实例 jedis

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis java客户端 jedis 源码分析系列二:单实例 jedis相关的知识,希望对你有一定的参考价值。

在使用Jedis的过程中最简单的用法就是单实例单连接的jedis,如下代码所示:

public void testJedis(){

   Jedis jedis = new Jedis("127.0.0.1");

   jedis.set("key", "value");

   jedis.get("key");

   jedis.close();

   

}

让我们深入到内部去看一看其结构,如下图所示:

技术分享图片技术分享图片

此处请先忽略 JedisPool 类和 Pool<Jedis> 两个类。

Jedis 通过继承 BinaryJedis 来持有Client对象,Client类继承BinaryClient,BinaryClient继承Connection,下面分别介绍各个类主要做什么事情。

Connection:主要用于与redis服务器建立Socket连接,并管理连接

Protocol:主要用于redis客户端通信协议的处理

BinaryClient:是对Connection的包装,将字节数据发送Connection中进行通信协议的编码,同时实现了Redis字节命令的相关接口

Client:是对BinaryClient的包装,主要用于处理字符数据到字节数据的转换工作,然后通过BinaryClient的接口来实现后续与Redis服务器的交互,同时实现了Redis字符命令的相关接口

BinaryJedis:持有Client对象,Transaction事务对象,Pipeline管道对象,其中与Client是组成关系,强约束,其它两个是分别在使用事务和管道时才会使用到。通过持有的Client来执行相关命令的,此类处理字节命令

Jedis:继承BinaryJedis,与Client类似,Jedis处理字符命令,BinaryJedis处理字节命令。

下面我跟随前面的示例代码来进行源码的跟踪:

jedis.set("key", "value");

/**

* Set the string value as value of the key. The string can't be longer than 1073741824 bytes (1

* GB).

* <p>

* Time complexity: O(1)

* @param key

* @param value

* @return Status code reply

*/

public String set(final String key, String value) {

 checkIsInMultiOrPipeline();

 client.set(key, value);

 return client.getStatusCodeReply();

}

很明显,Jedis的set方法直接转换成了client.set()方法。checkIsInMultiOrPipeline();这个方法是检查当前是否有事务或管道在执行。我们再看client的set方法,源码如下

public void set(final String key, final String value) {

 set(SafeEncoder.encode(key), SafeEncoder.encode(value));

}

在Client类中将字符转换成了字节,然后调用BinaryClient的set方法,再看BinaryClient的set方法,源码如下

public void set(final byte[] key, final byte[] value) {

 sendCommand(Command.SET, key, value);

}

在BinaryClient类中直接调用了Connection中的sendCommand方法来完成命令的发送,接着看Connection中的sendCommand方法,源码如下

protected Connection sendCommand(final Command cmd, final byte[]... args) {

 try {

   connect();

   Protocol.sendCommand(outputStream, cmd, args);

   pipelinedCommands++;

   return this;

 } 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工具类来完成Redis通信协议报文的组装,并写入到 参数outputStream 指定的输出流。catch子句是在出错时获取redis返回的错误并包装到异常信息中,以便外面可以知道出现了什么错误。

接下来看一下Connection的connect方法,源代码如下:

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

     // <[email protected]_add

     socket.connect(new InetSocketAddress(host, port), connectionTimeout);

     socket.setSoTimeout(soTimeout);

     if (ssl) {

       if (null == sslSocketFactory) {

         sslSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault();

       }

       socket = (SSLSocket) sslSocketFactory.createSocket(socket, host, port, true);

       if (null != sslParameters) {

         ((SSLSocket) socket).setSSLParameters(sslParameters);

       }

       if ((null != hostnameVerifier) &&

           (!hostnameVerifier.verify(host, ((SSLSocket) socket).getSession()))) {

         String message = String.format(

             "The connection to '%s' failed ssl/tls hostname verification.", host);

         throw new JedisConnectionException(message);

       }

     }

     outputStream = new RedisOutputStream(socket.getOutputStream());

     inputStream = new RedisInputStream(socket.getInputStream());

   } catch (IOException ex) {

     broken = true;

     throw new JedisConnectionException(ex);

   }

 }

}

该方法首先检查是否已有连接,若有不做任何事情直接返回,若没有则创建socket连接,如果是ssl连接,则对封装成ssl socket连接,最后将socket的输入输出流包装成redis的输入输出流并保存在成员变量中,其中outputStream成员变量在接下来的Protocol.sendCommand方法中作为参数传入,源代码如下:

public static void sendCommand(final RedisOutputStream os, final Command command,

   final byte[]... args) {

 sendCommand(os, command.raw, 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);

 }

}

很明显,Protocol的公共方法sendCommand直接调用了私有的sendCommand方法,私有sendCommand方法就是按照通信协议组装命令,Redis的通信协议格式如下:

*<参数数量>CRLF

$<参数1字节数量>CRLF

<参数1>CRLF

...

$<参数N字节数量>CRLF

<参数N>CRLF

例如:set hello world 命令转换成通信协议如下:

*3

$5

hello

$5

world

至此,完成了整个发送过程,除了flush之外,到目前为止尚未真正发送到redis服务端,我们重新回到Jedis的set方法看看后面的语句:

public String set(final String key, String value) {

 checkIsInMultiOrPipeline();

 client.set(key, value);

 return client.getStatusCodeReply();

}

接下来调用 了client的getStatusCodeReply方法获取返回结果,我们跟进去看看

public String getStatusCodeReply() {

 flush();

 pipelinedCommands--;

 final byte[] resp = (byte[]) readProtocolWithCheckingBroken();

 if (null == resp) {

   return null;

 } else {

   return SafeEncoder.encode(resp);

 }

}

getStatusCodeReply其实是Connection中的方法,方法的作用就是将outputStream中的命令真正发送到redis服务端,然后从inputStream中读取返回结果,先看flush方法:

protected void flush() {

 try {

   outputStream.flush();

 } catch (IOException ex) {

   broken = true;

   throw new JedisConnectionException(ex);

 }

}

很简单,直接调用输出流的flush方法,将输出流中缓存的命令发送到服务端再看读取返回值的readProtocolWithCheckingBroken方法:

protected Object readProtocolWithCheckingBroken() {

 try {

   return Protocol.read(inputStream);

 } catch (JedisConnectionException exc) {

   broken = true;

   throw exc;

 }

}

通过协议处理工具类Protocol从输入流中读取结果,继续跟进到Protocol类的read方法

public static Object read(final RedisInputStream is) {

 return process(is);

}

private static Object process(final RedisInputStream is) {

 final byte b = is.readByte();

 if (b == PLUS_BYTE) {

   return processStatusCodeReply(is);

 } else if (b == DOLLAR_BYTE) {

   return processBulkReply(is);

 } else if (b == ASTERISK_BYTE) {

   return processMultiBulkReply(is);

 } else if (b == COLON_BYTE) {

   return processInteger(is);

 } else if (b == MINUS_BYTE) {

   processError(is);

   return null;

 } else {

   throw new JedisConnectionException("Unknown reply: " + (char) b);

 }

}

Redis的返回结果分为如下5种类型:

  • 状态回复:输入流第一个字节为“+”

  • 错误回复:第一个字节为“-”

  • 整数回复:第一个字节为“:”

  • 字符串回复:第一个字节为“$”

  • 多条字符串回复:第一个字节为“*”

这些方法就是解析相应的回复,在此不再分析,都只是按照通信协议解析出结果。

至此,单实例jedis的访问流程解析完成


以上是关于Redis java客户端 jedis 源码分析系列二:单实例 jedis的主要内容,如果未能解决你的问题,请参考以下文章

redis源码解析

Redis:实例结合源码分析Jedis连接池原理以及Jedis连接池的实现

Redis安装以及Java客户端jedis连接不上相关问题解决

分析Jedis源码实现操作非关系型数据库Redis

Redis常见客户端异常汇总(Jedis篇) | 运维进阶

Redis学习之旅--与SpringBoot的结合