Redis面试题(三):持久化文件与淘汰策略

118 阅读10分钟

image.png


纠正一下:AOF三种写回策略是同步写,但AOF重写机制可以fork子进程来执行。\


Redis作为MySQL的缓存,能提供快速的读写,是基于内存的,当Redis宕机后,数据全部丢失,如何恢复缓存中的数据呢,最容易的办法是从数据库中同步这些数据到缓存中,若Redis本身既做缓存又做数据库,那如何处理?

原文地址:Redis面试题(三):持久化文件与淘汰策略

原文地址:Redis面试题(三):持久化文件与淘汰策略

原文地址:Redis面试题(三):持久化文件与淘汰策略

原文地址:Redis面试题(三):持久化文件与淘汰策略

欢迎关注我的公众号【意姆斯Talk】来聊聊Java面试,对线面试官系列持续更新中,因为模版图片不兼容,建议去原文地址观看。

AOF

先写缓存,然后再写AOF文件,AOF文件中记录的是每一条正确写入到缓存中的命令,Redis是一切从简,先写缓存后写日志的好处是:避免了像MySQL中分析器的作用:语法分析,词法分析,语义分析,也可以保证写入AOF文件中的命令是正确无误的。

优点:在命令执行完后,写入AOF文件,不会阻塞当前的写操作

\

但AOF文件具备两个风险:\

    • 若刚执行完一个命令,还未来得及写AOF文件,Redis宕机了怎么办?
      • 解决办法:
      • always:同步写回,每个写命令执行完,立马同步进磁盘。
      • everysec:每秒写回,每个写命令执行完,先把日志写进内存缓冲区,每隔一秒同步进磁盘中。
      • no:操作系统控制的写回,每个写命令执行完后,先把日志写进内存缓冲区,由操作系统决定何时同步进磁盘。

always可以确保数据基本不丢失,每个写命令后都有落盘操作,不可避免会带来主线程性能影响。everysec采用一秒写回一次的频率,避免了写一次同步一次的操作,减少了对性能的影响,若发生宕机,也只是会丢失一秒的数据。作为高级开发,权衡是必修课,everysec只能确保影响最小,性能最大。no性能最好了,Redis每次都把数据存储在缓冲区中,放完就走了,何时同步进磁盘都由操作系统决定,若宕机了,丢失的数据最多。
如果要获取高性能,则用no,如果要保证高可靠,就用always,如果既要保证高性能又要保证高可靠,就用everysec。

AOF的三种策略同步进磁盘,并非使用异步,也是由主线程来控制的。

\

随着命令执行得越多,AOF的文件大小就越大,Redis又提供了一种优化手段,AOF重写机制。\

AOF重写机制:Redis根据数据库的现状创建一个新的AOF文件,读取数据库中的所有键值对,然后对每一个键值对用命令记录它的写入,比如读到String类型的key和value,则写入set key value。AOF同步写入磁盘是追加写的,可以对一个key进行多次修改,比如set peter 01,set peter 02,set peter 03,追加写会导致AOF中保存了这三种命令,但AOF重写机制能保证只保存最新的命令,这样就大大减少了文件内存大小。

执行AOF重写机制时,是否会阻塞主线程? 每次执行重写时,主线程都会fork出一个子进程,在fork子进程时,子进程会复制父进程的内存页表,即虚拟内存和物理内存的映射关系表,在不拷贝物理内存的情况下,也能共享父进程的内存数据,因此,看起来好像是拷贝了最新的AOF文件数据,此时子进程和主线程互不影响,主线程还可以处理新来的请求。此时若有写操作,Redis会把这个写操作记录到缓冲区,同时fork子进程未执行完时,也会写进老AOF文件中,这里两个新老AOF都会写,若宕机,缓冲区的数据丢失,老AOF文件中还是有的,若未宕机,待子进程把所有的数据重写完成后,再把缓冲区中的最新写操作,同步进新的AOF文件中,保证此时的AOF文件是最新的,然后用新AOF文件代替旧的AOF文件,固不会阻塞主线程。

如果频繁的执行AOF重写命令,也会对主线程进行阻塞,因为在主线程fork子进程时,fork这个操作就是阻塞主线程的,子进程会拷贝父线程的内存页表,如果内存页很大,那么拷贝的时间也会很长,拷贝未完成之前是会一定会阻塞主线程的,拷贝完内存页之后,子进程和主线程都指向同一份内存地址,此时子进程才开始AOF重写。

在子进程重写期间,若有新的写请求执行,主线程会重新开辟一份空间,此时就做到了子进程和主线程真正的内存分离。

\

redis log中可以看见每次AOF重写后,主线程申请了多大的内存空间。

优化点:关闭内存大页机制(huge page),目的为了减少主线程申请内存时阻塞的概率。

何时出发AOF重写机制?

  • 手动执行bgrewriteaof命令,主线程fork子进程,来重写AOF文件,缩小文件大小。
  • 通过配置自动触发。


在恢复内存数据时,AOF是一行一行的执行命令,而RDB可以直接让快照数据加载进内存中。


RDB类似于拍照,记录时间段内的快照数据,支持主线程fork()子线程,来异步进行快照同步。注意:快照只是记录某一时刻的原始数据。步骤\

  • 执行bgSave命令时,主线程fork()子进程,使用子线程去执行持久化
  • 在RDB期间,子进程跟主线程是互不影响的,也是采用写实复制。子进程通过拷贝了主线程的内存页表,共享了主线程的数据,当主线程要修改正在快照中的数据时,会开启一块缓冲区,将修改的最新值记录在此,不会影响快照保存的数据。


bgsave是写时复制机制,  Redis借助操作系统提供的写时复制技术(Copy-On-write), 在生成快照的同时, 依然可以正常处理写命令, 原理: bgsave子进程是由fork生成的, 可以共享主线程的所有内存数据,  bgsave子进程运行后, 开始读取主线程的内存数据, 并把他们写入到RDB文件中
比如:t0时刻,执行了rdb持久化,此时peter:20,假设RDB持久化时间为20s,在t0+5时刻,peter:20修改为peter:30,此时我们应该记录的是t0时刻的peter:20,最新的数据只会在下一次快照时同步进RDB文件中。

图片


主进程fork()出的子进程,在进行RDB文件同步时,底层是使用的写实复制技术。当子进程拷贝完主线程的内存页地址后,主子进程都指向同一个内存地址,当有写操作触发的时候,才会进行分离,主进程会单独开辟一块新的空间记录这个修改操作。

图片


尽量要避免频繁的持久化,频繁持久化会带来两个影响:不断地fork()子进程和写入磁盘的消耗,用Redis时,应当时时刻刻具备权衡判断能力和是否会阻塞主线程的思想。一般我们为了权衡利弊:通常使用AOF和RDB混合持久化

一般我们推荐使用混合持久化:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据。在redis重启的时候, 可以先加载在RDB的内容, 再加载剩余的aof文件。\


**淘汰策略
**

  • noeviction
  • redis3.0版本后,默认情况下,当Redis中的内存空间超过阈值,则不会淘汰数据,再来了写请求,Redis将不会提供服务,而是直接返回错误。


以下四种跟过期时间有关,无论是过期时间快到了,或者到达了Redis的内存空间阈值,都会根据规则进行淘汰

  • volatile-random
  • 在设置过期时间的键值对中,进行随机删除。
  • volatile-ttl
  • 在设置过期时间的键值对中,根据过期时间的先后进行删除,越早过期的越先删除。
  • volatile-lru
  • 会使用LRU算法筛选设置了过期时间的键值对
  • volatile-lfu
  • 会使用LFU算法选择设置了过期时间的键值对
  • \

若无设置过期时间,则范围会扩大,针对于所有的键值对\

  • allkeys-lru
  • 使用LRU算法在所有键值对中进行筛选删除。
  • allkeys-random
  • 从所有的键值对中随机选择并删除数据。
  • allkeys-lfu
  • 使用LFU算法在所有键值对中进行筛选删除。
  • \

如果一个键值对被淘汰策略选中,那么有可能他过期时间还未到,或者未设置过期时间,他都会被删除。

LRU算法,Redis面试题(一):常见的底层结构,在redisObject中有一个字段就是记录LRU属性的


typedef struct redisObject {//  总空间:  4 bit + 4 bit + 24 bit + 4 byte + 8 byte = 16 byte      unsigned type:4;        //  分别存储五种常用的数据类型,String,List,Set,Hash,Zset    unsigned encoding:4;    //  更细分,存储上面的编码方式    unsigned lru:LRU_BITS;  //  lru时间, 用于redis的淘汰机制的    int refcount;           //  共享对象,被引用了多少次      void *ptr;              //  指向sds地址,sds分多个结构体} robj;

记录当前对象最近被访问的时间,LRU算法是根据最少使用的原则来进行删选LRU会把所有的数据组织成一个链表,链表的头和尾分别表示MRU端和LRU端,MRU表示最近刚访问的数据,LRU表示很久没有访问的数据。假设咱们有1,2,3,4,5。五条数据,当先访问了5,则5位于表头,再访问了4,则4在表头,依此访问,链表的最终顺序为5,4,3,2,1。其实Redis的内存空间不够了,要写入6时,会把LRU端的数据给删掉,更新链表的数据为6,5,4,3,2。

LRU算法思想:最早被访问的优先淘汰,最近访问的,可能会再访问,在缓存内存满时,优先删除LRU端的元素。但每次有数据被访问时,都需要挪动链表顺序,时间复杂度是O(n),为了防止降低Redis性能。Redis在每个RedisObject中设置了lru字段,对LRU算法进行了优化,会随机选出一批数据(数据大小根据配置文件设置),组成一个回收集,从这回收集中把lru字段值最小的数据从缓存中淘汰出去,减去了链表的维护。当需要再次淘汰数据时,Redis继续挑选一批数据进入到回收集,进入回收集的必要条件:新的一批数据必须是小于回收集中最小的lru值,当回收集的个数到达配置值时,又会优先把最小的lru值元素给淘汰。

CONFIG SET maxmemory-samples 100// 回收集好像有点G1回收垃圾的味道