RESP协议
简介
Redis是一个CS架构的软件,通信一般分两步(不包括pipeline和PubSub):
- 客户端(client)向服务端(server)发送一条命令;
- 服务端解析并执行命令,返回响应结果给客户端;
客户端发送命令的格式、服务端响应结果的格式必须有一个规范,即通信协议。
RESP(Redis Serialization Protocol) 是 Redis 用于客户端和服务器之间通信的序列化协议。它定义了命令和数据的编码方式,使得 Redis 的命令可以被高效地传输和解析。
- Redis 1.2版本引入了RESP协议;
- Redis 2.0版本中成为与Redis服务端通信的标准,称为RESP2;
- Redis 6.0版本中,从RESP2升级到了RESP3协议,增加了更多数据类型并且支持6.0的新特性–客户端缓存;
目前默认使用的依然是RESP2协议。
协议格式
在RESP中,通过首字节的字符来区分不同数据类型,常用的数据类型包括5种:
-
**简单字符串:**以 +
开头,后跟字符串内容,以 \r\n
结尾。
-
**错误:**首字节是 ‘-’ ,与单行字符串格式一样,只是字符串是异常信息。
1
| -ERR unknown command 'foobar'\r\n
|
-
**数值:**首字节是 ‘:’ ,后面跟上数字格式的字符串,以CRLF结尾。
-
**多行字符串(Bulk String):**以上几种数据类型读到换行符后便会结束,非二进制安全。多行字符串首字节是$
,后跟字符串的字节总数,以 \r\n
分隔,再跟字符串内容,最后以 \r\n
结尾。由于它是按照字节数量读取数据的,因此是二进制安全的,最大支持512MB。
注意两种特殊情况:
-
如果大小为0,则代表空字符串:
-
如果大小为-1,则代表不存在:
-
**数组:**以 *
开头,后跟数组长度,以 \r\n
分隔,再跟数组中的每个元素(可以是任意类型),每个元素以 \r\n
分隔:
1 2 3 4 5
| *2\r\n $3\r\n foo\r\n $3\r\n bar\r\n
|
基于Socket自定义Redis客户端
Redis支持TCP通信,因此我们可以使用Socket来模拟客户端,与Redis服务端建立连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| public class Main {
static Socket s; static PrintWriter writer; static BufferedReader reader;
public static void main(String[] args) { try { String host = "192.168.150.101"; int port = 6379; s = new Socket(host, port); writer = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), StandardCharsets.UTF_8)); reader = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8));
sendRequest("auth", "123321"); Object obj = handleResponse(); System.out.println("obj = " + obj);
sendRequest("set", "name", "penciy"); obj = handleResponse(); System.out.println("obj = " + obj);
sendRequest("get", "name"); obj = handleResponse(); System.out.println("obj = " + obj);
sendRequest("mget", "name", "num", "msg"); obj = handleResponse(); System.out.println("obj = " + obj); } catch (IOException e) { e.printStackTrace(); } finally { try { if (reader != null) reader.close(); if (writer != null) writer.close(); if (s != null) s.close(); } catch (IOException e) { e.printStackTrace(); } } }
private static Object handleResponse() throws IOException { int prefix = reader.read(); switch (prefix) { case '+': return reader.readLine(); case '-': throw new RuntimeException(reader.readLine()); case ':': return Long.parseLong(reader.readLine()); case '$': int len = Integer.parseInt(reader.readLine()); if (len == -1) { return null; } if (len == 0) { return ""; } return reader.readLine(); case '*': return readBulkString(); default: throw new RuntimeException("错误的数据格式!"); } }
private static Object readBulkString() throws IOException { int len = Integer.parseInt(reader.readLine()); if (len <= 0) { return null; } List<Object> list = new ArrayList<>(len); for (int i = 0; i < len; i++) { list.add(handleResponse()); } return list; }
private static void sendRequest(String ... args) { writer.println("*" + args.length); for (String arg : args) { writer.println("$" + arg.getBytes(StandardCharsets.UTF_8).length); writer.println(arg); } writer.flush(); } }
|
比如现在客户端需要执行redis命令set name penciy
,那么最终需要在socket的输出流中写入:
1 2 3 4
| *3\r\n $3\r\nset\r\n $4\r\nname\r\n $6\r\npenciy\r\n
|
响应会返回到socket的输入流,客户端需要根据响应的首字节,实现不同的解析方式。比较复杂的是Bulk String的情况,采用了递归方式进行解析。
RESP协议和AOF持久化的关系
首先复习一下AOF 文件的用途:
- Redis 支持两种持久化机制:RDB 和 AOF,它们可以单独使用,也可以同时使用。
- AOF 文件记录了 Redis 服务器执行的所有写操作命令,这些命令在服务器重启时会被重新执行,从而恢复数据。如果同时启用了 RDB 和 AOF,Redis 重启时会优先使用 AOF 文件恢复数据。
- 在 Redis 的主从复制(Master-Replica)场景中,增量更新的同步机制主要依赖于AOF 文件,而全量更新依赖于RDB文件。
可以发现,AOF 文件中存储的是 Redis 服务器执行的命令序列,每个命令都以 RESP 格式编码,运行命令 hset user age 23
,AOF文件中的内容如下所示(注意要先修改配置文件或在redis-cli中配置appendonly为yes)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| *2 $6 SELECT $1 0 *4 $4 hset $4 user $3 age $2 23
|
AOF以RESP格式存储的好处有:
- 使用 RESP 格式可以确保这些命令与 Redis 客户端和服务器之间的通信格式一致,从而保证兼容性。
- RESP 格式是文本格式,易于阅读和调试。这使得 AOF 文件可以直接被人类阅读和编辑,便于排查问题。
- RESP 格式简单明了,易于实现。Redis 服务器可以直接将命令序列化为 RESP 格式并写入 AOF 文件,而无需额外的转换逻辑。
注意,原始的AOF文件是以 RESP 协议格式存储命令序列且看上去直观易懂,而一旦AOF Rewrite之后,文件中的命令会进行优化和压缩,例如几条命令合并为一条命令、采用压缩编码等,但其还是采用的RESP协议。
__END__