Reids的内存模型与内存调优

Reids的内存模型与内存调优

Redis的内存模型

查看Redis的内存统计

  • used_memory

    • 由Redis内存分配器分配的数据内存和缓冲内存的内存总量(字节),包括使用的虚拟内存(即swep)

    • used_memory_human :只是量化显示,人性化显示

  • used_memory_rss

    • 记录的是由操作系统分配的Redis进程内存和Redis内存中无法再被内存分配器分配的内存碎片(字节)

    • used_memory 与 used_memory_rss 的区别

      • 在实际应用中,Redis的数据量会比较大

      • 此时进程运行占用的内存与Redis数据量和内存碎片相比,会小很多

      • 因此两者的比列,就是衡量Redis内存碎片率的一个重要参数

      • 这个参数就是:mem_fragmentation_ratio

  • mem_fragmentation_ratio

    • 内存碎片率,为 used_memory_rss / used_memory 的比值

    • 当其小于1时,就说明使用了虚拟内存,由于虚拟内存的媒介是磁盘,所以速度开始下降

    • 此时就应该排查,增加Redis节点,增加Redis服务器内存,优化应用等手段避免这种情况的发生

    • 1.03左右是比较健康的状态,偏高就是碎片率过高

    • 至于上图的14.73,那是因为我们没在Redis中储存数据,导致used_memory_rss 比used_memory大得多

  • mem_allocator

    • Redis使用的内存分配器,在编译时指定;可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc

Redis的内存划分

  1. 数据存储占用

    • 做一个内存数据库,数据占用的内存板会统计在used_memoty中

    • Redis使用键值对储存数据,常用的比如字符串、哈希、列表、集合、有序集合

    • 这五种类型是Redis对外提供的,在Redis内部,每一种都有>=2种的编码实现

  2. 进程占用

    • Redis主进程的运行肯定要占用内存,为几兆大小,在生产环境中科忽略不计

    • 这部分内存不是由jemalloc分配,故不在used_memory的统计中

  3. 缓冲内存占用

    • 缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等

    • 客户端缓冲区:记录客户端连接中的输入输出缓冲

    • 复制积压缓冲区:用于部分复制功能使用

    • AOF缓冲区:用于在进行 AOF 重写时,保存最近的写入命令

  4. 内存碎片占用

    • 内存碎片是 Redis 在分配、回收物理内存过程中产生的

    • 例如,如果对数据的更改频繁,而且数据之间的大小相差很大,可能导致 Redis 释放的空间在物理内存中并没有释放

    • 但 Redis 又无法有效利用,这就形成了内存碎片,内存碎片不会统计在 used_memory 中

    • 如果 Redis 服务器中的内存碎片已经很大,可以通过安全重启的方式减小内存碎片

    • 重启之后,Redis 重新从备份文件中读取数据,在内存中进行重排,为每个数据重新选择合适的内存单元,减小内存碎片

Redis储存数据的细节

  • Redis是一个KV形式的键值对内存数据库

  • 五种基本类型都是针对V而言的

  • 下图我们以set hello world 为列,说明Redis的数据储存细节

  • dictEntry

    • 每个键值对都会有一个dictEntry,里面存储了指向Key和Value的指针;next指向下一个dictEntry,与本Key-Value无关

  • Key

    • 图中右上角可见,Key(”hello”)并不是直接以字符串存储,而是存储在SDS结构中。

  • redisObject

    • Value(“world”) 不是直接以字符串存储,也不是像Key一样直接存储在SDS中,而是存储在redisObject中

    • 不论Value是5种类型的哪一种,都是通过redisObject来存储的

    • 而redisObject中的type字段指明了Value对象的类型

    • ptr字段则指向对象所在的地址

    • 字符串对象虽然经过了redisObject的包装,但仍然需要通过SDS存储

  • jemalloc

    • 无论是DictEntry对象,还是redisObject、SDS对象,都需要内存分配器(如jemalloc)分配内存进行存储

内存分配器 : jemalloc

  • Redis在编译时便会指定内存分配器;内存分配器可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc

  • jemalloc作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好

  • jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围

  • 每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。

  • 如果需要存储大小为10字节的对象,jemalloc会将其放入16字节的内存单元中

redisObject

  • Redis对象有5种类型;无论是哪种类型,Redis都不会直接存储,而是通过redisObject对象进行存储

  • Redis对象的类型、内部编码、内存回收、共享对象等功能,都需要redisObject支持

  • Redis中的每个对象都是由如下结构表示(列出了与保存数据有关的三个属性)

{
    unsigned type:4;//类型 五种对象类型
    unsigned encoding:4;//编码
    void *ptr;//指向底层实现数据结构的指针
    //...
    int refcount;//引用计数
    //...
    unsigned lru:24;//记录最后一次被命令程序访问的时间
    //...
}robj;
  • type字段:表示对象的类型,占4个比特;目前包括字符串、列表、哈希、集合、有序集合。

    • 当我们执行type命令时,便是通过读取RedisObject的type字段获得对象的类型

127.0.0.1:6379> set key value
OK
127.0.0.1:6379> sadd  myset value1 value2 value3
(integer) 3
127.0.0.1:6379> type key
string
127.0.0.1:6379> type myset
set
127.0.0.1:6379>
  • encoding:encoding表示对象的内部编码,占4个比特

    • 对于Redis支持的每种类型,都有至少两种内部编码,例如对于字符串,有int、embstr、raw三种编码

    • 通过encoding属性,Redis可以根据不同的使用场景来为对象设置不同的编码,大大提高了Redis的灵活性和效率

    • 以列表对象为例,有压缩列表和双端链表两种编码方式

      • 如果列表中的元素较少,Redis倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入

      • 当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双端链表

    • 通过object encoding命令,可以查看对象采用的编码方式: object encoding key

    • 5种对象类型对应的编码方式以及使用条件,将在后面介绍

  • ptr:ptr指针指向具体的数据,如前面的例子中,set hello world,ptr指向包含字符串world的SDS

  • refcount 与 共享对象

    • refcount记录的是该对象被引用的次数,类型为整型 ,refcount的作用,主要在于对象的引用计数和内存回收

      • 当创建新对象时,refcount初始化为1;当有新程序使用该对象时,refcount加1;当对象不再被一个新程序使用时,refcount减1;当refcount变为0时,对象占用的内存会被释放

      • Redis中被多次使用的对象(refcount>1),称为共享对象

      • Redis为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。这个被重复使用的对象,就是共享对象。

      • 目前共享对象仅支持整数值的字符串对象

    • 共享对象的具体实现

      • 共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间

      • 对于整数值和字符串判断相等的操作难度很小,相反集合、hash、有序集合的操作难度就很高

        • 所以为了性能平衡点,只对整数和字符串支持

      • 但是5种类型都可能使用共享对象 ,只是默认只支持整数和字符串

        • Redis服务器在初始化时,会创建10000个字符串对象,值分别是0~9999的整数值;当Redis需要使用值为0~9999的字符串对象时,可以直接使用这些共享对象

      • 查看某个值被引用的次数object refcount key

    • lru:记录的是对象最后一次被命令程序访问的时间

      • 通过对比lru时间与当前时间,可以计算某个对象的闲置时间

      • object idletime key命令可以显示该闲置时间(单位是秒)

      • 如果Redis打开了maxmemory选项,且内存回收算法选择的是volatile-lru或allkeys—lru

        • 那么当Redis内存占用超过maxmemory指定的值时,Redis会优先选择空转时间最长的对象进行释放

    • 总结

      • 综上所述,redisObject的结构与对象类型、编码、内存回收、共享对象都有关系;

SDS

  • Redis没有直接使用C字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示,而是使用了SDS

  • redis 3.2 版本之前

    • struct sdshdr{
      //记录buf数组中已使用字节的数量,等于 SDS 保存字符串的长度
      int len;
      //记录 buf 数组中未使用字节的数量
      int free;
      //字节数组,用于保存字符串
      char buf[];
      }
    • buf表示字节数组,用来存储字符串;len表示buf已使用的长度,free表示buf未使用的长度。下面是两个例子

      • 通过SDS的结构可以看出,[buf数组的长度=free+len+1(其中1表示字符串结尾的空字符)

  • Redis 3.2版本之后

    • typedef char *sds;
      struct __attribute__ ((__packed__)) sdshdr5 { // 对应的字符串长度小于 1<<5
      unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
      char buf[];
      };
      struct __attribute__ ((__packed__)) sdshdr8 { // 对应的字符串长度小于 1<<8
      uint8_t len;             /* used */ //目前字符创的长度 用1字节存储
      uint8_t alloc;           //已经分配的总长度 用1字节存储
      unsigned char flags;     //flag用3bit来标明类型,类型后续解释,其余5bit目前没有使用
      char buf[];              //柔性数组,以'\0'结尾
      };
      struct __attribute__ ((__packed__)) sdshdr16 { // 对应的字符串长度小于 1<<16
      uint16_t len;             /*已使用长度,用2字节存储*/
      uint16_t alloc;           /* 总长度,用2字节存储*/
      unsigned char flags;      /* 3 lsb of type, 5 unused bits */
      char buf[];
      };
      struct __attribute__ ((__packed__)) sdshdr32 { // 对应的字符串长度小于 1<<32
      uint32_t len;       /*已使用长度,用4字节存储*/
      uint32_t alloc;     /* 总长度,用4字节存储*/
      unsigned char flags;/* 低3位存储类型, 高5位预留 */
      char buf[];         /*柔性数组,存放实际内容*/
      };
      struct __attribute__ ((__packed__)) sdshdr64 { // 对应的字符串长度小于 1<<64
      uint64_t len;        /*已使用长度,用8字节存储*/
      uint64_t alloc;      /* 总长度,用8字节存储*/
      unsigned char flags; /* 低3位存储类型, 高5位预留 */
      char buf[];          /*柔性数组,存放实际内容*/
      };

SDS与C字符串的比较

  • 获取字符串长度:SDS是O(1),C字符串是O(n)

  • 缓冲区溢出

    • 使用C变更字符串时,忘记重新分配内存,很容易造成缓冲区的溢出

    • SDS由于记录了长度,相应的API在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出

  • 修改字符串时内存的重分配

    • C字符串的变更,如果不注意内存的申请和释放,就会造成内存的溢出或者泄漏

    • 而对于SDS,由于可以记录len和free,因此解除了字符串长度和空间数组长度之间的关联

    • 优化手段1:空间预分配策略(即分配内存时比实际需要的多)使得字符串长度增大时重新分配内存的概率大大减小

    • 优化手段2: 惰性空间释放策略使得字符串长度减小时重新分配内存的概率大大减小

  • 存取二进制数据

    • 对于一些二进制文件,比如图片,内容可能包括空字符串,因此C字符串无法正确存取

    • 而SDS以字符串长度len来作为字符串结束标识,因此没有这个问题

  • 此外,由于SDS中的buf仍然使用了C字符串(即以’\0’结尾),因此SDS可以使用C字符串库中的部分函数;但是需要注意的是,只有当SDS用来存储文本数据时才可以这样使用,在存储二进制数据时则不行(’\0’不一定是结尾)

Redis的对象类型与内粗编码

以下内容,有部分我也不是很明白,但是笔记先做在这,等以后功力深厚后再次翻阅

  • 之前我们就有说到:Redis支持5种对象类型,而每种结构都有至少两种编码;

  • Redis各种对象类型支持的内部编码如下图所示(只列出重点的):

字符串:String (SDS)

  • 字符串是最基础的类型,因为所有的键都是字符串类型,且字符串之外的其他几种复杂类型的元素也是字符串

  • 字符串长度不能超过512MB

内部编码

  • 字符串类型的内部编码有3种,它们的应用场景如下

    • int:8个字节的长整型。字符串值是整型时,这个值使用long整型表示

    • embstr:<=44字节的字符串。embstr与raw都使用redisObject和sds保存数据

    • raw:大于44个字节的字符串

    • 【异同】embstr 与 row的区别

      • embstr与raw都使用redisObject和sds保存数据

      • embstr的使用只分配一次内存空间(因此redisObject和sds是连续的)

      • embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便

      • 而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读

      • 而raw需要分配两次内存空间(分别为redisObject和sds分配空间)

列表:list

  • 列表(list)用来存储多个有序的字符串,每个字符串称为元素;

  • Redis中的列表支持两端插入和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队列、栈等

内部编码

  • Redis3.0之前列表的内部编码可以是压缩列表(ziplist)或双端链表(linkedlist)。选择的折中方案是两种数据类型的转换

  • 因为转换也是个费时且复杂的操作 ,在3.2版本之后引入结合了双向列表linkedlist和ziplist的特点,称之为quicklist (快速列表)

  • 所有的节点都用quicklist存储,省去了到临界条件是的格式转换

压缩列表 [3.2之前]

  • 当一个列表只包含少量列表项时,并且每个列表项是小整数值或短字符串,那么Redis会使用压缩列表来做该列表的底层实现

  • 压缩列表(ziplist)是Redis为了节省内存而开发的

  • 是由一系列特殊编码的连续内存块组成的顺序型数据结构

  • 一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值,放到一个连续内存区

双向链表 [3.2之前]

  • 双向链表(linkedlist):由一个list结构和多个listNode结构组成;

  • 典型结构如下所示

    • 双端链表同时保存了表头指针和表尾指针

    • 每个节点都有指向前和指向后的指针 ,链表中保存了列表的长度

    • dup、free和match为节点值设置类型特定函数 ,所以链表可以用于保存各种不同类型的值

    • 链表中每个节点指向的是type为字符串的redisObject

快速列表 [3.2优化]

  • 我们仍旧可以将其看作一个双向列表,但是列表的每个节点都是一个ziplist

  • 其实就是linkedlist和ziplist的结合

  • quicklist中的每个节点ziplist都能够存储多个数据元素

  • Redis3.2开始,列表采用quicklist进行编码。

//32byte 的空间
typedef struct quicklist {
    quicklistNode *head;    // 指向quicklist的头部
    quicklistNode *tail;    // 指向quicklist的尾部
    unsigned long count;    // 列表中所有数据项的个数总和
    unsigned int len;       // quicklist节点的个数,即ziplist的个数
    // ziplist大小限定,由list-max-ziplist-size给定
    // 表示不用整个int存储fill,而是只用了其中的16位来存储
    int fill : 16;
    unsigned int compress : 16;  // 节点压缩深度设置,由list-compress-depth给定
} quicklist;
typedef struct quicklistNode {
    struct quicklistNode *prev; // 指向上一个ziplist节点
    struct quicklistNode *next; // 指向下一个ziplist节点
    unsigned char *zl; // 数据指针,如果没有被压缩,就指向ziplist结构,反之指向quicklistLZF结构
    unsigned int sz; // 表示指向ziplist结构的总长度(内存占用长度)
    unsigned int count : 16; // 表示ziplist中的数据项个数
    unsigned int encoding : 2; // 编码方式,1--ziplist,2--quicklistLZF
    unsigned int container : 2; // 预留字段,存放数据的方式,1--NONE,2--ziplist
    unsigned int recompress : 1; // 解压标记,当查看一个被压缩的数据时,需要暂时解压,标记此参数为1,之后再重新进行压缩
    unsigned int attempted_compress : 1; // 测试相关
    unsigned int extra : 10; // 扩展字段,暂时没用
} quicklistNode;

哈希 (压缩列表和哈希表)

  • 哈希(作为一种数据结构),不仅是redis对外提供的5种对象类型的一种,也是Redis作为Key-Value数据库所使用的数据结构

  • 在这里便于区分,我们将Redis支持的五种数据类型的哈希成为内层的哈希

  • 至于Redis作为Key-Value数据库所使用的数据结构哈希,称之为外层的哈希

内部编码

  • 内层的哈希使用的内部编码可以是压缩列表(ziplist)和哈希表(hashtable)两种

  • Redis的外层的哈希则只使用了哈希表hashtable

  • 压缩列表前面已介绍。与哈希表相比,压缩列表用于元素个数少、元素长度小的场景;其优势在 于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于哈希中 元素数量较少,因此操作的时间并没有明显劣势

  • hashtable:一个hashtable由1个dict结构、2个dictht结构、1个dictEntry指针数组(称为bucket)和多个dictEntry结构组成。

  • dict

    • 通过使用dictht和dictEntry结构,便可以实现普通哈希表的功能

    • 但是Redis的实现中,在dictht结构的上层,还有一个dict结构

    • typedef struct dict{
          dictType *type; // type里面主要记录了一系列的函数,可以说是规定了一系列的接口
          void *privdata; // privdata保存了需要传递给那些类型特定函数的可选参数
          dictht ht[2];   //两张哈希表 便于渐进式rehash
          int trehashidx; //rehash 索引,并没有rehash时,值为 -1
          int iterators;  //目前正在运行的安全迭代器的数量
      } dict;
      • type属性和privdata属性是为了适应不同类型的键值对,用于创建多态字典

      • ht属性和trehashidx属性则用于rehash

        • 当哈希表需要扩展或收缩时使用。ht是一个包含两个项的数组,每项都指向一个dictht结构

        • 这也是Redis的哈希会有1个dict、2个dictht结构的原因

        • 通常情况下,所有的数据都是存在放dict的ht[0]中,ht[1]只在rehash的时候使用

        • dict进行rehash操作的时候,将ht[0]中的所有数据rehash到ht[1]中。然后将ht[1]赋值给ht[0],并清空ht[1]

        • 因此,Redis中的哈希之所以在dictht和dictEntry结构之外还有一个dict结构,一方面是为了适应不同类型的键值对,另一方面是为了rehash

    • dictht

      • typedef struct dictht{
            dictEntry **table;     //哈希表数组,每个元素都是一条链表
            unsigned long size;    //哈希表大小
            unsigned long sizemask;// 哈希表大小掩码,用于计算索引值,总是等于 size - 1
            unsigned long used;    // 该哈希表已有节点的数量
        }dictht;
        • 其中,各个属性的功能说明如下:

          • table属性是一个指针,指向bucket;

          • size属性记录了哈希表的大小,即bucket的大小;

          • used记录了已使用的dictEntry的数量

          • sizemask属性的值总是为size-1,这个属性和哈希值一起决定一个键在table中存储的位置

    • bucket

      • bucket是一个数组,数组的每个元素都是指向dictEntry结构的指针

    • dictEntry

      • dictEntry结构用于保存键值对,结构定义如下:

        • //
          typedef struct dictEntry{
              void *key;
              union{ //值v的类型可以是以下三种类型
                  void *val;
                  uint64_tu64;
                  int64_ts64;
              }v;
              // 指向下个哈希表节点,形成链表
              struct dictEntry *next;
          }dictEntry;
          • key:键值对中的键

          • val:键值对中的值

          • next:指向下一个dictEntry,用于解决哈希冲突问题

编码转换

  • Redis中内层的哈希既可能使用哈希表,也可能使用压缩列表

  • 只有同时满足下面两个条件时,才会使用压缩列表

    • 哈希中元素数量小于512个

    • 哈希中所有键值对的键和值字符串长度都小于64字节

集合:Set(整数集合和哈希表)

  • 集合(set)与列表类似,都是用来保存多个字符串

  • 集合中的元素是无序的,因此不能通过索引来操作元素

  • 集合中的元素不能有重复

一个集合中最多可以存储2^32-1个元素;除了支持常规的增删改查,Redis还支持多个集合取交 集、并集、差集。

内部编码

  • 集合的内部编码可以是整数集合(intset)或哈希表(hashtable)

  • 哈希表前面已经讲过,这里略过不提;需要注意的是,集合在使用哈希表时,值全部被置为null

  • typedef struct intset{
        uint32_t encoding; // 编码方式
        uint32_t length; // 集合包含的元素数量
        int8_t contents[]; // 保存元素的数组
    } intset;
    • encoding代表contents中存储内容的类型,虽然contents(存储集合中的元素)是int8_t类型,

    • 但实际上其存储的值是int16_t、int32_t或int64_t,具体的类型便是由encoding决定的;length表示元素个数。

整数集合适用于集合所有元素都是整数且集合元素数量较小的时候,与哈希表相比,整数集合的 优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于 集合数量较少,因此操作的时间并没有明显劣势

编码转换

  • 只有同时满足下面两个条件时,集合才会使用整数集合,如果有一个条件不满足,则使用哈希表 :

    • 集合中元素数量小于512个

    • 集合中所有元素都是整数值

  • 编码只可能由整数集合转化为哈希表,反方向则不可能

有序集合(亚索列表和跳跃表)

  • 有序集合与集合一样,元素都不能重复

  • 但与集合不同的是,有序集合中的元素是有顺序的

  • 与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据

内部编码

  • 有序集合的内部编码可以是压缩列表(ziplist)或跳跃表(skiplist) .

  • 跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的

  • 除了跳跃表,实现有序数据结构的另一种典型实现是平衡树

  • 大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多

  • 因此redis中选用跳跃表代替平衡树。跳跃表支持平均O(logN)、最坏O(N)的复杂点进行节点查找,并支持顺序操作

  • Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成:

    • 前者用于保存跳跃表信息(如头结点、尾节点、长度等

    • 后者用于表示跳跃表节点

  • 理解跳跃表

    • 我们直接以跳跃表—3层为列,我们要寻找:19

      • 首先跳跃表从上往下依次轮询定位19所在的区间

        • 第一层 -INF <------> 21,19位列其中,得到-INF <------> 21区间

        • 第二层-INF <------> 9未命中,9 <------> 21命中,得到9 <------> 21区间

        • 第三层9 <------> 17未命中,17 <------> 21命中,得到 17<------> 21区间

        • 第四层17 <------> 19命中,19 <------> 21不再轮询,得到19返回

    • 然后就是关于这个跳跃表的一些细节

      • 跳跃表最高为31层

      • 我们的数字在插入的时候,会依照上面的定位区间方法定位该值在有序集合中的位置

      • 然后在0-31区间取随机值,得到的值就会在该高度形成一个层级标明一个区间

编码转换

  • 只有同时满足下面两个条件时,才会使用压缩列表

    • 有序集合中元素数量小于128个

    • 有序集合中所有成员长度都不足64字节

  • 如果有一个条件不满足,则使用跳跃表;且编码只可能由压缩列表转化为跳跃表,反方向则不可能。

[优化] 估算Redis内存使用量

  • 要估算redis中的数据占据的内存大小,需要对redis的内存模型有比较全面的了解

  • 比如:hashtable、sds、redisobject、各种对象类型的编码方式等

  • 下面以最简单的字符串类型来进行说明

假设有90000个键值对,每个key的长度是10个字节,每个value的长度也是10个字节(且key和value都不是整数)

  • 下面来估算这90000个键值对所占用的空间。在估算占据空间之前,首先可以判定字符串类型使用的编码方式:embstr

  • 90000个键值对占据的内存空间主要可以分为两部分

    • 一部分是90000个dictEntry占据的空间

    • 一部分是键值对所需要的bucket空间

  • 每个dictEntry占据的空间包括

    • 一个dictEntry结构,24字节

      • jemalloc会分配32字节的内存块(64位操作系统下,一个指针8字节,一个dictEntry由三个指针组成)

    • 一个key,12字节

      • 所以SDS(key)需要12+4=16 个字节([SDS的长度=4+字符串长度)

      • jemalloc会分配16字节的内存块

    • 一个redisObject,16字节

      • jemalloc会分配16字节的内存块:(4bit+4bit+24bit+4Byte+8Byte=16Byte)

    • 一个value,12字节

      • 所以SDS(value)需要12+4=16个字节([SDS的长度=4+字符串长度)

      • jemalloc会分配16字节的内存块

    • 综上,一个dictEntry所占据的空间需要32+16+16+16=80个字节

  • bucket数组的大小为大于90000的最小的2^n,是131072

  • 每个bucket元素(bucket中存储的都是指针元素)为8字节(因为64位系统中指针大小为8字节)

因此,可以估算出这90000个键值对占据的内存大小为:[9000080 + 1310728 = 8248576

作为对比将key和value的长度由12字节增加到13字节,则对应的SDS变为17个字节,jemalloc会分配32个字节,因此每个dictEntry占用的字节数也由80字节变为112字节。此时估算这90000个键值对占据内存大小为:90000112 + 1310728 = 11128576

[优化] 优化内存占用

利用jemalloc特性进行优化.

  • 上一小节所讲述的90000个键值便是一个例子

  • 由于jemalloc分配内存时数值是不连续的,因此key/value字符串变化一个字节,可能会引起占用内存很大的变动

  • 所以在设计时可以利用这一点:

例如,如果key的长度如果是13个字节,则SDS为17字节,jemalloc分配32字节;此时将key长度缩减为12个字节,则SDS为16字节,jemalloc分配16字节;则每个key所占用的空间都可以缩小一半

使用整形/长整形

  • 如果是整型/长整型,Redis会使用int类型(8字节)存储来代替字符串,可以节省更多空间

  • 因此在可以使用长整型/整型代替字符串的场景下,尽量使用长整型/整型

共享对象

  • 利用共享对象,可以减少对象的创建(同时减少了redisObject的创建),节省内存空间

  • 目前redis中的共享对象只包括10000个整数(0-9999)

  • 可以通过调整REDIS_SHARED_INTEGERS参数提高共享对象的个数

例如将REDIS_SHARED_INTEGERS调整到20000,则0-19999之间的对象都可以共享

论坛网站在redis中存储了每个帖子的浏览数,而这些浏览数绝大多数分布在0-20000之间,这时 候通过适当增大REDIS_SHARED_INTEGERS参数,便可以利用共享对象节省内存空间 。

缩短键值对的储存长度

  • 键值对的长度是和性能成反比的

  • 比如我们看看专业的测试数据:向Redis中写入数据

  • 从以上数据可以看出,在 key 不变的情况下,value 值越大操作效率越慢

  • 因为 Redis 对于同一种数据类型会使用不同的内部编码进行存储

  • 比如字符串的内部编码就有三种:int(整数编码)、raw(优化内存分配的字符串编码)、embstr(动态字符串编码)

  • 数据量越大使用的内部编码就越复杂,而越是复杂的内部编码存储的性能就越低

  • 这还只是写入时的速度,当键值对内容较大时,还会带来另外几个问题

    • 内容越大需要的持久化时间就越长,需要挂起的时间越长,Redis 的性能就会越低;

    • 内容越大在网络上传输的内容就越多,需要的时间就越长,整体的运行速度就越低

    • 内容越大占用的内存就越多,就会更频繁的触发内存淘汰机制,从而给 Redis 带来了更多的运行负担

  • 因此在保证完整语义的同时,我们要尽量的缩短键值对的存储长度,必要时要对数据进行序列化和压缩再存储

[优化] 基于内存淘汰机制的优化

设置键值的过期时间(惰性删除)

  • 我们应该根据实际的业务情况,对键值设置合理的过期时间,这样 Redis 会帮你自动清除过期的键值对

  • 以节约对内存的占用,以避免键值过多的堆积,频繁的触发内存淘汰策略

  • Redis 有四个不同的命令可以用于设置键的生存时间过期时间

    • EXPlRE 命令用于将键key 的生存时间设置为ttl 秒

      • expire key1 20

    • PEXPIRE 命令用于将键key 的生存时间设置为ttl 毫秒

    • EXPIREAT < timestamp> 命令用于将键key 的过期时间设置为timestamp所指定的秒数时间戳

使用lazy free特性

  • lazy free 特性是 Redis 4.0 新增的一个非常使用的功能,它可以理解为惰性删除或延迟删除

  • 意思是在删除的时候提供异步延时释放键值的功能

  • 把键值释放操作放在 BIO(Background I/O) 单独的子线程处理中,以减少删除删除对 Redis 主线程的阻塞

  • 可以有效地避免删除 big key 时带来的性能和可用性问题

  • lazy free 对应了 4 种场景,默认都是关闭的:

    • lazyfree-lazy-eviction no
      lazyfree-lazy-expire no
      lazyfree-lazy-server-del no
      slave-lazy-flush no
    • lazyfree-lazy-eviction

      • 表示当 Redis 运行内存超过最大内存时,是否开启 lazy free 机制删除

    • lazyfree-lazy-expire

      • 表示设置了过期时间的键值,当过期之后是否开启 lazy free 机制删除

    • lazyfree-lazy-server-del

      • 有些指令在处理已存在的键时,会带有一个隐式的 del 键的操作

      • 比如rename 命令,当目标键已存在,Redis 会先删除目标键

      • 如果这些目标键是一个 big key,就会造成阻塞删除的问题,此配置表示在这种场景中是否开启 lazy free 机制删除

    • slave-lazy-flush

      • 针对 slave(从节点) 进行全量数据同步

      • slave 在加载 master 的 RDB 文件前,会运行 flushall 来清理自己的数据

      • 它表示此时是否开启 lazy free 机制删除

建议开启其中的 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-server-del 等配置,这样 就可以有效的提高主线程的执行效率

限制Redis内存大小(核心)

  • 有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置

  • 在 64 位操作系统中 Redis 的内存大小是没有限制的 ,默认物理内存 + 虚拟内存

    • 这样就会导致在物理内存不足时,使用 swap 空间既交换空间

    • 而当操作系统将 Redis所用的内存分页移至 swap 空间时,将会阻塞Redis进程,导致Redis出现延迟,从而影响Redis的整体性能

    • 因此我们需要限制 Redis 的内存大小为一个固定的值

      • 当 Redis 的运行到达此值时会触发内存淘汰策略,内存淘汰策略在 Redis 4.0 之后有 8 种

LRU原理:最近最少使用

  • 该算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”

  • 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下 :

  • 基本思路为:

    • 新数据插入到链表头部;

    • 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;

    • 链表长度固定,当链表满的时候,将链表尾部的数据丢弃。

LFU原理:最不经常使用

  • 在一段时间内,数据被使用频次最少的,优先被淘汰。

  • 采用LFU算法的最简单方法是为每个加载到缓存的块分配一个计数器

  • 每次引用该块时,计数器将增加一。当缓存达到容量并有一个新的内存块等待插入时

  • 系统将搜索计数器最低的块并将其从缓存中删除 《维基百科》

  • 第一次:

    • 链表如是,右侧为其key的访问次数,由高到低排列

  • 第二次

    • key4的键被多次访问,超过了key3

  • 第三次

    • 链表的排序发生了改变,key4排在了key3的前面

  • 第四次

    • 这个时候内存不够了,新增了key6的键值对想要加入到缓存中来

    • 把这个链表中访问量最低的key5给干掉,留出位置给key6

LRU 和 LFU 的异同

  • LRU主要体现在对元素的使用时间上,而LFU主要体现在对元素的使用频次上

  • LFU的缺陷是:

    • 在短期的时间内,对某些缓存的访问频次很高,这些缓存会立刻晋升为热点数据,而保证不会淘汰

    • 瞬时的高频访问将会造成这部分数据的引用暴增,导致在后面长时间不访问时仍占有一席之地

    • 如果这些数据在短时间高频访问后就拉垮了,他的地位会让他在内存中继续持有很长时间,直到他成为垫底数据才会被删除

    • 此时一些新加入的缓存很容易被快速删除,因为它们的引用频率还没有老大哥高就被收割了

Redis的内存淘汰策略

  • redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略

  • maxmemory-policy属性用以配置内存淘汰策略,内存淘汰策略在 Redis 4.0 之后有 8 种

  • noeviction

    • 默认,不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;

  • allkeys-lru

    • 淘汰整个键值中最久未使用的键值

  • allkeys-random

    • 随机淘汰任意键值

  • volatile-lru

    • 淘汰所有设置了过期时间的键值中最久未使用的键值

  • volatile-random

    • 随机淘汰设置了过期时间的任意键值

  • volatile-ttl

    • 优先淘汰更早过期的键值

  • volatile-lfu 【Redis 4.0 后加入】

    • 淘汰所有设置了过期时间的键值中,最少使用的键值

  • allkeys-lfu 【Redis 4.0 后加入】

    • 淘汰整个键值中最少使用的键值

  • 其中 allkeys-xxx 表示从所有的键值中淘汰数据,而 volatile-xxx 表示从设置了过期键的键值中淘汰数据

  • 默认的淘汰策略不淘汰任何数据,在新增时会报错

.

 

 

posted @ 2020-07-22 00:25  鞋破露脚尖儿  阅读(313)  评论(0编辑  收藏  举报