从RESP的角度理解事务和管道

1. RESP 是什么?

16 进制 0d 0a 就是 \r\n

RESP 就是 Redis 服务端和客户端之间进行通信的协议,它是建立在 TCP 之上的一种简单的应用层协议。你可以把它理解成 HTTP 协议,不过它更加的简单。

它支持很多数据类型,这里列举几个常用的:

  • Simple Strings 简单字符串
  • Bulk Strings 批量字符串
  • Arrays 数组

简单字符串是格式是:以 + 开始,接着是字符串(不允许含有 \r 或者 \n),结尾是 \r\n。它用于传输短的,极小开销的非二进制字符串。例如:+OK\r\n,这个通常是用于服务端基于客户端的响应。如下图所示:

在这里插入图片描述

批量字符串的格式是:以 $ 开始,接着是字符串的长度,\r\n 分隔,字符串本身,\r\n 结尾。例如:$5\r\nhello\r\n,获取上次设置的键值:

在这里插入图片描述

这是抓包获取的结果,右下角红色框选的即为服务器的响应的解码形式(左侧为16进制形式)。你在程序层面看到的是 CrazyDragon,但是在网络层面它是这样的形势:$11\r\nCrazyDragon\r\n

在这里插入图片描述

数组的格式是:以 * 开头,接着是数组元素的个数,\r\n 分隔,然后是数组的每一个元素。例如:*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n 表示 "hello""world" 两个字符串。现在我们用批量命令 mget 获取两个 author:001author:002 的值。

在这里插入图片描述

同样的抓包分析它,可以看到返回的即是 Arrays 格式的数据,把它写成这样的形式就更加明了了:*2\r\n$11\r\nCrazyDragon\r\n$3\r\nTom\r\n,只不过它不显示转义符。

在这里插入图片描述

这里只是简单介绍一下 RESP,具体的规范请直接查看官方文档,并自己抓包分析:

Redis serialization protocol specification

2. Redis 的管道和事务

在 Redis 中,实现批量操作可以通过管道或者事务来实现,它的主要区别是:

  1. Redis 的 pipeline 是客户端行为,对于服务器来说是透明的。它就是当客户端需要执行多条 redis 命令时,通过管道一次性将要执行的多条命令发送给服务端。
  2. Reeis 的事务是服务器行为,它是通过 MULTI EXEC WATCH DISCARD 来完成的。当用户输入 MULTI,接下来输入的指令,会被服务端存储起来,最后当作一个原子性的指令执行。

刚才在第一部分,我们已经知道了它们之间的通信协议。所以,我们就可以从网络的层次来了解这个过程,因为之前这些区别都是从文本获取的,大多数人只是机械的记忆了区别,却没有真正的看过它们的区别。既然我们可以通过网络抓包来分析,那么就来做一些有意义的事情吧!所以,我们就来从 RESP 的角度来看一下它们的区别。

2.1 安装软件

既然需要查看 Redis 的网络通信,那么自然是需要一个 Redis 了。这里我是下载的 github 上面的一个 windows 便携版。这个版本使用起来很方便,只是测试的话,不需要去启动虚拟机,然后再启动 docker 容器了。然后还需要一个抓包软件,这里推荐使用 WireShark,在官网下载安装即可。

在这里插入图片描述

在这里插入图片描述

2.2 抓包准备工作

先启动 redis 服务端,再启动一个客户端并连接,然后启动 wireshark 并设置好过滤条件即可。

启动 Redis 服务器:

在这里插入图片描述

启动 Redis 客户端,并插入三条数据:

在这里插入图片描述

启动 WireShark,选择 loopback 适配器(因为这里用的是 127.0.0.1 loopback 地址):

在这里插入图片描述

然后进入抓包页面,这里会实时显示已经获取的包,但是它会显示所有经过 loopback 的包(各种协议和端口),显然我们是不需要那么多的,太多了也会形成干扰,所以要过滤一下:tcp.port == 6379

在这里插入图片描述

进行端口过滤之后,这个页面就没有数据了。因为这时还没有网络通信,所以这时没有捕获的网络数据包:

在这里插入图片描述

2.3 开始抓包

2.3.1 执行事务的网络数据包

这里通过开启一个事务来获取数据,在事务中执行三次 get 指令,最后执行整个事务,这里的演示比较简单(TX 即是 Transaction 的简写)。

在这里插入图片描述

这里我们不关注这个结果,我们关注的是它执行的过程,直接看网络抓包的结果,下图即是上面整个事务从开始到执行成功的网络包。从协议列(protocol),可以看出来 RESP 是基于 TCP 的,所以每一个 RESP 之前都需要 TCP 的三次握手建立连接。

注:这里不得不说一句:WireShark 真是一个伟大的软件!它是直接识别了 RESP 了。所以,多了解一些东西,原来透明i的概念就会显现出来了。

在这里插入图片描述

我们这里只需要重点关注应用层的 RESP 即可,所以传输层的 TCP 报文也过滤掉:tcp.port == 6379 and resp

在这里插入图片描述

这样再看这个协议的交互就清晰多了,它还是很常见的 Request-Response 模式。我们来看一个 Request 和一个 Response。

在这里插入图片描述
在这里插入图片描述

把数据给复制出来(选择以可打印的格式):

在这里插入图片描述

下面两个分别是请求报文和响应报文(不是同一个请求和响应报文),因为换行符 \r\n 不可见,所以我给它手动补上了(你复制下来不是转移字符)。

*2\r\n$3\r\nget\r\n$10\r\nauthor:002\r\n
*3\r\n$11\r\nCrazyDragon\r\n$3\r\nTom\r\n$5\r\nPeter\r\n

前面我们已经简单了解了 RESP,这里大家看到这个报文应该就能知道它的意思了。

2.3.2 执行管道的网络数据包

老实说,只是使用管道和事务的话,是很难了解它们的区别。

在这里插入图片描述

直接看抓包的信息,这里可以看出,通过管道执行,它就是将命令按照 RESP 的格式给拼接起来,然后直接发送出去了,响应也是每一条命令的执行结果以及最终返回的数据。

在这里插入图片描述

不过这里有点不对劲,因为管道命令里面也是事务。我去看了一下,是因为 Python 的 redis 库默认的管道命令是原子性的。不过,这里并不需要,我们给它关掉,重新抓一个包吧。

在这里插入图片描述

在这里插入图片描述
这样就是最传统的非原子性的管道了,下面是它的报文(这里是直接复制的报文,我就不把 \r\n 打出来了,不过你应该知道的),可以看出来它们就是简单的命令拼接。

*2
$3
GET
$10
author:001
*2
$3
GET
$10
author:002
*2
$3
GET
$10
author:003

3. 总结

通过网络抓包分析,我们可以清晰的看出来。事务的每一条指令都会进行一次网络请求,所以在 QUEUED 阶段失败了,整个事务就失败了,因为服务端是可以感知的,成功了它才会发送 QUEUED 指令。而管道呢,则是把若干条指令拼接起来一次性发送,它最大的作用是节省了多次建立连接所需要的时间(不要小看了每次建立断开连接是开销,累计起来是很庞大的!)。

4. 额外的内容

前面我们简单了解了 RESP,以及在此基础上去观察事务和管道命令在执行上面的区别。那么我们还能用它来做什么呢?来整一个活!

代码示例:

package main

import (
	"fmt"
	"net"
	"strings"
)

func main() {
	DragonRedisClient()
}

func DragonRedisClient() {

	// 连接到 TCP 的 6379 端口
	conn, err := net.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer conn.Close()

	// 执行命令:set love "I love you yesterday and today."
	var builder strings.Builder
	builder.WriteString("*3") // 3 个字符串
	builder.WriteString("\r\n")
	builder.WriteString("$3") // set 字符串长度
	builder.WriteString("\r\n")
	builder.WriteString("set") // set
	builder.WriteString("\r\n")
	builder.WriteString("$4") // love 字符串长度
	builder.WriteString("\r\n")
	builder.WriteString("love") // love 键名
	builder.WriteString("\r\n")
	builder.WriteString("$31") // love 值内容长度
	builder.WriteString("\r\n")
	builder.WriteString(`I love you yesterday and today.`) // love 值内容
	builder.WriteString("\r\n")

	// 发送请求报文
	_, err = conn.Write([]byte(builder.String()))
	if err != nil {
		fmt.Println(err)
		return
	}

	// 读取响应报文
	resp := make([]byte, 20)
	n, err := conn.Read(resp)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(resp[:n]))
}

执行结果:

在这里插入图片描述

网络数据包(前面搞错了长度,导致请求一直解析失败,哈哈,最下面才是成功的。):

在这里插入图片描述

在终端查看命令,第一个 get love 是在未执行程序前,第二个是在执行程序后:

在这里插入图片描述

所以,你说这算是个什么东西?当然了,这里是非常简陋的一个代码,哈哈。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值