总结Redis持久化、服务处理模型

  • 时间:
  • 浏览:
  • 来源:互联网

文章目录

  • 服务模型
    • 文件事件
    • 时间事件
    • 一次请求过程
    • 数据库
    • 过期键删除策略
    • 内存淘汰策略
  • 持久化
    • RDB
    • AOF
    • 总结

服务模型

redis服务器可以与多个客户端建立连接,客户端将命令请求通过网络传输给服务器。redis服务器从在相应的数据库上执行读写操作。

redis服务是通过单线程单进程的方式处理客户端的请求。当一个redis客户端与redis服务器建立连接之后,服务器为客户端在服务器内部维护一个结构体RedisClient表示客户端状态,这个结构体用于保存当前客户的上下文信息。主要就是与客户端建立连接的套接字描述符、输入/输出缓冲区等与客户端有关的信息。

套接字描述符属性记录了客户端正在使用的套接字描述符。而输入缓存区用于保存redis客户端向redis服务器发送的命令,输出缓冲区用于保存redis服务器执行完毕命令,待回写到redis客户端的命令。

redis服务器可以一次性服务多个用户,这些用户使用链表结构组织起来。redis本质上就是一个事件驱动程序,可以处理文件事件和时间时间。文件事件就是服务器对套接字操作的抽象,因为send和receive本身就可以看作特殊的文件接口,而对端的socket输入缓冲区就是打开的文件。时间时间是需要在给定时间点执行的事件,是服务器定时操作的抽象
redis的文件处理器是reactor模型,基于事件驱动的。文件事件处理器使用I/O多路复用程序可以同时监听多个套接字,并且注册感兴趣的事件,包括accept(连接建立事件)、read(读事件)、write(写事件)、close(关闭事件)、slaveof(主从复制)等

Redis单进程线程的架构意思是:从网络IO到实际处理读写事件、时间事件等都是有单个线程基于I/O复用完成的。并不是整个redis中只有一个主线程和单一进程。
Redis 6支持多线程技术,仅针对处理网络请求的过程采用了多线程,而数据的读写命令仍然采用单线程进行处理。这里使用多线程IO的原因是在等待网络IO的时候最大化利用CPU资源

多路复用的IO模型,处理网络请求的时候,select()调用是阻塞的。如果并发量很高的情况下,可能成为瓶颈。多线程可以利用CPU多核的优势,使得多个线程并行。当select()调用返回的时候,请求依次交给多个线程去处理,充分利用CPU多核的优势。

但是处理事件(执行事件处理器)本身是很快的,不存在CPU瓶颈。而且可以避免线程安全问题。
虽然多线程模型执行读写事件能够提升并发性能,但是引入了多线程会使得程序的执行具有不确定性,还会造成额外的切换开销

redis基于内存数据库,本身在执行上不存在CPU瓶颈。如果采用多线程反而会增加上下文切换带来的开销,以及线程安全问题,为程序的执行带来不确定性。redis采用I/O多路复用模型,使得它可以同时响应多个事件,这极大地提升了I/O利用率。另外redis的对象底层根据不同的场景,会使用不同的数据结构进行实现,优化了性能。
redis真正的瓶颈在于网络带宽和机器内存大小

(redis4.0也支持了多线程技术,主要是用于后台处理包括对象回收、过期键回收等redis服务器部分的时间事件的功能)

文件事件

文件事件处理器由四个部分组成:套接字、I/O多路复用程序、文件事件分派器(dispatcher)、事件处理器(controller)
【1】redis客户端与redis服务器建立连接后,redis服务器就会在内存中维护一个redis客户端的对象,并且将redis客户端的套接字注册在I/O多路复用程序上
【2】I/O多路复用程序监听这些套接字,一旦有感兴趣的事件发生,就会传输产生了事件的套接字给文件事件分派器(他们通过队列通信,I/O复用程序将套接字同步有序的放入队列,而分派器则从队列中取出,类似基于生产者消费者模型的阻塞队列)
【3】分派器根据文件事件的种类,调用相应的事件处理器接口(事件处理函数,例如连接应答、命令请求、命令回复)(可以类比springMVC中的dispatcherSerlet和controller)

其中I/O多路复用程序底层依赖/O复用类库如select、epoll等。

时间事件

时间事件主要分为定时事件和周期事件,一个事件是定时事件还是周期事件取决于时间事件处理器的返回值(redis内部定义的常量值)。服务器会将所有的时间事件存入一个无序的链表。当时间事件执行器运行的时候,他就遍历整个链表,找到所有已达到的时间事件,并调用相应的事件处理器。redis作为内存数据库,遍历链表的操作是可以达到常量级别的。
redis服务器需要定时对自身的资源和状态进行检查和调整,这些定时操作由serverCron函数负责,包括定期清理过期键值对、更新记账信息、与从服务器定期同步、定期持久化操作等。正常模式下,redis服务器只运行serverCron一个时间事件,并且是周期事件

serverCron默认每隔100毫秒执行一次,负责管理服务器的资源。包括更新LRU时钟、更新服务器每秒执行命令的次数、更新服务器内存峰值记录、处理kill (15即sig term) 的信号、检查持久化操作的运行状态。
serverCron函数每次执行的时候,都会调用clientsCron函数管理客户端资源,以及调用databasesCron函数,管理数据库资源

文件事件和时间时间都是原子、有序、同步地执行的,它们都会尽可能少地减少程序的阻塞时间,并且在有需要的时候主动让出执行权。如果执行时间、数据大小超过预设的阈值,通常会留在下一轮事件循环中执行。它们之间是合作关系,服务器会轮流执行这两个事件,并且执行过程不会互相抢占。

一次请求过程

【1】当redis客户端向redis服务器发送一个命令请求时,这个命令会以某种格式(自解释协议),通过连接传输到对端的套接字。当命令成功传输到对端后,服务器对应该客户端的连接套接字变得可读,该套接字经由分派器转发到对应的读处理器进行处理——将请求保存到输入缓冲区,将请求命令与参数进行解析并保存到客户端状态中。

客户端的命令传输到服务器后,服务器将命令保存在客户端状态的输入缓冲区,并按照协议解析出命令命令参数。服务器会搜索命令表(一个KV结构,命令标识->命令实现函数),并找到为命令的抽象出的对象RedisCommand(结构体),并且将保存在服务器的客户端状态中的cmd属性指向RedisCommand对象。接着服务器就可以执行这个命令请求,并向客户端返回结果。

【2】调用命令执行器的接口,执行命令。命令执行器根据请求的命令标识在命令表中查找命令对象RedisCommand,并且保存到客户端状态的cmd属性(RedisClient对象的RedisCommand成员cmd指向目标RedisCommand对象)。在命令正式执行之前还会做一些校验工作包括检查执行权限、参数校验、检查内存空间等。
【3】RedisCommand的proc属性是一个函数指针,指向命令的实现函数。服务器执行命令就是回调RedisClient的cmd成员指向的RedisCommand对象的proc回调函数。最终的调用结果将会保存在客户端状态的输出缓冲区。此时连接套接字的状态变为可写,分派器则将套接字分派给命令回复处理器进行处理。(当redis客户端收到命令,它会将信息转换为可读性更高的格式输出到终端)
【4】最后就是一些记账相关的操作,例如记录慢查询日志、保存到AOF缓冲区、命令传播给从服务器、更新RedisCommand对象的执行耗时milliseconds和调用计数器calls属性

数据库

redis服务器将所有的数据库状态都保存在了服务器状态RedisServer的db数组中,每一项都是一个redisDb结构体。每个redis客户端都可以使用服务器的某个数据库,RedisClient的RedisDb类型指针db指向当前数据库,切换数据库就是切换RedisClient的db指向(即select命令)。
RedisDb结构的字典保存了数据库中所有的键值对,这个字典也称为键空间。与数据库的交互本质上是与键空间这个字典进行交互。

通过expire或pexpire(precise)命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(TTL)。当到底过期时间后,会达到一个逻辑删除的效果——键不会被立刻删除,而是由定时事件处理器serverCron进行异步删除,但是在获取的时候会“视而不见”
通过TTL或PTTL命令可以查询这个键还有多少时间过期。

setex time就是将set k v 和 expire k time 合成了一个原子命令,但是只能对字符串使用。

RedisDb结构的expire字典保存了数据库中所有键的过期时间,称为过期字典。键空间的键和过期字典的键都是指针类型,他们共同指向同一个对象(RedisObject结构体),因此不会造成空间的浪费
当expire命令执行成功后,数据库中的目标键就会被关联上一个过期时间。(persist命令可以解除过期时间,尤其是检测到某些数据是当前热点数据)

过期键删除策略

redis的每个对象都是RedisObject结构体的实例,而删除一个键其实就是将这个对象close掉。
redis支持两种删除过期键的策略。
【1】惰性删除
对内存不友好,因为如果一段时间内不释放内存,那么这些内存是一种内存泄露的表现。对CPU时间友好,因为redis服务器在执行用户任务的同时,顺便释放了无用的内存空间。
惰性删除的引入,其中就是在命令真正执行之前多加了一层判断,判断当前的键是否过期或者存在
【2】定时删除
对内存有好,但是会在某一时间段内窃取CPU周期,相当于文件事件和定时事件需要竞争CPU周期。
定时删除操作由时间函数serverCron负责,它会在规定的时间内分多次遍历服务器的各个数据库,从数据库的过期字典中随机检查一部分键的过期时间,并删除其中的过期键。

为了保证软实时并没有遍历检查所有的key,而是随机抽取若干个key进行检查,并且删除其中过期的key。这是因为redis可以存储成千上万个key,每次都遍历所有的key是不现实的,更加难以实现软实时。

生成RDB文件的时候,已经过期的键不会被保存到新建的RDB文件中。如果服务器开启了AOF功能,但是写入AOF文件后这个键才过期,程序会向AOF文件追加一条DEL命令来显示删除目标键。已经过期的键也不会被保存到重写后的AOF文件。
当启动服务器并载入RDB文件的时候,如果是主服务器,RDB文件中过期的键会被直接忽略。如果服务器以从服务器的模式运行,载入RDB文件的时候过期的键也会被保存到数据库中,不过在数据同步的时候,过期键就会被清除掉。(命令传播阶段会收到主服务器传递的DEL命令)

在一主多从模式下,无法达到实时一致性,只能达到最终一致性。这是C(一致性)A(可用性)P(分区容错性)选择后两者的权衡

内存淘汰策略

不管是惰性删除还是定时删除,都无法精确的删除所有过期键,并且不考虑过期键,redis本身也是有上限的(在开始maxMemory之后)。因此必须考虑内存淘汰(过期键导致的内存泄露以及内存不够用导致的内存溢出)的场景。

内存淘汰策略基本就是各种“删除类型”以及“删除算法”的组合

首先,默认的策略就是内存超过maxMemory的时候直接抛出异常/返回错误
删除范围包括:所有键allkeys、配置了过期时间的键volatile
删除算法包括:LRU最近的长时间未使用的、LFU最近的使用频率最小的、ttl马上过期的、random随机删除

持久化

redis的数据库状态使用RedisDb结构体定义,RedisServer维护RedisDb的数组,为了将数据库状态保存起来可以使用redis的持久化功能。

RDB

RDB持久化是默认开启的,是采用快照的方式将数据库状态在某一个时间点的数据进行截取,并保存仅一个RDB文件。RDB文件是一个经过压缩的二进制文件
redis有两种方式可以生成RDB文件,save命令会阻塞redis服务器进程。bgSave会fork出一个子进程,然后由子进程负责创建RDB文件。仅仅在fork()调用创建子进程的时候会进行阻塞。

如果数据十分庞大或内存吃紧,以至于fork()创建子进程的代价十分大,可以考虑save

RDB文件的载入工作是在服务器启动的时候自动执行的,只要redis服务器在启动的时候检测到RDB文件的存在,就会自动载入RDB文件。
但是如果服务器开启了AOF功能,会优先使用AOF文件进行数据库状态的还原。只有当AOF功能处于关闭的时候,服务器才会使用RDB文件进行数据库状态的还原

同一时间内只能执行一个bgsave命令,在某一个bgsave命令执行期间,客户端发送的save和bgsave命令都会被拒绝,因为它们会产生竞争条件,这是从性能的方面做出的考虑——如果并发执行多个子进程,那么它们将同时做出大量磁盘写入操作,降低性能。

服务器在载入RDB期间,会一直处于阻塞状态。

可以通过设置redis.conf的save选项,设定服务器每隔一段时间自动执行的周期

触发机制:
【1】满足save的规则情况下,会自动触发rdb规则
【2】执行flushall也会触发rdb规则
【3】退出Redis,也会产生rdb

bgsave的实现原理
Redis会单独创建(fork)一个子线程进行持久化,会先将数据写入一个临时文件中,待持久化过程结束了,再使用这个临时文件替换之前的持久化文件。(dump.rdb)
整个过程,主线程是不进行任何IO操作的,这确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那么RDB更加高效。另外,如果redis意外宕机,可能造成一段时间的数据丢失,同时数据量如果很大,导致需要复制大量数据,fork()可能很耗时。

子进程创建后,和父进程共享数据段、代码段,本质上是将子进程的页面指针指向了父进程的页表,并且将映射目标数据页的页表项标记为只读,当父进程或子进程(即共享了该物理内存/数据页的进程)视图修改这些数据的时候会出现异常导致CPU陷入内核。操作系统将会为子进程分配独立的地址空间,陷入退出后,子进程和父进程的地址空间就是独立的了。

AOF

AOF通过保存redis服务器所执行的写命令来记录数据库状态,类似于mysql binlog的statement格式。被写入redis的命令按照请求协议格式保存,是纯文本格式的,可读性较高(命令传输的时候也是按照这个格式,命令传播一定程度上也通过AOF实现)。
AOF功能默认是关闭的,需要append only yes进行开启。

AOF的实现分为三个部分:
【1】服务器执行完一个(写)命令后,会以协议格式追加到服务器状态(RedisServer)的缓冲区中(就是一个sds字符串)
【2】每一个redis服务器的事件循环的末尾,都会考虑是否将内存缓冲区的内容不同到文件中(每次都会将redis缓冲区的内容复制到操作系统的page cache,但是刷盘事件根据配置文件的参数而定)。

可以在redis.conf配置文件中配置三种选择:每个操作都同步always、每秒everysec(默认)、仅仅复制到操作系统的page cache,刷盘时机取决于操作系统 no。

如果redis突然掉电,需要分情况讨论:如果使用AOF的配置对每一条指令同步磁盘,则不会丢失数据。如果是定时sync,如每N秒syn一次,则最多丢失N秒内的数据

Redis 是先执行写操作,在将记录保存在AOF日志中的,好处:
【1】避免额外的检查开销(只有当该命令执行成功时,才会将命令记录在AOF日志中,不需要额外的检查开销,保证AOF日志中的命令一定是正确的)
【2】不会阻塞当前写操作命令的执行
风险:掉电后,AOF日志可能会丢失至少一次事件循环的操作

当载入AOF持久化文件的时候,会创建一个本地客户端(伪客户端),伪客户端从本地AOF文件中读取指令并发送给redis服务器执行命令。

AOF由于记录的是命令,并且是基于文本格式的,为了防止AOF文件体积膨胀过大的问题(本质上是为了缩短数据恢复的时间),redis提供了AOF文件重写的功能。redis会创建一个新的AOF文件去替换原来的文件,并且体积小很多。该功能通过读取当前服务器状态进行实现——从数据库中读取键值,使用一条命令去记录键值对,替代原来的记录该键值的多条命令
一般常用AOF后台重写的功能,通过为了保证当前数据库状态和重写后的AOF文件保存的数据库状态是一致的,redis额外设置了AOF重写缓冲区,服务器创建子进程之后开始使用。

当redis服务器执行完一个写命令之后,他会同时将这个命令发送给AOF缓冲区和AOF重写缓冲区
,当AOF重写任务执行完毕后,子进程向父进程发送信号,父进程调用事件处理函数,将AOF重写缓冲区的内容保存进一个新的AOF文件中,并原子地覆盖旧的AOF文件。

重写缓冲区不是一次申请好的,而是边用边申请的,当无法继续申请时打印一条日志后进程退出,

为什么bgsave和AOF重写日志的时候通常创建子进程,而不是子线程
因为如果创建的是线程,多个线程之间会共享内存,那么访问执行快照生成或AOF缓冲区访问的时候必须先申请锁(如果主线程要执行写,还可能会被阻塞),这会降低性能。
而采用子进程时,基于COW,子进程即可以不加锁地读取原副本,一旦父进程的主线程(进程)执行写操作,生成独立数据副本,减少锁的开销。

总结

RDB持久化本质上是数据快照,而AOF持久化本质上是记录增量指令。

RDB适合做冷备份灾难恢复 ,它会生成多个数据文件,每个数据文件代表某一时刻redis里面的数据。如果服务挂了,可以拷贝前若干分钟的数据。同时,RDB对redis的性能影响非常小,因为同步数据的时候,redis会fork一个子进程进行持久化,而它在进行大数据量恢复的情况下,速度也快于AOF。
由于RDB是快照文件,不宜频繁的生成,默认五分钟生成一次快照文件。而且如果生成的文件很大可能会使得客户端的正常请求收到影响。

AOF比RDB更加可靠,默认AOF一秒一次(everysec)去通过一个后台线程fsync操作,最多丢失一秒钟的数据,而且AOF文件的可读性更高。性能上,AOF将每个增量命令追加到内存缓冲区,并通过异步的方式进行磁盘文件同步,不会造成主线程的阻塞。适合进行热备份
但是,同样的数据,AOF文件往往比RDB更加大,加载持久化文件的时候执行速度也慢一些。AOF开启后,redis的QPS(每秒查询次数)比基于RDB持久化更低,因为每秒都需要额外去异步刷新日志,相当于窃取了CPU时间。

两种机制全部开启的时候,redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据比RDB更加完整

选择:先使用RDB数据恢复(快),然后使用AOF做数据补全(出事瞬间,数据丢失少)
RDB做镜像全量持久化,AOF做增量持久化。RDB会耗费较长时间,不够实时,而且宕机时可能丢失大量数据,需要AOF配合。

在redis实例重启的时候,会使用RDB持久化文件构建基础的数据库状态,然后使用AOF重放最近的操作指令,来完整恢复重启之前的状态。

本文链接http://metronic.net.cn/metronic/show-53492.html