——总结自《redis深度历险》
**redis是一个单线程程序。**对于处理并发客户端连接,redis使用了非阻塞IO,事件轮询,多路复用。
线程IO模型
非阻塞IO:非阻塞IO在套接字对象上提供了一个选项Non_Blocking,当这个选项打开时,读写方法不会阻塞,而是能读多少读多少,能写多少写多少。
事件轮询:用于解决在非阻塞IO中线程如何得到通知。最简单的事件轮询API是select函数,输入是读写描述符列表read_fds&write_fds,输出是对应的可读可写事件。同时还提供了一个timeout参数,如果没有任何事件到来,最多等待timeout的值的时间,线程处于阻塞状态。现代操作系统的多路复用API不再使用select,而是该用性能更好的epoll(Linux)和kqueue(FreeBSD和macosx)
。服务器套接字serversocket对象的读操作是指调用accept接口客户端连接的,何时有新连接到来,也是通过select系统调用的读事件来得到通知的。
指令队列:redis会将每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行顺序处理。
响应队列:redis为每个客户端套接字关联一个响应队列。redis服务器通过响应队列来将指令的返回结果回复给客户端。如果队列为空,则将当前的客户端描述符重write_fds里面移出来,等待队列里面有数据了再将描述符放进去。避免select系统调用立即返回写事件,结果发现没有数据可写,导致CPU消耗飙升。
定时任务:redis的定时任务会被记录在一个最小堆的数据结构汇总。在每个循环周期里,redis都会对最小堆里面已经到时间点的任务进行处理。处理完毕后,将最快要执行的任务还需要的事件记录下来,这个时间就是select系统要调用的timeout参数。
RESP:是redis序列化协议的简写,是一种直观的文本写,优势在于实现过程异常简单,解析性能很好。redis协议将传输的数据结构分为5种最小单元类型,单元结束时统一加上回车符\r\n。
- 单行字符串以“+”开头。
- 多行字符串以“$”开头,后跟字符串长度。
- 整数值以“:”开头,后跟整数的字符串形式。
- 错误消息以“-”开头。
- 数组以“*”开头,后跟数组长度。
客户端给服务端发送的指令只有一种格式,多行字符串数组。服务器向客户端恢复的响应支持多种数据结构。
持久化
redis的持久化机制有两种,第一种是快照,第二种是AOF日志。快照是一次全量备份,是内存数据的二进制序列化形式,在结构上非常紧凑。AOF是连续的增量备份,是内存数据修改的指令记录文本。
redis使用操作系统的多进程COW(copy on write)机制来进行快照持久化。redis在持久化会调动glibc函数fork产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端的请求。子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。数据段是由很多操作系统的页面组成的,当父进程对其中一个页面的数据进行修改时,会将被共享页面复制一份分离出来,然后对这个复制页面进行修改。随每个页面的大小为4KB。
AOF只记录对内存进行修改的指令记录。redis会在收到客户端修改指令后,进行参数校验、逻辑处理。如果没有问题,就立即将该指令文本存储到AOF日志中。即先指令指令才将日志存盘,这点与leveldb、hbase等先存储日志再做逻辑处理的存储引擎不同。
AOF重写:redis提供了bgrewriteaof指令用户对AOF日志进行重写。其原理是开辟一个子进程对内存进行遍历,转换成一系列redis的操作指令,序列化到一个新的AOF日志中。序列化完成后将操作期间发生的增量AOF日志追加到这个新的日志中欧冠,然后代替新的代替旧的AOF。
Linux的glibc提供了fsync(int fd)函数将指定文件的内容强制从内存刷到磁盘,但是这个操作很慢。redis通常是每隔1s左右执行一次fsync操作,这个操作可以避免AOF日志内容还未完全写到磁盘就丢失。fsync是可以配置的。
redis混合持久化:redis4中可使用混合持久化。即将rdb文件的内容和增量的AOF日志文件放在一起,这里的 AOF文件不再是全量的日志文件,而是自持久化开始到持久化结束这段时间发生的增量AOF日志。在redis重启的时候,可以先加载rdb的内容,再重放AOF日志。这样重启效率得到大幅度提升。
管道
redis的管道是客户端实现的,其流程为:
- 客户端进程调用write将消息写到操作系统内核为套接字分配的