Redis服务器和客户端的通信

时间:2023-03-10 06:57:11
Redis服务器和客户端的通信

Redis客户端使用RESP(Redis序列化协议)与Redis服务器进行通信,RESP在位于TCP之上,而网络模型上客户端和服务器是保持的双工的连接。如图1

Redis服务器和客户端的通信

而一个简单的请求/响应的串行通信模型如下图:

Redis服务器和客户端的通信

串行化通信

串行化通信比较简单,上面那张图就很表面的反应出来这种通信方式,同一个Connction需要在等上一个命令执行完成之后在执行下一个命令,我们在前面文章讲Redis各种类型的时候做的测试,就是用这种方式。客户端发送一个指令到Redis实例,Redis实例处理完成之后将结果返回给客户端。

前面文章说Redis为什么要用多线程中有说过,Redis处理请求的速度特别快,我们一个请求的瓶颈主要是在I/O上面,而对于串行化通信,每一个请求的发送都要等到上一个请求的响应介绍,因此在串行模式下,单连接的大部分时间都浪费在网络等待上,没有充分的利用服务器的处理能力

管道技术

Redis在很早的时候就支持管道技术了,简单来说,就是可以完全无需等待服务端应答地发送多条指令给服务端,并最终一次性读取所有应答。管道技术最显著的优势是提高了redis服务的性能,通过管道技术来进行大批量的操作的时候,可以节省很多在网络延迟上的时间。

在.net core 的Redis客户端StackExchange.Redis则是基于Task来实现管道技术,而StackExchangeRedis本身的异步也都是通过管道技术来实现。

事务

在菜鸟教程中是这么介绍的

Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:

  • 批量操作在发送 EXEC 命令前被放入队列缓存。
  • 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

一个事务从开始到执行会经历以下三个阶段:

  • 开始事务。
  • 命令入队。
  • 执行事务
  • 放弃事务

原理很简单,客户端发送命令MULTI,服务器会将后续的命令都放入队列缓存,直到收到EXEC命令才会依次执行命令。单个Redis的命令是原子性的,但是Redis并没有在事务上增加任何的维持原子性的机制,当中间某条命令失败并不会导致其他命令的回滚,这个跟我们在关系型数据库的理解不一样,更多的像一个打包的批处理脚本。

菜鸟中有这么一句话

在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

粗略一看我还理解为事务开启会阻塞其他客户端的命令,吓得我马上做了一下测试

在客户端1中开启事务multi,并发送一个set 和 get 的命令,能看到都是QUEUED的状态,表明是正确的入队了

Redis服务器和客户端的通信

接着在客户端2中获取key1发现值是null,说明客户端1的命令还没有真正执行,接着设置key1的值为value2,接着取得key1的值,在客户端1中开启事务后,在客户端2是可以顺利执行命令的,菜鸟中的话的意思其实客户端的命令不会进入开启事务那个客户端的命令队列中。

Redis服务器和客户端的通信

我们接着在客户端1提交命令,key1的值变为value1,客户端2中设置的value2被更改为value1了。

我们将Redis事务与数据库事务的四大特征对比下

原子性 不支持 Redis单个指令是具有原子性的,但是事务没有
一致性 不支持 在上面的例子就可以看见,在客户端1的事务开启的时候,我仍然能修改key1的值,在关系型数据库中我们有悲观锁和乐观锁来解决这种并发问题,Redis也通过Watch可以实现乐观锁的效果,但是我还是没有体会出来有什么用处。在关系型数据中的事务,我们可能会先取出来值,在进行修改,最后提交事务,如果没有锁来保证,那么我们最后的数据就没有一致性了,但是对于Redis我还是没想出来什么场景下会需要用乐观锁来控制并发,知道的小伙伴麻烦告知一声。
隔离性 支持 Redis本身是没有隔离性这个说法的,之所以我觉得是支持隔离性,因为我觉得Redis的事务都是在最后才执行,而本身命令又是原子性的,所以隔离性对Redis是无意义的。
持久性 不支持 Redis有持久化方案,但是最高数据安全性的方式-AOF中的修改同步,仍然会在异常情况下导致数据丢失。

其实这个对比不太恰当,Redis的事务只是顶着事务这个名字,做的还是批量处理的事情,它的关注点不应该在正真的事务上

脚本

在说事务的时候有说事务更像是批处理的感觉,而脚本也是批处理,不同的是,我们可以根据上一个指令的结果作为我们下个指令的参数,这是处理逻辑问题的时候特别有用。

Redis脚本是通过Eval命令实现,当客户都安使用Eval命令的时候,Redis实例会通过lua解释器来执行脚本,我们这里的脚本也是lua脚本,用Abp中清除缓存的的源码作为示例

EVAL "local keys = redis.call('keys', ARGV[1])
for i=1,#keys,5000
do
redis.call('del', unpack(keys, i, math.min(i+4999, #keys)))
end"
0 'Test_*'

这个脚本第一步将以Test做为前缀的key全部取出来存入变量keys,接着从1开始,以keys的长度为最大值,步长为5000进行遍历,每一步都是删除5000个key。为什么要用每次5000遍历来执行呢?因为unpack函数在数量太多的时候会出现 'too many results to unpack' 的错误,我们来实际操作下,往实例中添加10个用Test_为前缀的值,然后执行上面的脚本

Redis服务器和客户端的通信

Redis服务器和客户端的通信

可以看到我们以Test_做为前缀的Key都被删除了

发布/订阅模式

前面有讲到过,Redis实例和客户都之间是双工连接的,但是前面所说的不管是简单的命令还是事务脚本都是客户端主动发起请求,Redis实例被动回应的,而发布/订阅模式则是可以由Redis实例主动给客户端发送消息,在下一节会详细说这种模式。