67 views
# Redis 协议介绍 分享人:周丰 本次分享将介绍 redis 服务器和 redis 客户端间的通信协议 (communication protocol)。 主要内容包括: ``` 1. Redis 介绍 2. RESP, 5 种数据类型 3. pipeline,协议如何支持 4. RESP3,新版协议的一些改进和提升 ``` ## 1. redis 介绍 redis 是一个 key value 数据库,可以把 redis 数据库看做是一个 hash 表(编程语言里的 map、dict、object),这个 hash 表以 (key, value) pairs 的形式存储数据。 下面的例子描述了一个 redis 数据库: ``` { "key1": "abc", "key2": ["a","b","a", "c"], "key3": {"a", "b", "c"}, "key4": {"x": "a", "y": "b", "z": "c"}, "key5": {"a": 1.1, "b": 1.2, "c": 1.3} } ``` redis 支持多种 value 类型:string、list、set、zset、hash 等。 ## 2. redis 协议介绍 ### 2.1 什么是 redis 协议 工作模式:request response 模式 ``` client -> server (client 发送命令1) server -> client (server 响应命令1) client -> server (client 发送命令2) server -> client (server 响应命令2) .... ``` redis 目前使用 RESP 协议进行 server 与 client 间通信。 RESP 是 Redis serialization protocol 的缩写。 serialization 是指将程序 runtime 中的数据结构转换为可以存储或传输的格式的过程。之后使用这些数据时,能够从存储或网络中读取出来,并在程序中重新创建出和之前等同的数据结构。 [维基百科](https://en.wikipedia.org/wiki/Serialization)的解释: In computing, serialization (US spelling) or serialisation (UK spelling) is the process of translating a data structure or object state into a format that can be stored (for example, in a file or memory data buffer) or transmitted (for example, over a computer network) and reconstructed later (possibly in a different computer environment). When the resulting series of bits is reread according to the serialization format, it can be used to create a semantically identical clone of the original object. RESP 协议即规范了 redis 客户端和服务器间的 serialization 格式,规定了怎么在两者间通讯的规范。 RESP 协议作为应用层协议,一般用于 TCP 之上。 ### 2.2 为什么需要了解 redis 协议 拿 HTTP 协议做类比,在实际的使用中,我们不直接处理 HTTP 协议的各种细节,而是使用 HTTP 库,我们在开发中使用的是这些库抽象出的一些对象,如request, response,但是了解一些 HTTP 协议对于我们的日常开发是很有用的。Redis 也是一样,各种语言里面也有 redis 库处理 redis 协议,我们大多数时候不需要关心细节。但是学习 redis 协议也能够帮助我们更好的理解 redis 的工作机制,在日常的工作中,遇到一些问题或者需要调试的时候,还是很有帮助的。 另外 redis 协议设计的比较简单,可以作为案例进行学习,对以后理解其它协议或设计协议也能有一些帮助和启发。 ### 2.3. 协议格式 RESP 协议定义了 5 种数据类型。以第一个字节来区分不同的数据类型。 数据都以 `\r\n (<CR><LF>, 0d 0a)` 作为结尾。 redis client 和 server 间传递的信息都是这几种数据类型的数据的组合。 redis 协议支持的 5 种数据类型包括:simple string, bulk string, integer, array, error。 ``` simple string: +<string>\r\n bulk string: $<string_len>\r\n<string>\r\n integer: :<integer>\r\n array: *<array_len>\r\n<item1><item2> error: -<error_message> ``` #### 2.3.1. simple string simple string 第一个字节是 `+`,后面是字符串内容,最后以 `\r\n` 结尾。 如 `+OK\r\n` 是一个 simple string。 在实际的使用中,simple string 用于服务器返回,表示操作的状态:成功/失败。 举例如下: ``` 127.0.0.1:6379> set a b OK ``` ngrep 抓包如下: ``` ➜ sudo ngrep -d lo0 -x port 6379 interface: lo0 (127.0.0.0/255.0.0.0) filter: ( port 6379 ) and (ip || ip6) # T 127.0.0.1:49862 -> 127.0.0.1:6379 [AP] #1 2a 33 0d 0a 24 33 0d 0a 73 65 74 0d 0a 24 31 0d *3..$3..set..$1. 0a 61 0d 0a 24 31 0d 0a 62 0d 0a .a..$1..b.. ## T 127.0.0.1:6379 -> 127.0.0.1:49862 [AP] #3 2b 4f 4b 0d 0a +OK.. # ``` 在 Go 语言的 redis 客户端中,返回的 simple string 被封装为字符串。 #### 2.3.2. bulk string bulk string 第一个字节是 `$`,后面是字符串长度,以 `\r\n` 结束,后面是字符串的具体内容,最后以 `\r\n` 结束。 比如 `$3\r\nfoo\r\n` 是一个 bulk string。 举例如下: ``` 127.0.0.1:6379> get a "b" ``` ngrep 抓包如下: ``` T 127.0.0.1:49862 -> 127.0.0.1:6379 [AP] #5 2a 32 0d 0a 24 33 0d 0a 67 65 74 0d 0a 24 31 0d *2..$3..get..$1. 0a 61 0d 0a .a.. ## T 127.0.0.1:6379 -> 127.0.0.1:49862 [AP] #7 24 31 0d 0a 62 0d 0a $1..b.. # ``` 问题:为什么需要 simple string 和 bulk string 两种字符串呢? :::spoiler 答案 使用场景的不同,simple string 一般用于 redis server 返回状态,表示当前的操作状态是成功/失败,如 set 命令的返回。且 simple string 不是 binary safe 的,不能表示包含 `\r\n` 的字符串。 比如:`+OK\r\n\r\n` 表示字符串 `OK` 还是 `OK\r\n`? bulk string 用于传输 key, value 数据,这种情况下,要求能表示包含特殊字符串的数据。因为 redis 支持 key,value 可以包含任意二进制数据。 simple string 也可以被表示为 bulk string,比如 `+OK\r\n` 可以被表示为 `$2\r\nOK\r\n`,不过前者更加简洁,减少了网络传输,可能也是一种考虑。 ::: --------------------- **NULL string** bulk string 有一个特别的 string: NULL string,表示为 `$-1\r\n`。与长度为0的字符串(`$0\r\n\r\n`)不同,是 null 值。 看下面的例子: ``` 127.0.0.1:6379> get a "" 127.0.0.1:6379> get not_exist (nil) ``` ngrep 抓包为: ``` T 127.0.0.1:53463 -> 127.0.0.1:6379 [AP] #42215 2a 32 0d 0a 24 33 0d 0a 67 65 74 0d 0a 24 31 0d *2..$3..get..$1. 0a 61 0d 0a .a.. ## T 127.0.0.1:6379 -> 127.0.0.1:53463 [AP] #42217 24 30 0d 0a 0d 0a $0.... ## T 127.0.0.1:53463 -> 127.0.0.1:6379 [AP] #42219 2a 32 0d 0a 24 33 0d 0a 67 65 74 0d 0a 24 39 0d *2..$3..get..$9. 0a 6e 6f 74 5f 65 78 69 73 74 0d 0a .not_exist.. ## T 127.0.0.1:6379 -> 127.0.0.1:53463 [AP] #42221 24 2d 31 0d 0a $-1.. ``` 上面的两种场景,在 Go 的 redis 客户端中,分别被表示为长度为0的字符串和 redis.Nil 错误。实际使用时需要注意。 #### 2.3.3 integer integer 第一个字节是 `:`, 后面是具体的数字,并以 `\r\n` 结束。 ``` 127.0.0.1:6379> strlen a (integer) 1 ``` ``` T 127.0.0.1:49862 -> 127.0.0.1:6379 [AP] #17 2a 32 0d 0a 24 36 0d 0a 73 74 72 6c 65 6e 0d 0a *2..$6..strlen.. 24 31 0d 0a 61 0d 0a $1..a.. ## T 127.0.0.1:6379 -> 127.0.0.1:49862 [AP] #19 3a 31 0d 0a :1.. ``` #### 2.3.4 array array 第一个字节是 `*`,然后是一个数字,表示 array 中的元素个数,以 `\r\n` 结尾,后面是 array 中的每个元素。 比如 `*3\r\n$2\r\nab\r\n$1\r\na\r\n$3\r\nxyz\r\n` 是一个 array,表示 ["ab", "a", "xyz"] array 中的各个元素可以是不同的类型。 以上例子中,客户端发送的命令都是 array 类型。 作为命令的返回结果,在 Go 客户端中,array 类型根据场景不同被解析为 map(hash), slice(list、set), redis.Z (zset)。 Go redis 客户端库的部分函数: ``` func (c cmdable) HGetAll(ctx context.Context, key string) *StringStringMapCmd func (c cmdable) LRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd func (c cmdable) SMembers(ctx context.Context, key string) *StringSliceCmd func (c cmdable) ZRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd type Z struct { Score float64 Member interface{} } ``` **empty array 与 null array** empty array: `*0\r\n` null array: `*-1\r\n`,用于某些命令的返回中,表示 null。 两个 null: null array (`*-1\r\n`) 和 null string (`$-1\r\n`) #### 2.3.5 error error 第一个字节是 `-`, 后面是具体的错误信息,最后以 `\r\n` 结尾。 ``` 127.0.0.1:6379> get a b (error) ERR wrong number of arguments for 'get' command 127.0.0.1:6379> llen a (error) WRONGTYPE Operation against a key holding the wrong kind of value ``` ngrep 抓包如下: ``` T 127.0.0.1:49862 -> 127.0.0.1:6379 [AP] #1797 2a 33 0d 0a 24 33 0d 0a 67 65 74 0d 0a 24 31 0d *3..$3..get..$1. 0a 61 0d 0a 24 31 0d 0a 62 0d 0a .a..$1..b.. ## T 127.0.0.1:6379 -> 127.0.0.1:49862 [AP] #1799 2d 45 52 52 20 77 72 6f 6e 67 20 6e 75 6d 62 65 -ERR wrong numbe 72 20 6f 66 20 61 72 67 75 6d 65 6e 74 73 20 66 r of arguments f 6f 72 20 27 67 65 74 27 20 63 6f 6d 6d 61 6e 64 or 'get' command 0d 0a .. T 127.0.0.1:49862 -> 127.0.0.1:6379 [AP] #1813 2a 32 0d 0a 24 34 0d 0a 6c 6c 65 6e 0d 0a 24 31 *2..$4..llen..$1 0d 0a 61 0d 0a ..a.. ## T 127.0.0.1:6379 -> 127.0.0.1:49862 [AP] #1815 2d 57 52 4f 4e 47 54 59 50 45 20 4f 70 65 72 61 -WRONGTYPE Opera 74 69 6f 6e 20 61 67 61 69 6e 73 74 20 61 20 6b tion against a k 65 79 20 68 6f 6c 64 69 6e 67 20 74 68 65 20 77 ey holding the w 72 6f 6e 67 20 6b 69 6e 64 20 6f 66 20 76 61 6c rong kind of val 75 65 0d 0a ue.. ``` 在 redis server 的具体实现中,`-` 后面有时会有一个单词描述具体的错误类型(一般是只包含大写字母),然后才是具体的错误信息。这个描述错误信息的单词叫做 error prefix,客户端可以据此区分错误类型,但是这些错误类型并没有在协议中进行严格的说明,只是一种习惯用法。 以上就是 redis 协议支持的所有数据类型:simple string、bulk string、integer、error、array。 问题:如何表示 float 类型,如 zset 中的 score? :::spoiler 答案 float类型被表示为 bulk string ``` 127.0.0.1:6379> zrange zseta 0 -1 withscores 1) "a" 2) "1.5" 3) "b" 4) "2" 5) "c" 6) "10" T 127.0.0.1:50727 -> 127.0.0.1:6379 [AP] #1 2a 35 0d 0a 24 36 0d 0a 7a 72 61 6e 67 65 0d 0a *5..$6..zrange.. 24 35 0d 0a 7a 73 65 74 61 0d 0a 24 31 0d 0a 30 $5..zseta..$1..0 0d 0a 24 32 0d 0a 2d 31 0d 0a 24 31 30 0d 0a 77 ..$2..-1..$10..w 69 74 68 73 63 6f 72 65 73 0d 0a ithscores.. ## T 127.0.0.1:6379 -> 127.0.0.1:50727 [AP] #3 2a 36 0d 0a 24 31 0d 0a 61 0d 0a 24 33 0d 0a 31 *6..$1..a..$3..1 2e 35 0d 0a 24 31 0d 0a 62 0d 0a 24 31 0d 0a 32 .5..$1..b..$1..2 0d 0a 24 31 0d 0a 63 0d 0a 24 32 0d 0a 31 30 0d ..$1..c..$2..10. 0a . # ``` ``` 127.0.0.1:6379> INCRBYFLOAT float_key 20.22 "20.22" T 127.0.0.1:53463 -> 127.0.0.1:6379 [AP] #42227 2a 33 0d 0a 24 31 31 0d 0a 49 4e 43 52 42 59 46 *3..$11..INCRBYF 4c 4f 41 54 0d 0a 24 39 0d 0a 66 6c 6f 61 74 5f LOAT..$9..float_ 6b 65 79 0d 0a 24 35 0d 0a 32 30 2e 32 32 0d 0a key..$5..20.22.. ## T 127.0.0.1:6379 -> 127.0.0.1:53463 [AP] #42229 24 35 0d 0a 32 30 2e 32 32 0d 0a $5..20.22.. ``` ::: -------------------------------- ## 3. pipeline 介绍 pipeline 模式,以及看一下 pipeline 模式下的协议数据格式。 redis 协议在使用时采用了 request、response 模型,但是在实际的使用中,redis 支持 pipeline 模式。 ``` client -> server (client 发送命令1) server -> client (server 响应命令1) client -> server (client 发送命令2) server -> client (server 响应命令2) .... ``` pipeline 模式指 client 发送多条命令,服务器进行处理,处理完成后一次性返回所有命令的响应,多条响应按照请求的顺序排序。 ``` client -> server (client 发送命令1..N) server -> client (server 响应命令1..N) client -> server (client 发送命令N+1..2N) server -> client (server 响应命令N+1..2N) .... ``` **RESP 协议对 pipeline 的支持** RESP 协议没有对 pipeline 模式进行额外的处理,如下所示是 pipeline 模式下处理`set a abc` 和 `get a`两条命令的网络包: ``` T 127.0.0.1:51785 -> 127.0.0.1:6379 [AP] #7 2a 33 0d 0a 24 33 0d 0a 73 65 74 0d 0a 24 31 0d *3..$3..set..$1. 0a 61 0d 0a 24 33 0d 0a 61 62 63 0d 0a 2a 32 0d .a..$3..abc..*2. 0a 24 33 0d 0a 67 65 74 0d 0a 24 31 0d 0a 61 0d .$3..get..$1..a. 0a . ## T 127.0.0.1:6379 -> 127.0.0.1:51785 [AP] #9 2b 4f 4b 0d 0a 24 33 0d 0a 61 62 63 0d 0a +OK..$3..abc.. ``` 如果 pipeline 中有命令出错,并不会影响后面的命令继续执行。 下面的例子在一个 pipeline 中执行了 3 条命令: ``` set a abc hgetall a get a ``` 其中第二条命令会返回错误,但是第三条命令仍返回了。 ``` T 127.0.0.1:52641 -> 127.0.0.1:6379 [AP] #35 2a 33 0d 0a 24 33 0d 0a 73 65 74 0d 0a 24 31 0d *3..$3..set..$1. 0a 61 0d 0a 24 33 0d 0a 61 62 63 0d 0a 2a 32 0d .a..$3..abc..*2. 0a 24 37 0d 0a 68 67 65 74 61 6c 6c 0d 0a 24 31 .$7..hgetall..$1 0d 0a 61 0d 0a 2a 32 0d 0a 24 33 0d 0a 67 65 74 ..a..*2..$3..get 0d 0a 24 31 0d 0a 61 0d 0a ..$1..a.. ## T 127.0.0.1:6379 -> 127.0.0.1:52641 [AP] #37 2b 4f 4b 0d 0a 2d 57 52 4f 4e 47 54 59 50 45 20 +OK..-WRONGTYPE 4f 70 65 72 61 74 69 6f 6e 20 61 67 61 69 6e 73 Operation agains 74 20 61 20 6b 65 79 20 68 6f 6c 64 69 6e 67 20 t a key holding 74 68 65 20 77 72 6f 6e 67 20 6b 69 6e 64 20 6f the wrong kind o 66 20 76 61 6c 75 65 0d 0a 24 33 0d 0a 61 62 63 f value..$3..abc 0d 0a .. ``` :::spoiler 附 Go 代码: ``` package main import ( "context" "fmt" "github.com/go-redis/redis/v8" ) func main() { opts := &redis.Options{Addr: "localhost:6379"} client := redis.NewClient(opts) ctx := context.TODO() key := "a" cmds, err := client.Pipelined(ctx, func(p redis.Pipeliner) error { if _, err := p.Set(ctx, key, "abc", 0).Result(); err != nil { return err } if _, err := p.HGetAll(ctx, key).Result(); err != nil { return err } if _, err := p.Get(ctx, key).Result(); err != nil { return err } return nil }) if err != nil { fmt.Printf("pipeline error %s\n", err) } for _, cmd := range cmds { switch command := cmd.(type) { case *redis.StatusCmd: result, err := command.Result() if err != nil { fmt.Printf("command %s error %s\n", command.String(), err) } else { fmt.Printf("command %s result %s\n", command.String(), result) } case *redis.StringStringMapCmd: result, err := command.Result() if err != nil { fmt.Printf("command %s error %s\n", command.String(), err) } else { fmt.Printf("command %s result %s\n", command.String(), result) } case *redis.StringCmd: result, err := command.Result() if err != nil { fmt.Printf("command %s error %s\n", command.String(), err) } else { fmt.Printf("command %s result %s\n", command.String(), result) } } } } ``` 程序输出: ``` pipeline error WRONGTYPE Operation against a key holding the wrong kind of value command set a abc: OK result OK command hgetall a: WRONGTYPE Operation against a key holding the wrong kind of value error WRONGTYPE Operation against a key holding the wrong kind of value command get a: abc result abc ``` ::: ------------------------- 建议:能用 pipeline 时就用,特别是在命令较多的情况,能极大提升性能。 使用限制:命令间不能有依赖。 ::: spoiler pipeline 提升性能的例子 当使用 pipeline 时,性能会有所提升,示例如下所示。 例子中对比了没用 pipeline 以及不同 pipeline size 时,程序处理 10000条 Get 命令所用的时长。 ``` package main import ( "context" "fmt" "time" "github.com/go-redis/redis/v8" ) func main() { opts := &redis.ClusterOptions{ Addrs: []string{"bytepower-server-redis-debug.qvy2lg.clustercfg.cnw1.cache.amazonaws.com.cn:6379"}} client := redis.NewClusterClient(opts) ctx := context.TODO() key := "a" commandCount := 10000 // no pipeline startTime := time.Now() for i := 0; i < commandCount; i++ { client.Get(ctx, key) } duration := time.Since(startTime) fmt.Printf("no pipline tasks %s\n", duration) // different pipeline sizes pipelineSizes := []int{50, 200, 500, 1000} for _, pipelineSize := range pipelineSizes { startTime = time.Now() for i := 0; i < commandCount/pipelineSize; i++ { pipeline := client.Pipeline() for j := 0; j < pipelineSize; j++ { pipeline.Get(ctx, key) } pipeline.Exec(ctx) } duration = time.Since(startTime) fmt.Printf("pipeline with size %d takes %s\n", pipelineSize, duration) } } ``` 程序输出如下: ``` no pipline tasks 986.422169ms pipeline with size 50 takes 40.275496ms pipeline with size 200 takes 21.251794ms pipeline with size 500 takes 15.70927ms pipeline with size 1000 takes 16.133405ms ``` 问题:pipeline 性能提升的原因是什么? 1、网络延迟 假设 rtt (round-trip time) 是 t,那么 N 条命令,依次发送的话,网络所耗的时间是 t * N 如果 pipeline size 是 M,那么网络所消耗的时间是 t * (N/M) ``` client -> server (client 发送命令 1) server -> client (server 响应命令 1) client -> server (client 发送命令 2) server -> client (server 响应命令 2) ... ``` ``` client -> server (client 发送命令 1..M) server -> client (server 响应命令 1..M) client -> server (client 发送命令 M+1..2M) server -> client (server 响应命令 M+1..2M) .... ``` 2、系统调用 server 每次从 socket 上读写数据时,需要进行系统调用(read/write)陷入内核态,相比用户态的函数调用,系统调用代价较高,使用 pipeline 后,server 可以一次读/写多个命令的数据,减少了系统调用次数。 ::: -------------------------------- ## 4. RESP3 从 redis 6.0 开始,redis 服务器支持两种协议,RESP 和 RESP3。 RESP3 是 RESP 的一个超集,在 RESP 的基础上,RESP3 有一些改进。几处主要的改进包括: 1. 除 array 外,添加了其它的复杂类型,包括 set(`~` 开头) 和 map(`%` 开头) 2. 添加了 新的 null,来替换两种 null ($-1\r\n 和 *-1\r\n) 3. 添加了 bool 类型(`#t\r\n` 和 `#f\r\n`) 和 double 类型(`,` 开头) 另外还有其它的一些改进,具体可以参考[文档](https://github.com/antirez/RESP3/blob/master/spec.md)。 连接建立时默认是 RESP,可以用 hello 命令切换模式至 RESP3。 ``` 127.0.0.1:6379> hello 3 1# "server" => "redis" 2# "version" => "6.0.9" 3# "proto" => (integer) 3 4# "id" => (integer) 4 5# "mode" => "standalone" 6# "role" => "master" 7# "modules" => (empty array) ``` 对比 RESP 协议和 RESP3 协议如何表示 redis 的各种数据类型 **1 set 类型表示的变化** RESP ``` 127.0.0.1:6379> smembers seta 1) "c" 2) "b" 3) "a" 4) "d" ``` ``` T 127.0.0.1:49862 -> 127.0.0.1:6379 [AP] #124601 2a 32 0d 0a 24 38 0d 0a 73 6d 65 6d 62 65 72 73 *2..$8..smembers 0d 0a 24 34 0d 0a 73 65 74 61 0d 0a ..$4..seta.. ## T 127.0.0.1:6379 -> 127.0.0.1:49862 [AP] #124603 2a 34 0d 0a 24 31 0d 0a 63 0d 0a 24 31 0d 0a 62 *4..$1..c..$1..b 0d 0a 24 31 0d 0a 61 0d 0a 24 31 0d 0a 64 0d 0a ..$1..a..$1..d.. ``` RESP3 ``` 127.0.0.1:6379> smembers seta 1~ "c" 2~ "b" 3~ "a" 4~ "d" T 127.0.0.1:49862 -> 127.0.0.1:6379 [AP] #124625 2a 32 0d 0a 24 38 0d 0a 73 6d 65 6d 62 65 72 73 *2..$8..smembers 0d 0a 24 34 0d 0a 73 65 74 61 0d 0a ..$4..seta.. ## T 127.0.0.1:6379 -> 127.0.0.1:49862 [AP] #124627 7e 34 0d 0a 24 31 0d 0a 63 0d 0a 24 31 0d 0a 62 ~4..$1..c..$1..b 0d 0a 24 31 0d 0a 61 0d 0a 24 31 0d 0a 64 0d 0a ..$1..a..$1..d.. ``` **2. zset 类型表示的变化** RESP: ``` 127.0.0.1:6379> ZRANGE zseta 0 -1 withscores 1) "a" 2) "1.5" 3) "b" 4) "2" 5) "c" 6) "10" T 127.0.0.1:49862 -> 127.0.0.1:6379 [AP] #124605 2a 35 0d 0a 24 36 0d 0a 5a 52 41 4e 47 45 0d 0a *5..$6..ZRANGE.. 24 35 0d 0a 7a 73 65 74 61 0d 0a 24 31 0d 0a 30 $5..zseta..$1..0 0d 0a 24 32 0d 0a 2d 31 0d 0a 24 31 30 0d 0a 77 ..$2..-1..$10..w 69 74 68 73 63 6f 72 65 73 0d 0a ithscores.. ## T 127.0.0.1:6379 -> 127.0.0.1:49862 [AP] #124607 2a 36 0d 0a 24 31 0d 0a 61 0d 0a 24 33 0d 0a 31 *6..$1..a..$3..1 2e 35 0d 0a 24 31 0d 0a 62 0d 0a 24 31 0d 0a 32 .5..$1..b..$1..2 0d 0a 24 31 0d 0a 63 0d 0a 24 32 0d 0a 31 30 0d ..$1..c..$2..10. 0a . ``` RESP3 ``` 127.0.0.1:6379> ZRANGE zseta 0 -1 withscores 1) 1) "a" 2) (double) 1.5 2) 1) "b" 2) (double) 2 3) 1) "c" 2) (double) 10 T 127.0.0.1:49862 -> 127.0.0.1:6379 [AP] #124629 2a 35 0d 0a 24 36 0d 0a 5a 52 41 4e 47 45 0d 0a *5..$6..ZRANGE.. 24 35 0d 0a 7a 73 65 74 61 0d 0a 24 31 0d 0a 30 $5..zseta..$1..0 0d 0a 24 32 0d 0a 2d 31 0d 0a 24 31 30 0d 0a 77 ..$2..-1..$10..w 69 74 68 73 63 6f 72 65 73 0d 0a ithscores.. ## T 127.0.0.1:6379 -> 127.0.0.1:49862 [AP] #124631 2a 33 0d 0a 2a 32 0d 0a 24 31 0d 0a 61 0d 0a 2c *3..*2..$1..a.., 31 2e 35 0d 0a 2a 32 0d 0a 24 31 0d 0a 62 0d 0a 1.5..*2..$1..b.. 2c 32 0d 0a 2a 32 0d 0a 24 31 0d 0a 63 0d 0a 2c ,2..*2..$1..c.., 31 30 0d 0a 10.. ``` **3. hash 类型表示的变化** RESP ``` 127.0.0.1:6379> hgetall hasha 1) "field_1" 2) "value_1" 3) "field_2" 4) "value_2" T 127.0.0.1:49862 -> 127.0.0.1:6379 [AP] #124641 2a 32 0d 0a 24 37 0d 0a 68 67 65 74 61 6c 6c 0d *2..$7..hgetall. 0a 24 35 0d 0a 68 61 73 68 61 0d 0a .$5..hasha.. ## T 127.0.0.1:6379 -> 127.0.0.1:49862 [AP] #124643 2a 34 0d 0a 24 37 0d 0a 66 69 65 6c 64 5f 31 0d *4..$7..field_1. 0a 24 37 0d 0a 76 61 6c 75 65 5f 31 0d 0a 24 37 .$7..value_1..$7 0d 0a 66 69 65 6c 64 5f 32 0d 0a 24 37 0d 0a 76 ..field_2..$7..v 61 6c 75 65 5f 32 0d 0a alue_2.. ``` RESP3 ``` 127.0.0.1:6379> hgetall hasha 1# "field_1" => "value_1" 2# "field_2" => "value_2" T 127.0.0.1:49862 -> 127.0.0.1:6379 [AP] #124633 2a 32 0d 0a 24 37 0d 0a 68 67 65 74 61 6c 6c 0d *2..$7..hgetall. 0a 24 35 0d 0a 68 61 73 68 61 0d 0a .$5..hasha.. ## T 127.0.0.1:6379 -> 127.0.0.1:49862 [AP] #124635 25 32 0d 0a 24 37 0d 0a 66 69 65 6c 64 5f 31 0d %2..$7..field_1. 0a 24 37 0d 0a 76 61 6c 75 65 5f 31 0d 0a 24 37 .$7..value_1..$7 0d 0a 66 69 65 6c 64 5f 32 0d 0a 24 37 0d 0a 76 ..field_2..$7..v 61 6c 75 65 5f 32 0d 0a alue_2.. ``` 多添加了几种类型,更方便 redis 客户端库进行类型转换。 ## 5. 总结 分享了 redis 协议 RESP 的协议格式,讨论了 pipeline 以及新版的协议 RESP3。 ## 6. 参考 1. [RESP 协议官方文档](https://redis.io/docs/reference/protocol-spec/) 2. [Pipeline document](https://redis.io/docs/manual/pipelining/) 3. [RESP3 协议文档](https://github.com/antirez/RESP3/blob/master/spec.md)