Redis 学习笔记(2. 客户端与服务端的交互)

客户端与服务端的交互

基于Redis协议,Redis客户端和协议端可以实现多种典型的交互模式:

  • 串行的请求/响应模式
  • 双工的请求响应模式(pipeline)
  • 原子化的批量请求/响应模式(事务)
  • 发布/订阅模式
  • 脚本化的批量执行(脚本模式)

客户端与服务端之间的交互

Redis的交互协议分为两部分: 网络模型序列化协议

网络模型

Redis协议位于TCP 层之上,即客户端和Redis实例保持双工的连接。

序列化协议

客户端/服务器端交互是序列化后的协议数据,在Redis中,协议数据分为不同的类型,每种类型的数据均以CRLF(\r\n)结束,通过数据的首字符区分类型。

1. inline command

此类数据表示Redis命令,首字符为Redis命名的字符,格式为str1 str2 str3...。例如Exists key1,表示Redis检查Key1是否存在这个命令。

2. simple string

首字符为+
后续字符为string的内容,且该string不能包含\r\n两个字符;
最后以\r\n结束。
例如:+OK\r\n这五个字符代表的是OK这个string数据。

3. bulk string

对于string本身内容包含了\r或者\n的情况,simple string不再适用。
Redis采用的是长度自描述来解决此问题,即bulk string
bulk string首字符为$,紧随其后的是string数据的长度,\r\n之后紧跟着string的内容本身(可以包含\r或者\n),最后再以\r\n结尾
例如:$12\r\nhello\r\nworld\r\n描述的为hello\r\nworld
$0\r\n\r\n代表空字符串
$-1\r\n代表null

4.error
此为异常信息,在Redis中其内容就是一个普通的string,和simple string的表达能力一致,格式也类似。区别在于首字符。<br>
error的首字符为`-`而simple string首字符为`+`。<br>
客户端直接通过首字符即可判断本次交互是否出错。<br>
例如:`-ERR unknown command 'footbar'\r\n`表示的就是一个erro和其error信息。
复制代码
5. integer
以`:`字符开头,紧跟着整型数字本身,最后以`\r\n`结尾。
复制代码
6. array
以`*`字符开头,紧跟着数组长度,`\r\n`之后是数组中每个元素序列化数据。<br>
例如:`*2\r\n+abc\r\n:9\r\n` 代表的就是数组`["abc",9]`。<br>
数组长度为0或者-1代表空数组或null。<br>
并且数组的元素本身也可以是数组。
复制代码
7. C/S 两端使用的协议数据类型
由客户端发送给服务端的类型为:inline command,有bulk string 组成的array<br>
由服务端发送给客户端的类型为**除了inline command**之外的所有数据,并根据客户端命令或者交互模式的不同进行确定。<br>
例如:
- 请求/响应模式下,对客户端发送的`EXISTS key1`命令,返回integet型数据。
- 发布/订阅模式下,对channel订阅者推送的消息,采用`array`型数据。
复制代码

请求/响应模式

同一个连接上,请求/响应模式如下:

  • 交互方向:客户端发送请求数据,服务端发送相应数据
  • 对应关系:每一个请求数据有且仅有一个对应的响应数据。
  • 时序:响应数据的发送发生在“服务器完全接受到其对应的请求数据之后”。

1.串行化实现

最简单的实现方式为串行化实现,即同一个连接上,客户端收完第一个请求的响应之后,再发起第二个请求。
这种实现的问题在于每次请求的发送都依赖于上一次请求的返回。

2. pipeline 实现

由于依赖的TCP协议本身是全双工的,请求/响应即便穿插进行,也不会发生请求和响应数据的混淆。
这种模式适合于批量的独立写入操作(每次写入的数据不依赖上一次请求的执行结果)
这种不等上一次结果返回就发送下一次请求的模式成为pipeline

pipeline的实现完全取决于客户端:

  • 通过批量请求发送还是异步请求发送来实现
  • 非异步化的批量发送下需要考虑到每个批次的数据量,避免连接的buffer满了之后的死锁。
  • 对使用者如何封装接口,是的pipeline使用简单。

事务模式:

一个客户端批量发送的每一条命令和另外一个客户端的命令在Redis服务器端看来是同等的,其执行顺序可能存在交叉。
会出现一个客户端的批量命令夹杂在另外一个客户端的批量命令之中国。
所以我们会引入Redis的事务模式来保证批量执行的语句原子化。

1. 入队/执行分离的事务原子性(MULTIEXEC)

客户端通过和redis服务器两阶段的交互做到批量命令原子化执行的事务效果:

  • 入队阶段:客户端将请求发送至服务器端,后者将其暂存在连接对象对应的请求队列中。
  • 执行阶段:发送完一个批次的所有请求后,Redis服务器依次执行连接对象队列中的所有请求。由于单个实例的Redis仅单线程执行所有请求,一个连接的请求在执行批量请求的过程中,是不会执行其他客户端的请求。

由于Redis执行器单线程的一次执行的粒度是“命令”,所以为了让批量的请求一次性地全部执行,引入“批量执行命令”:EXEC

MULTI命令开始事务,随后发送的请求都只暂存在服务器端的连接上,最后通过EXEC一次性批量执行(发生实际的数据修改),并将所有执行结果作为一个响应,以array类型的协议数据返回给客户端。

2. 事务的一致性(Redis本身不包括回滚机制)

当入队阶段发生语法错误时,不执行后续的EXEC,不会对数据实际产生影响。
EXEC中有一条请求执行失败时,后续请求继续执行,只在返回客户端的array型响应中标记这条出错的结果。由客户端决定如何恢复,Redis本身不包括回滚机制。

3. 事务的只读操作

批量请求在服务器端一次性执行,应用程序需要一开始就在入队阶段确定每次写操作的值,也就是说,每个请求的参数取值不能依赖上一次请求的执行结果。

只读操作在批量执行中没有任何意义,它的结果既不会改变事务的执行行为,也不会改变Redis的数据状态。

所以,入队的请求应该全是写操作。

由于在入队模式下所有的操作都只返回是否入队成功,只读操作无法在此状态下获取真正的数据结果。所以一个完整的业务事务,其只读操作需要放到MULTI语句之前执行。

4.乐观锁的可串行化事务隔离(串行化级别)

Redis通过 WATCH 机制实现乐观锁解决一致性问题。 实现过程:

  • 将本次事务涉及的所有key注册为观察模式,假设此时逻辑时间为tstart。
  • 执行只读操作
  • 根据只读操作的结果组装成写操作命令并发送到服务器端入队
  • 发送原子化的批量执行命令EXEC试图执行连接的请求队列中的命令,假设此时逻辑时间为tcommit

执行时有以下两种情况:

  • 假设前面注册为观察者模式的key中有一个或多个,在tstart和tcommit之间被修改过,那么EXEC将直接失败,拒接执行。
  • 否则顺序执行请求队列中的所有请求

无论EXEC是否执行,都会UNWATCH本连接注册的所有key

5. 事务是如何实现的?

watch 机制通过维护在redisDB 中的全局map(watched_keys)来实现。
map的key是被watch的key,值是watch这些key的redisClient指针的链表。

每当redis执行一个写命令时,它同时会对执行命令的这个key在map中找到对应的client并将这个client的flag置为REDIS_DIRTY_CAS,后续这个client在执行EXEC前如果看到flag是REDIS_DIRTY_CAS标记,则拒绝执行。

事务的结束或者显式UNWATCH都会重置redisClient中的REDIS_DIRTY_CAS标记,并从redisDB的watched_keys中的链表删除自己。

6. 事务交互模式

一个连接事务,如下所示:

  • 客户端发送四类请求:
    • 监听相关(WATCH,UNWATCH)
    • 只读操作
    • 写请求的批量执行或放弃执行请求(EXEC/DISCARD)
    • 写请求的入队(MULTI 和 EXEC/DISCARD之间)
  • 交互时序为:
    • 开启对于读写主键的监听
    • 只读操作
    • MULTI请求
    • 根据前面的只读操作的结果编排/参数赋值/入队写操作
    • 一次性批量执行队列中的写请求。

7. 脚本模式(逻辑嵌入)

Redis实现可串行化的隔离级别,使用者需要做到三点约束:

  • 事务的只读操作须先于写操作(在批量执行中进行读操作没有意义)
  • 所有写操作的执行不依赖其他写操作的执行结束
  • 使用乐观锁避免一致性问题,对相同key并发访问频繁时事务成功率低】

但是,如果如果能将事务编排逻辑直接置于服务器端执行,由于redis的执行器都是单线程的,原生地以串行方式实现了事务的原子/串行化隔离,保证了一致性,成为逻辑嵌入。

1. 脚本的交互模式
  • 客户端发送eval lua_srcipt_string 2 key1 key2 first second给服务器
  • 服务器端解析lua_script_string 并根据string本身的内容通过sha1计算出sha值,存放到redisServer对象的lua_srcipts成员变量中,它为map类型,sha作为键
  • 服务器端原子化地通过内置lua环境执行lua_script_string,后者可能包含对Rdis的方法调用比如set key命令。
  • 执行完后将lua结果转换成Redis类型返回给客户端。
2. script 特性

?????

发布/订阅模式

Redis还有一种交互模式是一个客户端触发,多个客户端被动接受,通过服务器中的中转,称为发布/订阅模式。

客户端分为发布者和订阅者两类角色。
发布端和订阅者通过channel关联。

两类 channel
  • 普通channel
    • 订阅者通过SUBSCRIBE/UNSUBSCRIBE 将自己绑定/解绑到某个channel上
    • 发布者的publish命令指定某个消息发送到哪个channel
    • 再由服务器将消息转发给channel上绑定的订阅者。
  • pattern channel
    • 订阅者通过SUBSCRIBE/UNSUBSCRIBE将自己绑定/解绑到某个pattern channel上
    • 发布者的publish命令指定每个消息发送到哪个channel
    • 再由服务器通过channel的名字和pattern channel的名字做匹配,匹配成功则将消息转发给这个pattern channel上绑定成功的订阅者
实现

pubsub_channels map维护普通channel和订阅者的关系
pubsub_patterns维护pattern channel和订阅者的关系

转载于:https://juejin.im/post/5c9f3de46fb9a05e4868b739

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值