鞭辟入里一一线程 IO 模型
Redis 是个单线程程序 !
非阻塞 IO
当我们调用套接字的读写方法,默认它们是阻塞的,例如read方法传递进去一个参数n,最多读取n个字节返回,如果没有字节,线程就会卡在那里,直到有新的数据来或连接关闭,read方法才返回,才线才能继续处理。write方法一般来说不会阻塞,除非内核为套接字分配的写缓冲区已经满了, write 方法就会阻塞,直到缓存区中有空间空闲出来。
非阻塞 IO 在套接字对象上提供了一个选项 Non_Blocking。读的字节取决于内核为套接字分配的读缓冲区内部的数据字节数,写的字节取决于内核为套接字分配的写缓冲区的空闲空间字节数。读方法和写方法都会通过返回值来告知程序实际读写了多少字节。
事件轮询(多路复用)
事件轮询 API 就是用来解决,当数据到来时,线程如何得到通知。事件轮询 API select 函数, 它是操作系统提供给用户程序的 API 输入是读写描述符列表 read_fds & write fds, 输出是与之对应的可读可写事件同时还提供了一个 timeout 参数,如果没有任何事 件到来,那么就最多等待 timeout 的值的时间,线程处于阻塞状态 一旦期间有任何事件到来,就可以立即返回。时间过了之后还是没有任何事件到来,也会立即返回。
指令队列
每个客户端套接字都关联一个指令队列,会通过队列来排队进行顺序处理。
响应队列
Redis 服务器通过响应队列来将指令的返回结果回复给客户端。如果队列为空,那么意味着连接暂时处于空闲状态,不需要去获取写事件,也就是可以将当前的客户端描述符从 write_fds 里面移出来。等到队列有数据了,再将描述符放进去,避免 select 系统调用立即返回写事件,当没什么数据可以写时,出现这种情况的线程会加大 CPU 消耗。
定时任务
Redis 的定时任务会记录在一个被称为“最小堆”的数据结构中。在这个堆中, 最快要执行的任务排在堆的最上方。在每个循环周期里, Redis 都会对最小堆里面已经到时间点的任务进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是 select 系统调用的 timeout 参数。 Nginx 和 Node 的事件处理原理和 Redis 也是类似的。
交头接耳一一通信协议
Redis 使用了浪费流量的文本协议,依然可以取得极高的访问性能,将所有数据都放在内存中,用一个单线程对外提供服务,单个节点在跑满 一个 CPU 核心的情况下可以达到了 10 w/s 的超高 QPS。
RESP
RESP是Redis序列化协议(Redis Serialization Protocol),是一种直观的文本协议,优势在于实现过程简单,解析性能很好。
Redis 协议将传输的结构数据有5种最小单元类型:(后面加 )
- 单行字符串以“+”符号开头。 2. 多行字符串以“$”符号开头,后跟字符串长度 3. 整数值以“:”符号开头,后跟整数的字符串形式 4. 错误消息以“-”符号开头。 5. 数组以“*”号开头,后跟数组的长度。
NULL 用多行字符串表示,不过长度要写成 -1 $-1
空串用多行字符串表示,长度填0 $0
客户端→服务器
客户端向服务器发送的指令只有一种格式,多行字符串数组。
eg: *3
\n $3
set
$4
name
$3
123
服务器→客户端
服务器向客户端回复的响应要支持多种数据结构,由上5种基本类型的组合。
单行字符串响应 +OK
错误响应 试图对个字符串进行自增,服务器抛出一个通用的错误。-ERR value is not an integer or out of range
整数响应 (integer) 1 : 1
多行字符串响应 "123" 使用双引号括起来的字符串就是多行字符串晌应
$8
codehole
数组响应
嵌套
127 . 0 . 0.1 : 6379> scan 0
1 )“0”
2 ) 1 ) ” info ”
2 )”books”
3 )”author”
scan 命令用来扫描服务器包含的所有 key 列表,它是以游标的形式获取,一次只获取一部分。 scan 命令返回的是一个嵌套数组。数组的第一个值表示游标的值,如果这个值 为零,说明已经遍历完毕。如果不为零,使用这个值作为 scan 命令的参数进行下一 次遍历。数组的第二个值又是一个数组,这个数组就是 key 列表。
*2
$1
0
*3
$4
info
$5
books
$6
author