Redis工作原理

1、redis为什么快?

  根据官方数据,redis的QPS可以达到10万左右,速度快主要有以下几点:

  (1)KV结构的内存数据库,时间复杂度O(1)

  (2)单线程,好处在于:

      没有创建线程、销毁线程带来的消耗;

      避免了上下文切换导致的CPU开销;

      避免了线程之间的竞争问题,如加锁释放锁等。

  (3)多路复用:异步非阻塞IO,多路复用处理并发连接。

2、底层原理

2.1、虚拟存储器

  计算机主存(内存)可看作一个由M个连续的字节大小的单元组成的数组,每个字节有一个唯一的地址,这个地址叫做物理地址(PA)。早期的计算机中,如果CPU需要内存,使用物理寻址,直接访问主存储器。这种方式有几个弊端:

    (1)在多用户多任务操作系统中,所有的进程共享主存,如果每个进程都独占一块物理地址空间,主存很快就会被用完。我们希望在不同的时刻,不同的进程可以共用同一块物理地址空间。

    (2)如果所有进程都是直接访问物理内存,那么一个进程就可以修改其他进程的内存数据,导致物理地址空间被破坏,程序运行就会出现异常。

  为了解决这些问题,我们就想了一个办法,在CPU和主存之间增加一个中间层。 CPU不再使用物理地址访问,而是访问一个虚拟地址,由这个中间层把地址转换成物理地址,最终获得数据。这个中间层就叫做虚拟存储器(Virtual Memory)。

                     

  在每一个进程开始创建的时候,都会分配一段虚拟地址,然后通过虚拟地址和物理地址的映射来获取真实数据,这样进程就不会直接接触到物理地址,甚至不知道自己调用的哪块物理地址的数据。在 32 位的系统上,虚拟地址空间大小是 2^32bit=4G。在64位系统上,最大虚拟地址空间大小是

多少?是不是 2^64bit=1024*1014TB=1024PB=16EB?实际上没有用到64位,因为用不到这么大的空间,而且会造成很大的系统开销。Linux一般用低48位来表示虚拟地址空间,也就是2^48bit=256T。实际的物理内存可能远远小于虚拟内存的大小。

  总结:引入虚拟内存,可以提供更大的地址空间,并且地址空间是连续的,使得程序编写、链接更加简单。并且可以对物理内存进行隔离,不同的进程操作互不影响。还可以通过把同一块物理内存映射到不同的虚拟地址空间实现内存共享。

2.2、用户空间和内核空间

  为了避免用户进程直接操作内核,保证内核安全,操作系统将虚拟内存划分为两部 分,一部分是内核空间(Kernel-space),一部分是用户空间(User-space)。

            

  内核是操作系统的核心,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中,都是对物理地址的映射。

  在Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3。当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。进程在内核空间以执行任意命令,调用系统的一切资源;在用户空间只能执行简单的运算,不能直接调用系统资源,必须通过系统接口(又称

systemcall),才能向内核发出指令。

2.3、进程切换与上下文切换

  多任务操作系统是怎么实现运行远大于CPU数量的任务个数的?当然,这些任务实际上并不是真的在同时运行,而是因为系统通过时间片分片算法,在很短的时间内,将CPU 轮流分配给它们,造成多任务同时运行的错觉。为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,

并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。

  什么又是上下文切换呢?

  在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,也就是说,需要系统事先帮它设置好 CPU 寄存器和程序计数器(ProgramCounter),这个叫做CPU的上下文(可以成为任务的存储地址和运行状态)。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调

度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。在切换上下文的时候,需要完成一系列的工作,这是一个很消耗资源的操作。

2.4、IO数据拷贝与阻塞

  当应用程序执行 read 系统调用读取文件描述符(FD)的时候,如果这块数据已经存在于用户进程的页内存中,就直接从内存中读取数据。如果数据不存在,则先将数据从磁盘加载数据到内核缓冲区中,再从内核缓冲区拷贝到用户进程的页内存中。(两次拷贝,两次user和kernel的上下文切换)。

  从磁盘复制数据到内核缓冲区是阻塞的,从内核缓冲区拷贝到用户空间,也是阻塞的,直到 copycomplete,内核返回结果,用户进程才解除block的状态。

                                                 

2.5、IO多路复用

  I/O指的是网络I/O。多路指的是多个TCP连接(Socket或Channel)。复用指的是复用一个或多个线程。它的基本原理就是不再由应用程序自己监视连接,而是由内核替应用程序监视文件描述符。

  客户端在操作的时候,会产生具有不同事件类型的socket。在服务端,I/O 多路复用程序(I/OMultiplexingModule)会把消息放入队列中,然后通过文件事件分派器(File event Dispatcher),转发到不同的事件处理器中。

              

  多路复用有很多的实现,以select为例,当用户进程调用了多路复用器,进程会被阻塞。内核会监视多路复用器负责的所有socket,当任何一个socket的数据准备好了,多路复用器就会返回。这时候用户进程再调用read操作,把数据从内核缓冲区拷贝到用户空间。

               

  所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪(readable)状态, select()函数就可以返回。

  说一下Redis响应快以及平时所说的Redis是单线程的理解:

    Redis采用多路复用epoll+自己实现的事件框架,由内核监视套接字描述符,IO多路复用器将请求放入队列中,然后由事件分派器将队列中的不同请求事件分发到不同的事件处理器(这一步是多线程工作的),再有服务器处理具体的请求(这一步是单线程的,但是由于Redis是基于内存的,所以单线程避免了多线程带来的上下文切换以及处理并发的开销,单线程反而更有优势),最后将数据返回给事件处理器并返回给客户端。

3、内存回收

   内存回收主要分为两类,一类是key过期,一类是内存使用达到上限(max_memory)触发内存淘汰。可以通过expire、pexpire、expireAt、pexpireAt设置键的过期时间,实际前三个命令也是转化成pexpireAt去执行。

  redis使用expires 字典保存了所有键的过期时间,称为过期字典。

    ——过期字典的键是一个指针,指向键空间中的键对象(所以不会出现任何重复对象,也不会浪费任何内存)

    ——过期字典的值是一个long long 类型的整数,保存了过期时间(一个毫秒级的UNIX时间戳)

typedef struct redisDb{ 
   dict *dict; /* 所有的键值对 */ 
   dict *expires; /* 设置了过期时间的键值对 */ 
   dict *blocking_keys; /*Keys withclientswaitingfordata(BLPOP)*/ 
   dict *ready_keys; /*BlockedkeysthatreceivedaPUSH*/ 
   dict *watched_keys; /*WATCHEDkeysforMULTI/EXECCAS*/ 
   int id; /*DatabaseID*/ 
   long long avg_ttl; /*Average TTL,justforstats*/ 
   list *defrag_later; /*Listofkeynamestoattempttodefragonebyone,gradually.*/ 
}redisDb;

3.1、过期策略

  redis中同时使用惰性删除和定期删除策略

(1)定时删除

  对内存友好,对CPU不友好。在设置键过期时间时,创建一个定时器(timer),在键的过期时间来临时,立即执行键的删除操作。可以保证过期键会尽可能快地被删除,但是也有很大缺陷:

    ——当过期键较多时,删除过期键会占用很大一部分CPU时间,会影响服务器的响应时间和吞吐量。

    ——创建定时器需要用到redis服务器的时间事件,而时间事件存储在一个无序链表中,查找时间事件的时间复杂度为O(N),所以不能高效处理时间事件。

(2)惰性删除

  只有访问一个key的时候才会判断该key是否过期,过期则清除,不过期则返回。虽然对CPU友好,但是如果大量过期键不再被访问,会导致大量内存被占用。

(3)定期删除

   每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。定期删除函数activeExpireCycle工作

模式如下:

  ——在规定时间内,分多次遍历服务器中各个数据库,每次从数据库的过期expires 字典中随机取出一部分键进行检查,过期则删除。

  ——由全局变量current_db 记录函数的执行进度(记录是几号数据库),下次再执行时从当前位置开始。当把所有数据库都遍历完成,current_db 重置为0。

3.2、不同持久化模式对过期键的处理

(1)RDB模式

  当执行SAVE或BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。在启动Redis服务器载入RDB文件时,如果服务器为master节点,则过期键会被忽略,不会载入到数据库中;如果服务器为slave节点,则不论键是否过

期,都会被载入到数据库中。

(2)AOF模式

  当服务器以AOF模式运行时,AOF文件不会因过期键而产生任何影响,而是当过期键被删除后,程序会向AOF文件追加一条DEL命令,来显示记录该键已被删除。但是AOF重写过程中,处理过期键类似RDB文件,过期键不会被保存到重写后的AOF文件中。

(3)复制模式

  当服务器以复制模式运行时,slave节点过期键的删除由master节点控制,即使从服务器执行客户端的读请求遇到过期键,也像处理未过期键一样,所以在该模式下,对主从节点上的过期键访问时可能会得到不一样的结果。

3.3、淘汰策略

  redis的内存淘汰策略,是指当内存使用达到最大极限时,使用淘汰算法来决定清理部分数据,以保证新数据的存储。

(1)最大内存设置

  redis.conf参数配置:maxmemory <bytes>,如果不设置maxmemory 或者设置为0,64位系统不限制内存,32位系统最多使用3GB内存。

  动态修改:

redis>config set maxmemory 2GB

(2)淘汰算法

  LRU,Least Recently Used:最近最少使用。判断最近被使用的时间,距离当前时间最远的数据优先被淘汰。

  LFU,Least Frequently Used,最不常用,4.0版本新增。

  random,随机删除。

                                

  如果没有符合前提条件的 key 被淘汰,那么 volatile-lru、volatile-random 、volatile-ttl 相当于 noeviction(不做内存回收)。

  动态修改淘汰策略:

redis>config set maxmemory-policy volatile-lru

  建议使用 volatile-lru,在保证正常服务的情况下,优先删除最近最少使用的key。

(3)LRU淘汰原理

  *  redis LRU相比传统LRU有什么优点?

    传统LRU需要额外的数据结构存储,消耗内存。redis LRU通过随机采样来调整算法的精度。根据配置的采样值maxmemory_samples(默认是5个)随机从数据库中选择m 个key,淘汰其中热度最低的key。所以采样参数m 配置的数值越大,就能越精确的查找到待淘汰的数据,但是       

 也消耗更多的CPU计算,执行效率降低。

  *  如何找出热点最低的数据?

    redis中所有对象结构都有一个 lru 字段,且使用了unsigned的低24位,这个字段用来记录对象的热度。对象被创建时会记录lru值。在被访问的时候也会更新lru的值。但是不是获取系统当前的时间戳,而是设置为全局变量server.lruclock的值。

typedef struct redisObject{ 
    unsigned type:4; 
    unsigned encoding:4; 
    unsigned lru:LRU_BITS;/*LRUtime(relative togloballru_clock)or *LFUdata(leastsignificant8bitsfrequency *andmostsignificant16bitsaccesstime).*/ 
    int refcount; 
    void *ptr; 
}robj;

    server.lruclock的值怎么来的?

    Redis 中 有 个 定 时 处 理 的 函 数 serverCron ,默认每 100 毫秒调用函数updateCachedTime 更新一次全局变量的 server.lruclock 的值,它记录的是当前 unix时间戳。(之所以lru 不使用精确的时间,是因为更新数据的lru热度值时,不用每次调用系统函数time,可以提高执行效率)

(4)LFU——基于访问频率的淘汰机制

  当lru 字段(24 bits)用作LFU时,其被分为两部分:

    ——高16位用来记录访问时间(单位为分钟,ldt,last decrement time)

    ——低8位用来记录访问频率,简称counter(logc,logistic counter)

  counter是用基于概率的对数计数器实现的,8位可以表示百万次的访问频率。对象被读写的时候,lfu的值会被更新。

  增长的速率由,lfu-log-factor越大,counter增长的越慢。没有被访问的时候,计数器也会递减,减少的值由衰减因子lfu-decay-time(分钟)来控制,如果值是1的话,N分钟没有访问就要减少N。

4、持久化机制

  redis速度快,很大一部分原因是因为它所有的数据都存储在内存中。如果断电或者宕机,都会导致内存中的数据丢失。为了实现重启后数据不丢失,redis提供了两种持久化的方案,一种是RDB快照(RedisDataBase),一种是AOF(AppendOnlyFile)。

4.1、RDB

  RDB是redis默认的持久化方案。当满足一定条件时,会把当前内存中的数据写入磁盘,生成一个快照文件dump.rdb。实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。redis重启会通过加载dump.rdb文件恢复数据。

         

(1)RDB触发时机——何时写入rdb文件

*  自动触发

  1)配置规则触发

    redis.cof,SNAPSHOTING,其中定义了触发把数据保存到磁盘的触发频率,如果不使用RDB方案,可以将save注释掉或配置成空字符串。

save 900 1 #900 秒后至少有一个 key 被修改(包括添加) 
save 300 10 #400 秒后至少有 10 个 key 被修
save 60 10000 #60 秒后至少有 10000 个 key 被修改

    只要满足上面任意一个条件都会触发rdb 文件的写入。

  2)shutdown触发,保证服务器正常关闭。

*  手动触发

  如果需要重启服务或者迁移数据,这个时候就需要手动触RDB快照保存。Redis提供了两条命令:

  1)save

    save在生成快照的时候会阻塞当前Redis服务器, Redis不能处理其他命令。如果内存中的数据比较多,会造成Redis长时间的阻塞。生产环境不建议使用这个命令。

  2)bgsave

    执行bgsave时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体操作是Redis进程执行fork操作创建子进程(copy-on-write),RDB持久化过程由子进程负责,完成后自动结束。它不会记录fork之后后续的命令。阻塞只发生在fork阶段,一般时间很短。

(2)RDB 文件的优势和劣势

  *  优势

    1)RDB 是一个非常紧凑(compact)的文件,它保存了 redis 在某个时间点上的数据集。这种文件非常适合用于进行备份和灾难恢复。

    2)生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。

    3)RDB 在恢复大数据集时的速度比AOF的恢复速度要快。

  *  劣势

    1)RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,频繁执行成本过高。

    2)在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照之后的所有修改(数据有丢失)。

    3)由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

  如果数据相对来说比较重要,希望将损失降到最小,则可以使用AOF方式进行持久化。

4.2、AOF

  Redis 默认不开启AOF。AOF采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改Redis数据的命令时,就会把命令写入到AOF文件中。Redis 重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作。

    

(1)AOF配置

  配置文件redis.conf

# 开关 
appendonly no 
# 文件名 
appendfilename "appendonly.aof"

*  数据(写命令)何时持久化到磁盘?

  由于操作系统的缓存机制,AOF数据并没有真正地写入硬盘,而是进入了系统的硬盘缓存。什么时候把缓冲区的内容写入到AOF文件?

    

*  AOF文件越来越大,怎么办?

   由于AOF持久化是Redis不断将写命令记录到 AOF 文件中,随着Redis不断的进行,AOF 的文件会越来越大,文件越大,占用服务器内存越大以及 AOF 恢复要求时间越长。

  为了解决这个问题,Redis新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。可以使用命令bgrewriteaof来重写。AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,

然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件。

    

*  重写过程中,AOF文件被更改了怎么办?

         

  另外有两个与AOF相关的参数:

     

  * 触发AOF后台重写的条件
    AOF重写可以由用户通过调用BGREWRITEAOF手动触发。
    服务器在AOF功能开启的情况下,会维持以下三个变量:

        记录当前AOF文件大小的变量aof_current_size。
        记录最后一次AOF重写之后,AOF文件大小的变量aof_rewrite_base_size。
        增长百分比变量aof_rewrite_perc。
    每次当serverCron(服务器周期性操作函数)函数执行时,它会检查以下条件是否全部满足,如果全部满足的话,就触发自动的AOF重写操作:

      没有BGSAVE命令(RDB持久化)/AOF持久化在执行;
      没有BGREWRITEAOF在进行;
    当前AOF文件大小要大于server.aof_rewrite_min_size(默认为1MB),或者在redis.conf配置了auto-aof-rewrite-min-size大小;
    当前AOF文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比(在配置文件设置了auto-aof-rewrite-percentage参数,不设置默认为100%)
    如果前面三个条件都满足,并且当前AOF文件大小比最后一次AOF重写时的大小要大于指定的百分比,那么触发自动AOF重写。

(2)AOF 优势与劣势

  *  优势

    1)AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis 最多也就丢失 1 秒的数据而已。

  *  缺点

    1)对于具有相同数据的的 Redis,AOF 文件通常会比 RDF 文件体积更大(RDB存的是数据快照)。

    2)虽然 AOF 提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。在高并发的情况下,RDB 比 AOF 具好更好的性能保证。

4.3、两种方案比较

  如果可以忍受一小段时间内数据的丢失,毫无疑问使用 RDB 是最好的,定时生成RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。

  否则就使用 AOF 重写。但是一般情况下建议不要单独使用某一种持久化机制,而是应该两种一起用,在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。

4.4、混合持久化模式

  由于RDB持久化会造成数据丢失,而AOF恢复数据的时间较慢,未解决这个问题,Redis4.0开始支持混合持久化模式(4.0默认关闭,5.0默认开启),混合持久化由AOF的bgrewriteaof(重写)触发,先将重写命令执行钱的内存进行RDB快照,将RDB快照内容写入到新的AOF文件,然后将增量的AOF写操作命令也写入新的AOF文件,最后用新的AOF文件替换旧的AOF文件。混合模式下的AOF文件,前面是AOF格式,后面是RDB格式。

  Redis的持久化可以参考(讲的比较详细):https://blog.csdn.net/riemann_/article/details/117447615?spm=1001.2014.3001.5501

posted @ 2020-05-19 21:29  jingyi_up  阅读(2293)  评论(0编辑  收藏  举报