Redis服务器
redis服务器本质是一个事件驱动程序,处理文件事件和时间事件两种事件。从事件处理的角度看,redis服务器的运行流程可以理解为在一个while循环中,等到文件事件产生后先处理已产生的文件事件,然后处理已达到的时间事件,事件的处理都是同步、有序且原子的执行。由于时间事件的处理依赖于文件事件的产生,所以时间事件通常会比设定的到达时间晚一些执行。而且为了尽可能的减少服务器阻塞的时间,降低事件饥饿的可能性,如果文件事件的写操作要写入的字节数大于一个预设值,会先跳出循环将余下的数据留到下次再写;如果一个时间事件非常耗时,会放到子线程或者子进程中执行。
伪代码表示
while(true) {
if (has file event) {
handle file event;
handle time events;
}
}
redis被看作是单线程的原因就在于这段伪代码的核心逻辑,但是在一些地方仍是多线程。
3.0以前,主进程会fork子进程来执行RDB持久化和AOF重写,同时有一个两核线程池来执行异步关闭文件和异步刷aof_buf的内容到文件中。
4.0变成了三核线程池,增加了异步删除过期键的处理。
6.0在网络模型中引入了多线程。客户端发送命令不会立马读取解析,而是先加到一个队列中,由主线程去分配IO线程读取客户端命令。同时执行完的结果也不会立马写回响应,也是先加到队列中,再去计算开销来判断是启动子线程来异步写回还是用休眠的IO线程写回。
所以只要这段核心逻辑不变,redis引入的并行或并发操作都不需要担心并发安全问题。
文件事件
基于Reactor模式开发了自己的文件事件处理器。虽然文件事件处理器是单线程的,但是引入了IO多路复用程序来监听多个文件描述符,既实现了高性能的网络通信模型,也很好的对接了redis中其他单线程运行的模块。
文件事件处理的构成
· 套接字:socket
· IO多路复用程序:单线程。通过包装了select、epoll、evport、kqueue这些IO多路复用函数库实现的,程序会在编译时自动选择系统中性能最高的函数库,基本上就是取决于OS的类型。将所有产生事件的socket放在一个队列里,然后有序、逐个的向文件事件派发器传送套接字,待文件事件派发器处理完一个后才会发下一个。
· 文件事件分派器:单线程。接收IO多路复用程序传来的socket,并根据socket产生的事件类型,将事件分发给相应的事件处理器。
· 事件处理器:封装的函数,对应了服务器的行为。根据socket事件类型,分为连接应答处理器、命令请求处理器、命令回复处理器。
socket 连接应答处理器
socket ===> IO多路复用模型 ====> 文件事件派发器 ===select==> 命令请求处理器
socket 命令回复处理器
一次完整的客户端与服务器连接事件示例:
1.一个客户端向服务器发起连接,监听套接字产生AE_READABLE事件,触发连接应答处理器,处理完后创建客户端套接字,以及客户端状态,并将客户端套接字的AE_READABLE事件与命令请求处理器进行关联,使得客户端可以向主服务器发送命令请求。
2.之后,如果客户端向主服务器发送一个命令请求,那么客户端套接字将产生AE_READABLE事件,引发命令请求处理器读取命令内容并传给相关程序去执行。
3.执行命令后,为了将产生的命令回复传给客户端,服务器会将客户端套接字的AE_WRITEABLE事件与命令回复处理器关联,当客户端尝试读取命令回复的时候,客户端套接字将产生AE_WRITEABLE事件触发命令回复处理器。当命令回复处理器将命令回复全部写入到套接字后在解除关联。
时间事件
时间事件分为定时事件和周期性事件。
实现
服务器将所有的时间事件都放在一个无序链表中,这里说的无序,指的是在链表中的时间事件并不是按执行时间点来排序的。每当时间事件执行器运行时,它就遍历这个链表,找到所有可执行的时间事件,并调用相应的事件处理器。由于正常模式下服务器只使用serverCron一个时间事件,而且在benchmark模式下只使用两个时间事件,所以即使是一个无序链表,也不会影响执行的性能。
ServerCron函数
· 更新服务器的各类统计信息
· 清理数据库中的过期键值对
· 关闭和清理连接失效的客户端
· 尝试进行AOF和RDB持久化操作
· 如果服务器是主服务器,定期对从服务器同步
· 如果处于集群模式,对集群进行定期同步和连接测试
Redis持久化
因为redis是内存数据库,所以一旦服务器进程退出,在内存中的数据就全部都会丢失。所以为了解决这个问题,Redis提供了RDB和AOF的持久化功能,用于将内存中的数据库状态保存到磁盘里。
1.RDB
RDB持久化可以手动执行,也可以根据服务器配置周期性执行。该功能将某个时间点上的数据库状态保存到一个rdb文件里,当redis重启时,只要rdb文件还在,就可以用它来还原数据库状态,以此来保证数据的持久化。
rdb文件的创建
有两个命令用于生成rdb文件:SAVE和BGSAVE。SAVE命令会阻塞服务器进程,直到rdb文件创建完毕;BGSAVE命令会从主进程中fork出一个子进程,然后由子进程创建rdb文件,不影响服务器继续处理命令请求。
rdb文件的载入
rdb文件的载入是在服务器启动时自动执行的,所以没有专门的命令,只要服务器启动时检测到rdb文件存在,就会自动载入,载入期间服务器进程会处于阻塞状态。但是如果开启了AOF,那会优先使用AOF文件来还原。
自动间隔性保存
执行 save m n命令,如果服务器在m秒内进行了n次以上修改,就会触发BGSAVE命令。
过期键的处理
生成RDB文件时,过期键会被忽略,不保存到RDB文件中。
载入RDB文件时,对于一个已过期的键,如果服务器是以主服务器模式运行,则会忽略;以服务器模式运行,则会一并载入,但因为主从服务器同步时会清空从服务器的数据库,所以一般来讲也不会造成影响。
优点
· 保存的是某个时间点的数据集,且RDB文件是压缩的二进制文件,占用空间较小。
· 最大化redis的性能,主进程只需要fork出一个子进程。
· 数据量较大时,RDB文件的恢复速度快于AOF。
缺点
· 服务器故障时容易造成数据的丢失。因为RDB文件的生成是个重操作,所以频率注定不会很高。
· fork子进程采用的是copy-on-write方式。刚fork出子进程时,子进程和主进程共享内存,但随着主进程的不断写入,主进程需要将修改的页面拷贝一份出来再修改。所有页面都修改了的极端情况下,内存占用达到了2倍。
· RDB文件保存也是依赖fork的子进程执行的。如果数据量比较大,子进程可能会非常耗时,占用较多的内存和CPU,造成redis服务一秒内的暂停。
2.AOF
AOF持久化是通过记录Redis服务器的写命令来记录数据库状态的,可以类比MySQL的redo log来理解,不同的是redo log记录的只是单纯的某个物理位置的物理偏移量,而AOF记录的是整个命令语句。
AOF持久化的实现
AOF持久化的实现可以分为命令追加、文件写入、文件同步三个步骤。
· 命令追加:当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议的格式将命令追加到服务器的aof_buf缓冲区内。
· 文件写入与同步:写入和同步可以分别理解为write和flush,write是写入操作系统的缓冲区中,flush是将操作系统的缓冲区中的内容刷到磁盘上。因为redis在处理文件事件时很可能发生写命令,所以每次事件循环结束前都会根据配置决定是否需要将aof_buf中的内容写入和保存到AOF文件中。配置appendfsync的值来调整行为:always(总是会对aof_buf缓冲区执行write+flush)、everysec(总是会对aof_buf缓冲区执行write,但只有距离上次flush超过一秒才会再次flush到AOF文件中,而且flush操作是由一个子线程来执行的)、no(只执行write,什么时候执行flush交给操作系统决定)。默认是everysec,具体用哪个,取决于对业务的判断,以做效率和安全性的取舍,这一点和redo log是一样的。
AOF的载入和数据还原
因为AOF文件中包含了重建数据库状态所需的所有写命令,所以只要读取AOF文件并全量执行一次就可以还原数据库关闭前的状态。还原步骤也很简单:
· 先创建一个不带网络连接的伪客户端,因为命令只能在客户端中执行,而要执行的写命令来源于AOF文件而不是网络请求,所以就无所谓了。
· 从AOF文件中逐条读取解析交给伪客户端执行
· 循环执行到AOF文件中的命令读完即可
AOF重写
既然AOF文件是记录写命令的,如果服务器一直运行下去,不做特殊处理的话AOF文件会越来越大,很可能对服务器甚至宿主计算机造成影响,并且AOF文件越大还原时间就越长。重写功能就是为了解决这个问题的。通过该功能,redis服务器可以创建一新一旧两个AOF文件。新的AOF文件是通过直接读取数据库状态来重写的,比如在旧的AOF文件中某个集合记录了5条单个的写入命令,那么在新的AOF文件中,其实就可以合并成一条批量写入命令,这样就能减少该集合的所需命令数量。但是为了避免还原时写入缓冲区溢出,单条批量命令最大只会记录64个元素,所以对于允许带有多个元素的类型的键,可能是多条批量命令记录在AOF文件内。
原理是这样,随之而来的两个问题肯定要想办法解决。第一个是重写命令会带来大量的写入操作,而redis作为单线程进程,为了不阻塞服务器的主进程,就要fork出一个子进程来执行aof_rewrite命令。第二个是重写过程中主进程依旧在接收处理写命令,而AOF重写是基于数据库的一个副本,这就会导致数据库最新状态和AOF文件保存的数据库状态不一致,所以设置了一个AOF重写缓冲区来记录重写执行过程中传入的写命令,在重写执行完毕后将缓冲区内的写命令再次同步到AOF新文件中,最后覆盖旧的AOF文件。整个过程中,只有重写执行完成后,将缓冲区内的命令写入AOF的操作会对主进程有一定的阻塞妨碍,如果重写过程中执行的写命令非常多,可能会导致主进程阻塞较久,这是不能接受的。
redis做了两点对于重写缓冲区的优化,重写开始时会创建父子进程通信的管道和一个文件事件。
· 重写过程中,该文件事件会通过该管道将重写缓冲区的内容发到子进程。
· 重写结束后,子进程会通过该管道尽量从父进程中读取更多的数据,每次等待1ms。最多执行1000次,也就是1秒,但如果连续20次没读取到数据,则结束这个过程。
过期键的处理
AOF文件写入时,如果过期键没有被删除,AOF文件也不会因为这个受到什么影响。过期键被删除后,会在AOF文件中追加一条Del命令来显式地记录该键已被删除。
AOF文件重写时,忽略过期键。
复制模式下,过期键的删除动作由主服务器控制,并显示的向所有从服务器发送一个Del命令来通知删除。在主服务器发出Del命令前,如果通过从服务器访问该key,即使已经过期了,也会当作没过期处理。
优点
· 比RDB更可靠,根据不同的fsync策略做持久化,宕机可能损失的数据量都比RDB小的多。
· 纯追加的日志文件,内容易懂,可以手动修改。
缺点
· 对于相同的数据集,AOF文件一般都比RDB文件大
· everysec、always的fsync策略仍是通过性能的牺牲来保证数据的可靠性。
· AOF文件中储存的阻塞命令可能会导致数据集无法正常恢复。
3.混合持久化
对已有的持久化方式的优化,只发生于AOF重写过程。本质就是执行AOF重写时,fork出的子进程先将当前全量数据以RDB当时写入AOF文件,然后将aof_buf中的增量命令追加到文件中,最后将旧的AOF文件替换掉。通过aof-use-rdb-preamble配置。
载入
使用AOF文件重建数据库状态时,会通过文件开头是否为REDIS来判断是否为混合持久化。是的话先加载记录的RDB内容,再增量的执行AOF内容记录的命令。
优点
结合了RDB和AOF的优点,更快的重写和恢复。
缺点
失去了AOF的可读性。
持久化的选择
如果追求数据的安全性,应该同时开启RDB和AOF,同时也可以启用混合持久化。
如果数据允许数分钟内的丢失,开启RDB即可。
如果不在意数据的丢失,可以关闭持久化功能。
Redis的过期键删除策略
在redis中,是通过一个额外的字典来存储键和过期时间的对应关系的,称这个字典为过期字典。通过过期字典就可以检查到键的过期设定,以此来判断键是否过期了。
redis的过期键删除策略,就是通过惰性删除策略+定期删除策略实现的。
常见的三种过期删除策略:
1.定时删除
在设置键过期时间时,生成一个定时器,让定时器在过期时间来临时立即执行键的删除。
优点:
可以保证过期键的即使清理,对内存十分友好。
缺点:
· 对CPU的占用,尤其是同一时间内过期键较多时,可能会影响到服务区的吞吐量和响应时间
· 频繁创建定时器,而且定时器在redis中是用时间事件实现的,本质就是一个无序链表,查找一个事件的时间复杂度位O(n),并不能高效的大量处理时间事件。
2.惰性删除
放任键过期不管,只有在取出键时才对键进行过期检查,这个时候才会去做删除键的操作。
优点:
不会在任何和当前无关的键上消耗CPU资源,对CPU十分友好。
缺点:
不删除意味着内存不释放,很可能导致大量内存消耗在无用的垃圾数据中,几乎等同于内存泄漏。
3.定期删除
每隔一段时间执行一次删除过期键的操作,并限制操作执行的时长和频率来减少对CPU的影响。
这种做法其实就是定时删除和惰性删除的折中策略,优点和缺点完全取决于执行时长和执行间隔的设定。
Redis内存淘汰策略
当redis的内存不足时,为了保证命中率,就会选择数据淘汰策略。在配置文件或者命令行里设置memory_policy的值。
八种策略
1.noeviction:默认策略。不淘汰任何数据,写入报错。
2.allkeys-lru:在所有的 key 中,使用 LRU算法淘汰部分 key。
3.allkeys-lfu:在所有的 key 中,使用 LFU算法淘汰部分 key。
4.allkeys-random:在所有的 key 中,随机淘汰部分 key。
5.volatile-lru:在设置了过期时间的 key 中,使用 LRU 算法淘汰部分 key。
6.volatile-lfu:在设置了过期时间的 key 中,使用 LFU 算法淘汰部分 key。
7.volatile-random:在设置了过期时间的 key 中,随机淘汰部分 key。
8.volatile-ttl:在设置了过期时间的 key 中,挑选 TTL短的 key 淘汰。
可以看出其实就是key的选择和淘汰规则的选择的排列组合。volatile代表过期键,allkeys代表所有键。
Redis客户端
redis服务器是典型的一对多服务器程序,可以与多个客户端建立网络连接。服务器通过管理一个clients链表来保存所有与服务器连接的客户端的状态结构,对客户端的操作都是通过遍历该链表来完成。
客户端可以分为两大类:伪客户端和客户端。简单理解一下就是,通过用户层面去连接或向redis服务器发送命令的,就是客户端;而Redis自身的一些能够发送命令的功能,就是通过构建一个伪客户端来实现的。