说明:本文的源代码是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
自定义的输入流、输出流,提供了追加、读取回车换行字节的便捷操作
2、除了Jedis类之外,还提供了JedisCluster类来支持集群操作,Pipeline、Transaction则是用来处理多个命令和事务