RESP协议

简介

Redis是一个CS架构的软件,通信一般分两步(不包括pipeline和PubSub):

  1. 客户端(client)向服务端(server)发送一条命令;
  2. 服务端解析并执行命令,返回响应结果给客户端;

客户端发送命令的格式、服务端响应结果的格式必须有一个规范,即通信协议。

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种:

  1. **简单字符串:**以 + 开头,后跟字符串内容,以 \r\n 结尾。

    1
    +OK\r\n
  2. **错误:**首字节是 ‘-’ ,与单行字符串格式一样,只是字符串是异常信息。

    1
    -ERR unknown command 'foobar'\r\n
  3. **数值:**首字节是 ‘:’ ,后面跟上数字格式的字符串,以CRLF结尾。

    1
    :12345\r\n
  4. **多行字符串(Bulk String):**以上几种数据类型读到换行符后便会结束,非二进制安全。多行字符串首字节是$,后跟字符串的字节总数,以 \r\n 分隔,再跟字符串内容,最后以 \r\n 结尾。由于它是按照字节数量读取数据的,因此是二进制安全的,最大支持512MB。

    1
    $5\r\nhello\r\n

    注意两种特殊情况:

    • 如果大小为0,则代表空字符串:

      1
      $0\r\n\r\n
    • 如果大小为-1,则代表不存在:

      1
      $-1\r\n
  5. **数组:**以 * 开头,后跟数组长度,以 \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 {
// 1.建立连接
String host = "192.168.150.101";
int port = 6379;
s = new Socket(host, port);
// 2.获取输出流、输入流
writer = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), StandardCharsets.UTF_8));
reader = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8));

// 3.发出请求
// 3.1.获取授权 auth 123321
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");
// 4.解析响应
obj = handleResponse();
System.out.println("obj = " + obj);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 5.释放连接
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 "";
}
// 再读数据,读len个字节。我们假设没有特殊字符,所以读一行(简化)
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;
}

// 根据可变参数args进行序列化,构建RESP格式的redis命令
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格式存储的好处有:

  1. 使用 RESP 格式可以确保这些命令与 Redis 客户端和服务器之间的通信格式一致,从而保证兼容性。
  2. RESP 格式是文本格式,易于阅读和调试。这使得 AOF 文件可以直接被人类阅读和编辑,便于排查问题。
  3. RESP 格式简单明了,易于实现。Redis 服务器可以直接将命令序列化为 RESP 格式并写入 AOF 文件,而无需额外的转换逻辑。

注意,原始的AOF文件是以 RESP 协议格式存储命令序列且看上去直观易懂,而一旦AOF Rewrite之后,文件中的命令会进行优化和压缩,例如几条命令合并为一条命令、采用压缩编码等,但其还是采用的RESP协议。

__END__