前言
Redis实例运行于单独的进程之中,应用系统和Redis通过Redis协议进行交互。在协议允许的范围内,客户端与服务器可以实现多种典型的交互模式:串行的请求/响应模式、双工的请求/响应模式(Pipeline)、原子化的批量请求/响应模式(事务)、发布/订阅模式、脚本化的批量执行(脚本模式)
双工模式,包括半双工、全双工
半双工:在同一时刻,A 、B通讯双方只能单方向传输信息,A -> B 或者 B -> A
全双工:在任意时刻,A、B通讯双方都能互传信息,支持任意时刻的A -> B ,B -> A
客户端/服务器协议
Redis的交互协议包括:
- 网络模型 数据交互的组织方式
- 序列化协议 数据本身序列化
1. 网络交互
Redis协议位于TCP层之上,即客户端和Redis实例保持双工的连接。客户端与服务端交互的内容,是序列化后的相应类型的协议数据,服务器端为每个客户端建立对应的连接,在应用层维护一系列的状态保存到连接中。连接之间没有相互关联性。在Redis中,连接通过redisClient结构体实现。
2. 序列化协议
在Redis中,协议数据分为不同的类型,每种类型的数据均以CRLF(\r\n)结束,通过数据首字符区分类型。
- inline command
这类数据表示Redis命令,首字符为Redis命令名的字符,格式为command str
如,keys * ,首字符为k,表示Redis查找所有的匹配的key
- simple string
首字符为+,后续字符为string的内容,且该string不能包含\r \n 两个字符;最后仍以\r\n结束
如,+OK\r\n 表示新增OK这个string数据
- 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
对于“”字符串和null通过$之后数字长度区分
- “$0\r\n\r\n”,代表空字符串
- "$-1\r\n"代表null
- error
对于服务端返回内容,客户端需要识别是正确内容还是异常响应。异常信息即error类型数据,在Redis中首字符“-”表示
如“-ERR unknown command'foobar'\r\n“ 表示一个错误,和一个错误信息
- integer
首字符“:”开头,紧跟着整型数字本身,最后\r\n结尾
- array
数组以首字符“*”开头,紧跟着数组的长度,\r\n之后为数组中每个元素的序列化数据。
如,*2\r\n+abc\r\n:9\r\n 代表长度为2的数组,["abc",9]
数组长度为0或-1代表空数组或者null
- C/S两端使用的协议数据类型
客户端发送给服务器端的类型为:inline command 有bulk string组成的array
由服务器端发送给客户端的类型为除了inline command 之外的所有类型,根据客户端命令或者交互模型不同进行确定,如:
- 请求/响应模式下,对客户端发送的EXISTS KEY1 返回integer型数据
- 发布/订阅模式下,为channel 订阅者推送的消息,采用array型数据
请求/响应模式
同一连接,请求/响应模式如下:
交互方向:客户端发送请求数据,服务器发送响应数据
对应关系:每一个请求数据其且仅有一个对应的响应数据
时序:响应数据的发送发生在“服务器完全接收到其对应的请求数据”之后
1.串行化实现
Redis对单个请求的处理时间通常比局域网延迟小一个数量级,因此串行化模式下,单连接的大部分时间都处于网络等待,没有充分利用服务器的处理能力。
2.pipeline实现
pipeline的实现取决于客户端,需要考虑:
- 通过批量请求发送还是异步化请求发送实现
- 非异步化的批量发送需要考虑每个批次的数据量,避免连接的buffer满后死锁
- 对使用者如何封装接口,使得使用pipeline简单
事务模式
当需要将批量执行的语句原子化,需要引入redis的事务模式
一次事务中多条命令以原子化方式执行,不同事务的命令相互时序不再交叉
1.入队/执行分离的事务原子性
入队阶段:客户端将请求发送到服务端之后,暂存在连接对象对应的请求队列中
执行阶段:发送完一个批次的所有请求后,redis服务器一次执行连接对象队列中的所有请求
执行过程,redis不再执行其余连接请求。
2.事务一致性
当入队阶段出现错误,不执行后续的执行exec,不会对数据实际产生影响
当执行阶段已经开始,其中一条请求执行失败,后续请求会继续执行,返回客户端的array型响应中标记出错的结果,redis本身不包含回滚机制
3.事务的只读操作
入队模式下,所有的操作都没有真正执行,仅返回是否入队成功。此时,以A账号向B账号转账说明:
100 <= get a 100 <= get b ok <= multi queued <= set b 110 queued <= set a 90
如果客户端2,中间对a或者b有所改变,那么最终一致性就会被破坏
4.乐观所得可串行化事务隔离
正如上述所说,不同事务的命令相互时序不再交叉,Redis通过watch机制实现乐观锁,解决事务一致性问题
- 将本次事务涉及的所有key注册为观察模式,假设此时逻辑时间为tstart
- 执行只读操作
- 根据只读操作结果组装写操作命令并发送到服务器端入队
- 发送原子化的批量执行命令,试图执行连接的请求队列中的命令,此时逻辑时间为tcommit
存在两种情况:
假设注册的key有一个或多个,在tstart和tcommit之间被修改,则原子化批量执行命令,拒绝执行;
否则执行所有队列中请求
5.事务实现
事务实现通过redisObject对象进行相关设置
6.事务交互模式
通过watch机制,保证了不存在数据不一致问题,执行器是单线程,保证所有的写操作都是通过串行方式实现串行化
脚本模式
事务模式,会有一定的局限,redis允许客户端向服务器提交一个脚本,来编排业务事务多个redis操作,
1.脚本交互模式
- 客户端发送eval lua_script_string 2 key1 key2 first second 给服务端
- 服务端解析lua_script_string 根据string内容通过sha1计算出sha值,存放到redisServer对象的lua_script 成员变量中
- 服务器端原子化通过内置lua执行脚本
- 执行完成后,将lua结果转为redis类型返回给客户端
2.script特性
脚本包含以下特性:
- 每一个提交的脚本,常驻不会消失,除非显式通过flush命令清理
- script在实例的主备间可以通过script重放和cmd重放实现复制
- 执行过的script之后,可以通过sha指定,不用重复发内容到服务端
发布/订阅模式
redis提供一种模式,一个客户端触发,多个客户端被动接受,称为发布/订阅模式
1.发布/订阅交互模式
该模式如下所述:
1)角色关系
- 客户端分为发布者和订阅者两种角色
- 发布端和订阅者通过channel关联
2)交互方向
- 发布者和redis服务器的交互认为请求/响应模式
- 服务端向订阅者发送数据,推送
- 时序:服务器收到发布者消息之后
2.两类channel
普通channel:明确定义的channel
pattern channel:模糊匹配的channle
3.订阅实现
typedef struct redisServer { ... dict *pubsub_channels; list *pubsub_patterns; ... }
通过维护指针链表来实现。