jedis源码解析

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

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.1.0</version>
</dependency>

结论

总的来说,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则是用来处理多个命令和事务

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容