jedis源码解析

说明:本文的源代码是3.1.0版本


    redis.clients
    jedis
    3.1.0

结论

总的来说,Jedis原理大概是这样的:
a、首先创建Jedis
创建时需要提供JedisShardInfo类封装的一些信息,用于连接到redis服务器。创建Jedis时会同时创建一个Client对象,这个对象封装了和Redis服务器的连接,其中有Socket、自定义的输入输出流等。客户端与服务端通信的协议被封装在Protocol类中,其中有具体的通信协议。

b、然后通过Jedis执行redis命令
其实就是将要执行的命令以redis制定的通信协议发送出去即可。发送方式是传统的Socket的输入输出流,发送内容则是在Protocol中实现的通信协议。

c、最后关闭Jedis
关闭Socket流等

源码追踪

说完了结论,再看源码,仔细分析。
a、创建Jedis对象,有多个构造方法,构造参数封装在JedisShardInfo类中。Jedis对象创建时会同时创建一个Client对象。

public Jedis(HostAndPort hp) {
    super(hp);
}
public Jedis(JedisShardInfo shardInfo) {
     super(shardInfo);
}

父类BianryJedis构造方法
public BinaryJedis(String host) {
    this.client = null;
    this.transaction = null;
    this.pipeline = null;
    this.dummyArray = new byte[0][];
    URI uri = URI.create(host);
    if (JedisURIHelper.isValid(uri)) {
        this.initializeClientFromURI(uri);
    } else {
        this.client = new Client(host);
    }

}
JedisShardInfo类中的属性

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

b、调用jedis的方法,所有的jedis方法大概都长下面这个样子,先检查是否在multi或者pipeline中,如果在,就会报错;然后调用client对应的方法;最后从socket中拿返回值

public String set(String key, String value) {
        this.checkIsInMultiOrPipeline();
        this.client.set(key, value);
        return this.client.getStatusCodeReply();
    }

c、Client.set
Client继承自BinaryClient,后者继承自Connection,这个Client可以理解为一个客户端连接的封装
Connection中持有封装了Socket输入流、输出流的两个自定义的流操作类

    public void set(String key, String value) {
        //将字符串用UTF8字符集解码为字节数组,然后调用父类的方法
        this.set(SafeEncoder.encode(key), SafeEncoder.encode(value));
    }
public void set(byte[] key, byte[] value) {
        //调用父类的方法,以redis要求的格式发送命令
        //所有的redis客户端命令被封装到了Protocol.Command枚举类,其实就是命令的字节数组的封装
        this.sendCommand(Command.SET, new byte[][]{key, value});
    }
public void sendCommand(ProtocolCommand cmd, byte[]... args) {
        try {
            //客户端发送第一条命令之前,需要先连接到redis服务器,后面同一个jedis就不需要再连接了
            this.connect();
            Protocol.sendCommand(this.outputStream, cmd, args);
        } catch (JedisConnectionException var6) {
            ...
        }
    }
public void connect() {
        if (!this.isConnected()) {
            try {
                this.socket = new Socket();//创建一个java.net.Socket,用于连接redis服务器
                this.socket.setReuseAddress(true);
                this.socket.setKeepAlive(true);
                this.socket.setTcpNoDelay(true);
                this.socket.setSoLinger(true, 0);
                this.socket.connect(new InetSocketAddress(this.host, this.port), this.connectionTimeout);
                this.socket.setSoTimeout(this.soTimeout);
                if (this.ssl) {
                    if (null == this.sslSocketFactory) {
                        this.sslSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault();
                    }

                    this.socket = this.sslSocketFactory.createSocket(this.socket, this.host, this.port, true);
                    if (null != this.sslParameters) {
                        ((SSLSocket)this.socket).setSSLParameters(this.sslParameters);
                    }

                    if (null != this.hostnameVerifier && !this.hostnameVerifier.verify(this.host, ((SSLSocket)this.socket).getSession())) {
                        String message = String.format("The connection to '%s' failed ssl/tls hostname verification.", this.host);
                        throw new JedisConnectionException(message);
                    }
                }

                //将socket的输入流、输出流封装到自定义的流处理类,这个类会提供一些读、写字节比较方便的操作
                this.outputStream = new RedisOutputStream(this.socket.getOutputStream());
                this.inputStream = new RedisInputStream(this.socket.getInputStream());
            } catch (IOException var2) {
                this.broken = true;
                throw new JedisConnectionException("Failed connecting to host " + this.host + ":" + this.port, var2);
            }
        }
    }

d、Protocol.sendCommand 这里封装了redis服务端和客户端的通信协议

public static void sendCommand(RedisOutputStream os, ProtocolCommand command, byte[]... args) {
        sendCommand(os, command.getRaw(), args);
    }
private static void sendCommand(RedisOutputStream os, byte[] command, byte[]... args) {
        //对于set a b 这样一个命令,发送出去的格式大概是这样的:
        //*3\r\n$1a\r\n$1b\r\n
        try {
            os.write((byte)42);
            os.writeIntCrLf(args.length + 1);
            os.write((byte)36);
            os.writeIntCrLf(command.length);
            os.write(command);
            os.writeCrLf();
            byte[][] var3 = args;
            int var4 = args.length;

            for(int var5 = 0; var5 < var4; ++var5) {
                byte[] arg = var3[var5];
                os.write((byte)36);
                os.writeIntCrLf(arg.length);
                os.write(arg);
                os.writeCrLf();
            }

        } catch (IOException var7) {
            throw new JedisConnectionException(var7);
        }
    }

redis通信协议比较简单,全部规则如下:
RESP 是 Redis 序列化协议的简写。它是一种直观的文本协议,优势在于实现异常简单,解析性能极好
Redis 协议将传输的结构数据分为 5 种最小单元类型,单元结束时统一加上回车换行符号\r\n。

单行字符串 以 + 符号开头。 
多行字符串 以 $ 符号开头,后跟字符串长度。 
整数值 以 : 符号开头,后跟整数的字符串形式。 
错误消息 以 - 符号开头。 
数组 以 * 号开头,后跟数组的长度

例子

单行字符串 hello world +hello world\r\n 
多行字符串 hello world $11\r\nhello world\r\n 
多行字符串当然也可以表示单行字符串。 
整数 1024 :1024\r\n 
错误 参数类型错误 -WRONGTYPE Operation against a key holding the wrong kind of value\r\n 
数组 [1,2,3] *3\r\n:1\r\n:2\r\n:3\r\n 
NULL 用多行字符串表示,不过长度要写成-1。 $-1\r\n 
空串 用多行字符串表示,长度填 0。 $0\r\n\r\n
注意这里有两个\r\n。为什么是两个?因为两个\r\n之间,隔的是空串。

e、Client.getStatusCodeReply 接收从redis服务器返回的字符串

public String getStatusCodeReply() {
        this.flush();//刷新流,socket缓冲区中的数据才能被读到
        byte[] resp = (byte[])((byte[])this.readProtocolWithCheckingBroken());
        return null == resp ? null : SafeEncoder.encode(resp);//将字节数组编码为字符串
    }
 protected Object readProtocolWithCheckingBroken() {
        if (this.broken) {
            throw new JedisConnectionException("Attempting to read from a broken connection");
        } else {
            try {
                //从这里可以看出,Protocol类的职责,它负责与Redis服务器通信,包括发送命令、接收返回值
                //返回值的格式和发送命令的格式是一样的,使用同一套规则
                return Protocol.read(this.inputStream);
            } catch (JedisConnectionException var2) {
                this.broken = true;
                throw var2;
            }
        }
    }

f、RedisInputStream&RedisOutputStream
自定义的输入流、输出流,提供了追加、读取回车换行字节的便捷操作

image.png

2、除了Jedis类之外,还提供了JedisCluster类来支持集群操作,Pipeline、Transaction则是用来处理多个命令和事务

你可能感兴趣的:(jedis源码解析)