学习《Redis设计与实现》Chapter2

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自身的一些能够发送命令的功能,就是通过构建一个伪客户端来实现的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值