• Linux中间件之redis的淘汰策略和持久化


    一、redis 淘汰策略

    淘汰策略可以通过maxmemory-policy参数来选择。默认是禁止淘汰,如果数据达到了最大内存限制,在向redis中写入数据时会报错。

    1.1、背景

    redis是内存数据库,当数据量足够多时,如果不加以限制,会把当前系统的内存空间耗尽;所以,redis可以设置最大限定内存。当数据达到最大限定内存时,如何处理写数据命令?这就涉及到淘汰策略。

    (1)键过期:expire / pexpire。当key的生存周期达到时,将对应的key-value删除。
    (2)对象空转时长。redis支持丰富的数据结构,每次操作value时,redis记录操作的时间和统计对key-value操作次数(8位统计次数,16位记录时间)。

    // server.h
    typedef struct redisObject {
        unsigned type:4;
        unsigned encoding:4;
        unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                                * LFU data (least significant 8 bits frequency
                                * and most significant 16 bits access time). */
        int refcount;
        void *ptr;
    } robj;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    lru字段就是用于记录操作value的时间,也会统计对key-value操作了多少次。可以使用object idletime key查询key的空转时长(单位是秒)。

    127.0.0.1:6379> set fly 100
    OK
    127.0.0.1:6379> OBJECT idletime fly
    (integer) 37
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (3)配置。redis有两个参数配置淘汰策略,maxmemory和maxmemory-policy。maxmemory限定redis可以使用的最大内存(单位是字节 ),一般设置为当前系统可用内存的一半;maxmemory-policy用于制定淘汰策略。

    # redis.conf
    maxmemory <bytes>
    maxmemory-policy noeviction
    
    • 1
    • 2
    • 3

    1.2、过期key

    key设置了expire / pexpire的key。
    针对过期key,有如下策略可以使用:
    (1)volatile-lru,最近最少使用(最长时间没有使用)就把它删除。这种模式下 lru整个字段都用于记录时间。
    (2)volatile-lfu,最少次数使用就把它删除。这种模式下 记录操作的时间和统计对key-value操作次数(8位统计次数,16位记录时间)。
    (3)volatil-ttl,最近要过期就把它删除。TTL指令可以查询key还有多长时间到期。
    示例:

    127.0.0.1:6379> EXPIRE fly 100
    (integer) 1
    127.0.0.1:6379> TTL fly
    (integer) 93
    127.0.0.1:6379> TTL fly
    (integer) 23
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    (4)volatile-random,从已过期的key中随机淘汰。

    1.3、所有key

    如果没有使用expire / pexpire设置key过期属性,那么就从所有key中进行淘汰。

    (1)allkeys-lru,所有key中最近最少使用(最长时间没有使用)的就把它删除。这种模式下 lru整个字段都用于记录时间。
    (2)allkeys-lfu,所有key中最少次数使用的就把它删除。这种模式下 记录操作的时间和统计对key-value操作次数(8位统计次数,16位记录时间)。
    (3)allkeys-random,所有key中随机淘汰。

    二、redis 持久化

    redis持久化技术有两种:aof和rdb。

    2.1、背景

    (1)redis是内存数据库,不可能让redis一直运行下去,可能需要将redis重启或者部署到其他机器。一旦redis关闭,内存的所有数据都会丢失,所以需要将内存的数据持久化到磁盘中,方便从下一次启动从磁盘中将数据加载到内存当中,恢复到原来状态。
    程序宕机也需要通过持久化将数据恢复回来。

    (2)redis有四种持久化技术,其中有三种需要fork进程。这就涉及到内核fork进程写时复制机制。进程是通过页表操作内存的,fork复制的是页表而不是物理内存,它和父进程指向相同的内存块。

    操作
    进程
    页表
    内存块

    fork的写时复制是这样的过程:
    fork主要目的是克隆出和父进程一模一样的子进程,包括内存的数据;最开始为了fork的效率,复制的只是页表(页表主要起到映射作用),然后页表会被设置为只读状态。当下一次要操作redis,往redis写入或修改内存数据时,此时发现页表是一个只读状态,触发缺页中断,缺页中断处理函数中将物理内存复制一份,然后子进程页表的页会指向新的复制内存块;缺页中断之后,会把对应页表设置为可读可写状态。这就是整个写时复制的原理。

    fork
    主进程
    页表
    内存块
    子进程
    页表2
    缺页中断
    fork
    主进程
    页表
    内存块
    新的内存块
    子进程
    页表2

    (3)大key;大key是指key-value中value占用大量内存。比如value是hash或者zset,里面存储大量的元素。

    2.2、aof 和 aof-rewrite

    aof的全称是append of file,采用追加文件的方式进行持久化。顺序写(也就是追加)的方式操作磁盘的速度最快。aof存储的是命令协议,也就是增删改操作附加到aof上;如果redis重启了,要恢复数据就通过读取aof文件,把命令协议读到内存中进行重放(replay),也就是重新执行命令,达到恢复数据的目的。
    aof有三种策略:always、every_sec、no。这三种策略主要差异是fsync()的调用时机。
    (1)always:write()之后立即调用fsync()将数据刷到磁盘文件中。
    (2)every_sec:每秒去调用fsync()将数据刷到磁盘文件中;在bio_aof_fsync线程中执行。
    (3)no:不自己调用fsync(),由系统决定什么时候调用fsync()将数据刷到磁盘文件中。

    always方式的数据稳定性是最高的,确保每个写命令都能落盘;但是,它的代价也是最高的,因为它是在主线程进行的。every_sec代价是比较低的,因为它在bio_aof_fsync线程中执行的。no方式丢失数据的可能性是最高的。redis一般使用aof的every_sec方式,这个方式可能会丢1s~2s的数据风险。

    aof的缺点:
    aof会把所有写命令都写到文件中,只会包含一些冗余数据(比如一个key设置了多次value,其实真正需要的是最后一次的命令),aof会使文件非常大以及数据恢复非常的满。
    举例:

    127.0.0.1:6379> set fly 100
    (integer) 1
    127.0.0.1:6379> set fly 101
    (integer) 1
    127.0.0.1:6379> set fly 102
    (integer) 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    aof会把三个命令到保存到文件中,其实真正需要的是最后一次的命令;恢复数据是也是执行了三个命令,其实真正有效的是最后一次的命令。

    aof-rewrite:
    aof文件过大,aof数据恢复速度过慢,为解决aof的缺点,只记录最后一个状态,因此有了aof-rewrite的优化方案。
    aof-rewrite通过fork进程,根据内存数据生成命令协议写到aof文件,避免同一个key历史冗余数据,就可以将aof文件降到小一些;在重写aof期间,对redis的写操作会记录到重写缓冲区,当重写aof结束后,子进程通过信号告诉父进程,再把重写缓冲区附加到aof文件。

    追加
    fork
    根据内存数据
    写入
    主进程
    页表
    内存块
    重写缓冲区
    aof文件
    子进程
    页表2
    内存块
    生成命令协议

    这种方式虽然有了优化,但是效率还是较低,因为数据恢复还是通过重放(replay)方式,即重新执行命令的方式,需要消耗CPU,需要走命令处理流程。

    2.3、rdb和rdb-aof

    快照的持久化方式;直接拿内存数据直接持久化到磁盘中。直接将对象编码(ziplist、quiklist、intset、skiplist、int、embstr、raw等)落盘;它也是通过fork一个子进程,根据对象编码(二进制数据)直接落盘。由于是二进制数据,不管是落盘速度还是恢复数据速度都是最快的。
    值得注意的是,skiplist是多层级链表,恢复数据时只会拿最底层进行填充,然后通过少量的CPU运算恢复内存中的数据。

    redis默认的持久化方式是rdb。rdb持久化过程中,通常是5分钟一次。
    rdb的持久化方式是以内存为单位,而aof持久化是以单条命令为单位,所以如果主机宕机,rdb方式丢失数据是最严重的。因此,选择持久化方式时,需要在可靠性和效率直接做平衡。

    rdb-aof:
    考虑到在rdb过程中,redis还会处理命令,因此,在rdb持久化期间,对redis的写操作会记录到重写缓冲区,当rdb持久化结束后,再将重写缓冲区的aof附加到rdb文件末尾。

    rdb-aof文件
    rdb的二进制数据
    aof的命令协议数据

    这可以弥补rdb的不足。

    三、redis 持久化方案的优缺点

    (1)aof。
    优点:数据可靠,数据丢失较少;持久化过程中代价较低。
    缺点:文件过大,数据恢复慢。
    (2)rdb。
    优点:rdb文件小,数据恢复快。
    缺点:数据丢失较多,持久化过程代价较高(因为是根据内存数据全部持久化,占用CPU运算)。

    四、大key对持久化的影响

    (1)fsync()压力大。比如aof的always,它是在主线程中做的持久化,如果value非常的大,会长时间占用主线程,这就是一个耗时的操作,会影响redis的响应性能;大key对aof的every_sec的影响较小,因为它在另外的线程进行持久化的;aof的no对redis的影响也较小,因为fsync()由系统决定,因此压力在系统上。

    (2)fork时间比较长。redis中有一个object_info,会记录fork的时间,如果主线程fork超过1s,那么它的效率是非常低的,阻塞住的主线程。同时,写时复制造成持久化时间过长。

    总结

    fork进程写时复制机制。

    fork进程写时复制机制
    避免物理内存的复制时间过长导致父进程长时间阻塞
    fork页表复制,共用同一块物理内存
    在发生写操作时,系统才会去复制物理内存
    父进程对数据修改时,触发缺页中断,从而进行物理内存的复制
    子进程的页表将指向新的物理内存
    父进程的页表指向原来的物理内存

    redis的四种持久化方式。

    持久化技术
    aof
    直接刷盘,根据不同调用fsync的时机分为always、every_sec、no
    aof-rewrite
    解决aof历史冗余,fork进程方式根据内存数据生成命令协议
    rdb
    因为重放的方式恢复数据效率较低,rdb直接根据对象编码将二进制数据落盘,恢复数据的速度非常的快
    rdb-aof
    根据aof-rewrite原理,将rdb持久化期间的写操作数据追加到rdb持久化文件的末尾

    在这里插入图片描述

  • 相关阅读:
    SSM框架简单介绍
    钟汉良日记:你相信神话吗?你会背《正气歌》吗
    c语言操作符(超详解)
    Selenium自动访问Firefox和Chrome并实现搜索截图
    ⼀⽂读懂加密资产交易赛道的新锐⼒量Bitdu
    深度学习_1_基本语法
    Springboot集成ItextPdf
    反沙箱技术
    Linux锁定用户的几种方法
    【生产力++】脚本自动化提取待复习内容 极大提高复习效率(下)
  • 原文地址:https://blog.csdn.net/Long_xu/article/details/127484868