本文已参与「新人创作礼」活动,一起开启掘金创作之路。
1 单线程
Redis4.0之前是单线程的,Redis4.0修改了原来的单线程模型,变成了多线程。但是这个多线程模型仅仅是新增了几个处理后台任务的异步线程。提供服务执行指令的依然是单线程,被称为主线程。所以还称得上是单线程服务。因此依然可以当做单线程去看。 Node.js和Nginx都是单线程的。 Redis虽然是单线程,但是Redis的运算都是内存级别的,且单线程避免了线程切换的消耗,因此能够保持较快的速度。 又是因为单线程,所以对于一些耗时的命令,特别是一些时间复杂度是O(n)的命令,需要谨慎使用,否则可能会因为这个命令时间时间较长,阻塞后续的命令,导致卡顿甚至后续命令超时异常。
1.1 多路复用IO
Redis是单线程,那么为什么能处理多个客户端的并发连接呢?这是因为Redis的IO模型为多路复用IO。多路复用IO用一个线程不断的去轮询socket,只有socket有实际的读写事件时,才真正调用实际的IO操作。而且多路复用IO与非阻塞IO不同的是多路复用IO轮询socket是在内核中进行的,而不是用户线程进行轮询的。因此多路复用IO较于非阻塞IO有更低的资源占用率,更高的效率。但是因为单线程的轮询,如果一个事件响应时间过长,会导致后续的事件迟迟得不到响应,影响后续的轮询。JAVA中的NIO就是采用的多路复用IO模型。另外提一下,现在的操作系统多路复用的API用epoll代替了传统的select,带来了更大性能提升。
1.2 指令队列与响应队列
Redis为每个客户端都关联了一个指令队列,客户端的指令通过队列来排队进行处理,先到先服务。 对于响应,Redis同样为每个客户端都关联了一个响应队列,通过响应队列将响应回复到客户端。如果响应队列为空,说明连接处于空闲状态,就是Redis现在不需要回复数据,此时会把这个客户端的描述符从write_fds拿出来,这样多路复用IO轮询时间就不会轮询这个客户端了,等到响应队列有数据的时间,就把描述符放回去,重新轮询。这样避免Redis数据还没准备好时间轮询到写事件,结果读不到数据,拉高CPU。 指令队列与响应队列这一部分的内容没有在百度上搜到相关内容进行交叉验证,所以不保证理解正确,如果读者了解这个,欢迎评论区教我,感谢。
1.3 定时任务
除了响应指令,Redis还有其他的事情要做,比如定时任务。而Redis又是单线程,而在没有指令时多路复用IO的轮询会阻塞线程节省资源,指令到的时间才会被唤醒,但是定时任务到点时没有指令过来岂不是定时任务就无法准时执行了?Redis的解决方案是这样的:首先多路复用IO的轮询系统有一个参数timeout,就是空闲状态下轮询线程阻塞超过这个时间,就会终止阻塞自动唤醒。然后定时任务记录在一个称为最小堆的数据结构中,这个堆中,离执行时间越短,越在上面。Redis每次轮询时都会将堆中到点的定时任务执行掉,如果没有事件,线程要阻塞时就会计算下一个定时任务还有多久要执行,然后设置这个时间为timeout,这样就保证了到点时即使没有事件唤醒线程处理,也会被timeout唤醒。 Nginx和Node的事件处理原理和Redis是类似的。 Redis4.0新增了后台线程,可以配置相应处理的异步处理开关,如果打开,那此时主线程的任务仅仅是生成异步任务放入异步队列,而不参与处理,避免影响服务。
2 通信协议
Redis的开发者认为数据库系统的瓶颈不在网络流量,而在自身的处理逻辑上。所以Redis即使使用了浪费流量的文本协议,依然有很高的性能。Redis单线程+纯内存在跑满一个CPU的情况下QPS可达10W/S。
2.1 RESP(Redis Serialization Protocol)
RESP是Redis序列化协议的缩写,它是一种直观的文本协议,优势在于实现异常简单,解析性能极好。 Redis协议将传输的结构数据分为5种最小单元类型,单元结束时统一加上回车换行符\r\n。
- 单行字符串以 + 符号开头
- 多行字符串以 $ 符号开头,后跟字符串长度
- 整数值以 : 符号开头,后跟整数的字符串形式
- 错误消息以 - 符号开头
- 数组以 * 符号开头,后跟数组的长度
示例: 单行字符串hello world
+ hello world\r\n
多行字符串hello world,单行字符串也可以用多行形式表示
$11\r\nhello world\r\n
整数1024
:1024\r\n
错误:参数类型错误
-WRONGTYPE Operation against a key holding the wrong kind of value
数组[1,2,3]
*3\r\n:1\r\n:2\r\n:3\r\n
NULL 用多行字符串表示,不过长度要写成-1
$-1\r\n
空串用多行字符串表示,长度为0:注意是两个\r\n,不是一个
$0\r\n\r\n
2.2 客户端到服务端
客户端到服务端都是指令,指令只有一种形式,就是多行字符串数组,比如一个指令 set key val 序列化之后就是:
*3\r\n$3\r\nset\r\n$3\r\nkey\r\n$3\r\nval\r\n
看着很复杂,但是放到控制台展示就很清晰了:
*3
$3
set
$3
key
$3
val
后面黑背景的都默认是控制台的显示模式了。
2.3 服务端到客户端
服务端到客户端就比较复杂了,不过再复杂也是上面5种基本类型的组合。
ip> set key val
OK
这个OK没有被引号括起来,就是单行字符串响应
+OK
错误响应:
ip> incr key
(error) ERR value is not an integer or out of range
这个错误响应时这样的:
-ERR value is not an integer or out of range
整数响应:
ip> incr key
(integer) 1
这个1就是整数响应:
:1
多行字符串响应:
ip> get key
"val"
这个val被引号括起来,就是多行字符串响应:
$8
val
数组响应:
ip> hgetall key
1) "name"
2) "yh"
3) "age"
3) "3"
hgetall返回的是一个文本,客户端会把这个文本解析成字典再返回:
*4
$4
name
$2
yh
$3
age
$1
3
嵌套响应:
ip> scan 0
1) "0"
2) 1) "yh"
2) "stu"
3) "bk"
整体是一个数组,第一个元素是多行字符串响应,第二个是嵌套的多行字符串数组响应:
*2
$1
0
*3
$2
yh
$3
stu
$2
bk
Redis的序列化协议传输过程中虽然有大量的\r\n回车换行符,但是并不影响它成为互联网技术领域非常受欢迎的一个文本协议,有很多项目都采用了RESP协议,技术领域性能并不是一切,还有简单性,易理解和易使用性,需要根据实际场景进行评估。
开发成长之旅 [持续更新中...]
欢迎关注…
参考资料
《Redis深度历险》