redis常用数据结构解析

Redis底层数据结构 :

目录

字符串SDS 

双端链表 

字典

跳跃表

整数集合

压缩列表

对象

字符串对象

列表对象

哈希对象

集合对象

有序集合对象

类型检查与命令多态

内存回收

对象共享

对象的空转时常

对象和底层数据结构关系如图

Redis数据库

过期策略

Redis持久化


字符串SDS 

struct sdshdr {
    // 记录buf数组中已使用的字节的数量
    // 等于SDS所保存字符串的长度
    int len;
    // 记录buf数组中未使用的字节的数量
    int free;
    // 字节数组,用于保存字符串
    char[] buf;
};

为什么不直接使用C中的字符串?

  1. 如果需要计算字符串长度,C中的字符串需要遍历字节数组,而SDS直接通过len字段得到。
  2. 更方便检测缓冲区是否溢出(如果要追加字符串, 可以直接通过free字段知道字节数组中是否有足够多的内存可用,而C的字符串如果忘记检查或者分配,则会导致缓冲区溢出)
  3. 减少修改字符串时带来的内存重新分配次数(内存分配比较复杂耗时,可能需要系统调用),具体策略如下:
    1. 空间预分配:用于优化SDS的字符串增长操作,比如当前SDS的len为5,free为5,如果要写入的字符串长度变成13, 则会扩展buf, len变为13,存储本次修改后的字符串, free也为13, 作为可供下次使用的内存。下次字符串长度变成20的时候, 就不需要再重新分配内存, sds的len变为20, free 变为6。分配公式 : 如果修改后 len 的长度小于1MB, 则free的长度等于len的长度。如果修改后的len的长度大于等于1MB,则free的长度等于1MB。
    2. 惰性释放:用于优化SDS的字符串缩短操作 ,当SDS的buf中存储的字符串缩短之后, 并不会立即释放内存,而是仅仅修改len和free的值,free记录这些被释放的空间等待将来增长的时候使用。 例如当前SDS中len为13, free为13, 修改后len为5,此时free则为21,不会触发内存释放。如果我们想真正释放这部分内存,修改buf的大小, redis有提供api。
  4. 二进制安全,C中字符串必须符合某种编码,并且除了字符串的末尾之外,字符串的中间不能包含空字符,否则最先被读入的空字符会被误认为是字符串的结尾符。也就导致C字符串不能保存图片、音视频等二进制数据。而SDS在读取buf中数据的时候则明确根据len判断是否结束,所以是二进制安全的。
  5. 兼容部分C字符串函数。虽然SDS都是二进制安全的,但是他仍然遵循C字符串中以空格字符结尾的惯例,所以部分C字符串中的函数还是可用用。

双端链表 

最基础的双向链表结构如下 :

typedef struct listNode {
    // 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    // 节点的值
    void *value;
} listNode;

多个 listNode 就可以通过prev和next指针组成双向链表

但是如下结构的链表在使用的时候更加方便(类似SDS的设计,提供了一些常用的东西)

typedef struct list {
    // 表头节点
    listNode *head;
    // 表尾节点
    listNode *tail;
    // 链表所包含的节点数量
    unsigned long len;
    // 节点值复制函数
    void *(*dup)(void *ptr);
    // 节点值释放函数
    void *(*free)(void *ptr);
    // 节点值对比函数
    int (*match)(void *ptr, void *key)
} list;

dup、free、match成员用于实现多态链表所需的类型特定函数。

Redis使用如上 list 实现链表的特性如下 :

  1. 双端:链表带 prev指针 和 next 指针,获取上一个节点和下一个节点中数据复杂的为O(1)。
  2. 无环:表头节点的prev和表尾节点的next都指向NULL,对链表的访问以NULL为终点。
  3. 带表头指针和表尾指针:通过他们获取链表的表头和表尾节点复杂的为O(1)。
  4. 带链表长度计算器:通过len属性记录节点数量。
  5. 多态。

字典

Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。

哈希表节点结构 :

typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union{
        void *val;
        uint_tu64;
        int64_ts64;
    } v;
    // 指向下个哈希表节点, 形成链表
    struct dictEntry *next;
} dictEntry;

Key属性保存着键值对中的键,而v属性保持着键值对中的值,值可以是一个指针,或者一个uint64_t整数或int64_t整数。

Next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,以次来解决哈希冲突。

哈希表结构 :

typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小(table数组大小)
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;
    // 哈希表已有节点的数量
    unsigned long used;
} dictht;

dictType :

typedef struct dictType {
    // 计算哈希值的函数
    unsigned int (*hashFunction) (const void *key);
    // 复制键的函数
    void *(*keyDup) (void *privdata, const void *key);
    // 复制值的函数
    void *(*valDup) (void *privdata, const void *obj);
    // 对比键的函数
    int (*keyCompare) (void *privdata, const void *key1, const void *key2);
    // 销毁键的函数
    void (*keyDestructor) (void *privdata, void *key);
    // 销毁值的函数
    void (*valDestructor) (void *privdata, void *obj);
} dictType;

其中主要保存了计算哈希的函数(这个是哈希最重要的东西,决定了键与值的对应关系)

字典:

typedef struct dict{
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    // 当 rehash 不在进行时, 值为 -1
    int rehashidx;
} dict;

type属性是一个指向 dictType 结构的指针,每个dictType保存了一簇用于操作特定类型键值对的函数,redis根据用途不同的字典设置不同的函数。

privdata属性则保存了需要传递给那些类型特定函数的可选参数。

ht属性是一个包含两个项的数组,数组中的每一个项都是一个dictType哈希表,一般情况下字典只使用ht[0],只有在对ht[0]使用 rehash 的时候才会用到ht[1](将ht[0]中的数据移动到ht[1]中)。

rehashidx属性记录了rehash目前的进度,没有解析rehash的时候为-1。

如上,ht[0]的table中实际存储的是一个链表数组,通过type属性中的哈希函数决定数组的大小以及成员的位置, 使用链表是为了通过拉链法解决哈希冲突。

字典的rehash :

如上结构,dict.ht[0].table的容量如果一直不变, 随着存入的数据越来越多, 每个链表也就越来越长,那么在查询的时候, 需要遍历链表的次数也就越多,性能越差。

所以在达到某条件的时候,就需要扩展dict.ht[0].table的容量减少链表的长度来提升查询性能。但是原先存储的数据又不能丢失, 所以才需要ht[1],先计算出容量, 为ht[1]创建好数组, 再将ht[0]中的dictEntry移动到ht[1]中。这个移动的过程可以是渐进式的,如果数据量非常大(上百万),触发rehash的时候全部移动过去会非常耗时。所以一般是哪个dictEntry被访问了就把哪个移过去,新增dictEntry的时候直接新增到ht[1]中。等到ht[0]中成员全部移动到ht[1]中时, 再删除ht[0], 修改ht[1]的下标为ht[0], 然后重新创建一个table为NULL的ht[1]。

跳跃表

跳跃表的概念: https://blog.csdn.net/xp178171640/article/details/102977210

想象一下,如果我们有一个东西需要维护他的顺序, 我们使用什么数据结构最简单。

既然是线性的, 那么首先想到的肯定是用数组或者链表。

情况一,用数组:查询的时候使用二分法可以很快的查到我们需要的数据,但是我如果想在中间某个位置插入一条数据呢?是不是他后面所有数据都要往后移一位?

情况二,用链表:我们在往中间位置插入数据的时候,直接修改相邻节点的指向就行,但是怎么找到要插入的位置呢?只能从头开始遍历。

上面两种方式结构简单,使用的时候各有优缺点。 也可以用树,但是树的数据结构相对复杂。 有没有一种什么办法既可以使用类似二分法的方式去查找数据, 又可以使用类似修改相邻节点指向的方式去插入数据呢?答案就是跳跃表。

如上, 它既然要实现 修改相邻节点指向的方式去插入数据功能, 那么他一定是一个链表。

而他实现类似二分查找的方式类似kafka的稀疏索引, 除了原始链表之外, 还有多条比原始链表短的索引链表,越上层的索引链表越短,间隔越大。且上层链表中的节点值一定能在下层链表中找到。

他的节点结构体类似下面这样 :

typedef struct SkipListNode {
    // 指向下一个节点的指针
    SkipListNode *next;
    // 指向上层索引中节点的指针
    SkipListNode *upper;
    // 节点中存储的值
    void *value;
}

比如现在要查找值为10的节点:

先在第二级索引找到值为7的索引, 然后向下找到第一级索引中值为7的索引,再找到第一级索引中值为9的节点, 然后向下找到原始链表中值为9的节点, 再通过他的 next 找到值为10的节点, 用了5步,线路如下 :

Redis中跳跃表的实现:

跳跃表节点:

typedef struct zskiplistNode {
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
    // 后退指针
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成员对象
    robj *obj;
} zskiplistNode;

层:跳跃表节点的level数组可以包含多个元素, 每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度, 一般来说层的数量越多,访问其他节点的速度越块。每次创建一个新跳跃表节点时,程序都根据幂次定律随机生成一个介于1和32之间的值作为level数组的大小,也就是层高。

前进指针:每个层都有一个指向表尾方向的指针,用于从表头向表尾方向访问节点。下图表示了程序从表头向表尾访问,遍历跳跃表中所有节点的路径:

  1. 迭代程序首先访问跳跃表的第一个节点(表头), 然后从第四层的前进指针移动到表中的第二个节点。
  2. 在第二个节点时,程序沿着第二层的前进指针移动到表中的第三个节点。
  3. 在第三个节点的时候, 程序同样沿着第二层的前进指针移动到第四个节点。
  4. 当程序再次沿着第四个节点的前进指针移动时,他碰到一个NULL,表示这时已经到达了跳跃表的表尾,于是结束这次遍历。

跨度:层的跨度用于记录相同层两个节点之间的距离,跨度越大,距离越远。指向NULL的前进节点跨度为0。跨度与遍历操作无关,是用来计算排位的:在查找某个节点的过程中,将沿途访问过的所有层的跨度累计,得到的结果就是目标节点在跳跃表中的排位。

后退指针:用于从表尾向表头方向访问节点,每次只能后退至前一个节点。

分值和成员:分值是一个 double 类型的浮点数,跳跃表所有节点都按分值从小到大排列。成员对象是一个指针,指向一个字符串对象(SDS),也就是有序集合中的value。

同一个跳跃表中,各节点保存的成员对象必须是唯一的,但是分值却可以相同,分值相同则按成员对象在字典中顺序进行排序。

跳跃表:

typedef struct zskiplist {
    // 表头节点和表尾节点
    structz skiplistNode *header, *tail;
    // 表中节点数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
} zskiplist;

仅靠多个跳跃表节点就可以组成跳跃表,但通过如上结构体能更方便的获取跳跃表的消息,操作跳跃表。

跳跃表总结:

对比我们之前写的最简单的跳跃表, redis的跳跃表并非维持多条链表, 也不是简单的链表数组,而是由多个跳跃表节点组成的一条链表。

而他的 跳跃 完全依赖于每个节点中的层 和跨度。

比如图5-9要访问分值为2.0的成员,流程如下:

  1. 找到表头中跨度非0的最高层L5中跨度为3,指向o3,发现 o3值为3.0,大于2.0。
  2. 表头节点向下 L4 跨度为1,指向o1节点,发现o1节点的分值为1.0。
  3. o1节点L4跨度为2,指向o3节点,发现 o3值为3.0,大于2.0。
  4. o1节点向下L3跨度为2,指向o3节点,发现 o3值为3.0,大于2.0。
  5. o1节点再向下L2跨度为1,指向o2节点,发现o2节点的值为2.0, 等于2.0,本次查找结束。

查找的总跨度为 1 + 1 = 2。 所以要找的节点为第二各节点。

整数集合

typedef struct intset {
    // 编码方式
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组
    int8_t contents[];
} intset;

【本质是一个数组】,当集合中只包含整数集元素, 且元素数量不多时(因为每次插入、删除、修改数据的时候都会移动数组中很多元素), redis使用整数集合作为集合的实现方式。

Contents 数组是整数集合的底层实现,集合中所有成员【按值从小到大排列在该数组中】,且数组中【不包含重复项】。

Length记录了集合中元素个数(也就是contents数组的长度)。

Encoding则记录了 contents 数组中元素的编码方式(也可以理解为元素类型),虽然声明 contents 数组的类型为 int8_t , 但实际上并不保存 int8_t 类型的数据。Encoding 类型主要有 int16_t类型、int32_t类型、int64_t类型分布如下。

9223372036854775808 < int64_t < -2147483648 < int32_t < -32768 < int16_t < 32767 < int32_t < 2147483647 < int64_t < 9223372036854775807

整数集合的升级 :

当我们往整数集合添加成员且成员的长度比现有成员都要长的时候, 整个整数集合就需要升级,升级流程如下 :

  1. 根据新成员类型扩展底层数组的大小,重新分配内存。
  2. 将原有所有成员类型全部转换成跟新成员一样的类型,并调整成员位置。
  3. 将新成员添加到底层数组里面。
  4. 修改encoding编码为新的成员编码。

使用升级策略的好处有提升灵活性(所有成员类型保持一致好操作)和节约内存(如果一开始就使用最大编码,则如果成员都没有那么长就会浪费内存)

整数集合不可以进行降级。

压缩列表

【本质是一个数组】,一个压缩列表可以包含任意多个节点, 每个节点可以保存一个字节数组或者一个整数值。

每个节点大概由一下三部分组成:

previous_entry_length,以字节为单位,记录了压缩列表中前一个节点的长度。previous_entry_length属性长度可以是一字节或者5字节,取决于前一个节点的长度,且长度可能随着前一个节点长度而变化。用于根据当前节点计算前一个节点的起始位置,实现压缩列表从表尾向表头遍历。

encoding,记录content属性所保存数据的类型和长度。

content,负责保存节点的值,节点可以是一个字节数组或者整数。

列表整体组成如下表:

连锁跟新:往压缩列表插入或者删除数据的时候,导致下一个节点中存储的previous_entry_length由1字节变为5字节,进而引发后面节点的 previous_entry_length 跟着变,也就是连锁跟新。

压缩列表中记录了节点数量等信息, 每个节点都是特殊类型的成员, 他本质上还是相当于一个数组。

对这些底层数据结构的观察发现, 他们的核心思想都是以内存换时间, 比如 SDS 、链表等结构中以len字段记录当前长度以在需要获取长度的时候避免遍历。

这种思想其实跟我们升级系统的时候采用 “缓存” 的思想类似。

对象

以上内容总结了 字符串SDS、双端链表list、字典dictht、跳跃表zskiplist、整数集合intset、压缩列表ziplist 六种数据结构。

但redis 并不直接使用这些数据结构来存储数据,而是基于他们再进行封装构建了一个 对象系统。这样我们同一种对象在不同的使用场景下可以使用不同的底层数据结构。并且可以在对象上实现一些公共的方法, 比如引用计数的内存回收和共享、访问时间记录等。

对象系统包括 : 字符串对象、列表对象、集合对象、哈希对象、有序集合对象。

对象结构如下:

typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    void *ptr;
    // ...
} robj;

类型 :对象的类型记录了对象的属性, 这个属性的值如表:

Redis中保存的键值对, 键总是字符串对象, 值可能是表中任意一种。type 命令返回值的对象类型。

编码和底层实现:对象的 ptr 指针指向对象的底层属性数据结构,而这些数据结构的 类型由encoding 属性决定。

使用 object encoding 命令可以查看一个键的值的编码类型(encoding值)

对象结构主要包含三个部分 : 对象本身类型、 底层数据结构类型、 指向底层数据结构的指针。通过这种方式将对外提供的数据结构和底层数据结构解耦,提高灵活度。

字符串对象

字符串对象的编码可以是 int(存储的较小数字) 、 raw(长字符串或者大数字) 、 embstr(存储的较短字符串) 。小数在存储的时候当作字符串处理。

如果是 int 类型, 则数据直接保存在 ptr属性 里面。

如果是raw类型, 数据单独用SDS字符串保存,ptr指针指向数据对应地址。

如果是 embstr 类型, 数据与obj的内存是连在一起的(减少内存分配、释放次数),也用SDS字符串保存。

如果当前对象的类型、长度等发生改变时候, 会发生 编码转换, 选择一种最适合当前值的类型。 embstr类型不可修改, 每次跟新 embstr类型数据的时候, 都是先转 raw 再转回去。

列表对象

列表对象的编码可以是 ziplist(压缩列表)或者linkedlist(双端链表)。

ziplist类型结构如下 :

Linkedlist类型结构如下:

Linkedlist编码的列表对象在底层的 双端列表结构中包含了多个字符串对象(用来存储值),字符串对象是redis五种类型对象中唯一一种会被其他四种嵌套的对象。

哈希对象

哈希对象的编码可以是 ziplist(压缩列表) 或者 hashtable(字典)。

使用压缩列表实现的哈希对象存储的数据是有序的。先加入的键值对存储在靠头的地方。

使用hashtable实现的哈希对象中每个键值对都使用一个字典键值对来保存。

当哈希对象保存的所有键值对的键和值长度都小于64字节且键值对数量小于512个时使用ziplist编码, 否则使用 hashtable编码。

集合对象

几个对象的编码可以是 intset(整数集合) 或者 hashtable(字典)。

使用hashtable编码的集合对象字典的每个键都是一个集合对象,字典的值则全部被设置为NULL。

当集合对象保存的元素都是整数值且元素数量不超过512个是才使用 intset 编码(最大数量可以在配置文件中修改)。

有序集合对象

有序集合对象的编码可以是 ziplist(压缩列表) 或者 skiplist(跳跃表)。

压缩列表中集合元素按分值从小到大排序。

跳跃表中的节点按分值大小有序连接,每个跳跃表节点保存一个集合元素。而集合中的字典为有序集合创建了一个从成员到分值的映射,键保存成员,值保存分值,用于最快的查找某成员的分值。跳跃表和字典都是通过指针来共享相同元素的成员和分值,所以不会浪费额外的空间。

当有序集合元素数量小于128个且所有成员长度都小于64字节时使用 ziplist 编码(以上两个参数都可以在配置文件中修改)。

类型检查与命令多态

Redis中用于操作键的命令基本上可分为两类:可以对任何类型的键执行和只能对特定类型的键执行。

如果命令只能对特定类型的键执行, 则在执行命令的时候需要进行类型检测。类型检测通过 rediobject 结构体中的 type 属性实现。多态命令主要是根据值对象的类型来决定redis对象的编码方式。

内存回收

Redis在对象系统中构建了引用计数技术实现的内存回收机制,通过这一机制, 程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。

引用计数信息由 redisObject 结构的 refcount 属性记录。

typedef struct redisObject {
    // ...
    // 引用计数
    int refcount;
    // ...
} robj;

对象的引用计数信息会随着对象的使用状态不断变化:

  1. 创建一个新对象时, 引用计数的值被初始化为1.
  2. 对象被一个新程序使用时,他的引用计数会被加1.
  3. 对象不再被一个程序使用时,他的引用计数会被减1.
  4. 对象的引用计数变为0时,所占内存会被释放(立马释放还是被扫描到了再释放?)

对象共享

引用计数属性除了实现内存回收时被用到,还可被用来实现对象共享(值对象)。可以通过 object refcount key命令查看键对应的值对象的引用计数。

例如 键A创建了一个包含整数值100的字符串对象作为值对象,次时键B也要创建一个包含整数值100的字符串对象作为值对象,那么服务器有一下两种选择:

  1. 、为键B新建一个包含整数值100的字符串对象。
  2. 、让键A和键B共享同一个字符串对象(用B的ptr指针也指向整数值100的字符串对象)。

明显第二种要更节约内存。实现方法二需要一下两步:

  1. 、将数据库键的值指针指向一个现有的值对象。
  2. 将被共享的值对象的引用计数增1.

对象的空转时常

Lru属性记录对象最后一次被命令程序访问的时间,可通过 object idletime key 命令获取。

typedef struct redisObject {
    // ...
    // 最后一次被命令程序访问的时间
    unsigned lru:22;
    // ...
} robj;

如果服务器开启了 maxmemory 选项,且服务器用于内存回收的算法为 volatile-lru 或 allkeys-lru,那么服务器占用内存超过 maxmemory 时,空转时常较高的那部分键会优先被服务器释放从而回收内存(即使没过期)。

对象和底层数据结构关系如图

Redis数据库

Redis服务端结构体如下:

struct redisServer {
    // ...
    // 一个数组,保存着服务器中的所有数据库
    redisDb *db;
    // 服务器的数据库数量
    int dbnum;
    // ...
};

初始化时根据 dbnum 属性决定创建多少个数据库。

Redis客户端结构体如下:

typedef struct redisClient {
    // ...
    // 记录客户端当前正在使用的数据库
    redisDb *db;
    // ...
} redisClient;

*db指向redisServer数组中当前被 select选中的数据库。

数据库的结构如下:

typedef struct redisDb {
    // ...
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;
    // ...
} redisDb;

键空间的键也就是数据库的键,每个键都是一个字符串对象。

键空间的值也就是数据库的值,每个值都可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象中的任意一种redis对象。

存储数据的redis结构大概如下:

对键的增删改查其实就是往键空间里面增删改查对象。往 redisDb.dict 里面加一个新的 dict, dict的键是一个字符串(新加入的key),dict的值是任意一种redis对象。

读写键空间的维护操作 :

当使用redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作,包括:

  1. 在读取一个键之后(读操作和写操作都要对键进行读写),服务器会根据键是否存在来跟新服务器键空间命中(hit)次数或者键空间不命中(miss)次数。这两个值可在 info status 命令的 keyspace_hits属性和 keyspace_misses 属性中查看。
  2. 读取一个键后, 会更新 lru 时间。Object idletime <key> 命令可查看。
  3. 读取一个键时发现键过期, 会先删除这个过期键,然后才执行余下的操作。
  4. 若客户端有使用 watch 命令监视了某个键,服务器改了被监视的键之后,会标记这个键为“脏”。
  5. 。。。

过期策略

Redis数据库中有一个专门的过期字典保存键的过期时间。

typedef struct redisDb {
    // ...
    // 数据库键过期时间空间,保存着数据库中的所有键的过期时间
    dict *expires;
    // ...
} redisDb;

过期键删除策略 :

  1. 定时删除:设置键过期时间的同时创建一个定时器,定时器在键到达过期时间时立即执行对键的删除操作。
  2. 惰性删除:放任键过期不管,等每次从键空间获取键时检查到键过期再删除。
  3. 定期删除:每隔一段时间程序就对数据库进行检测,删除里面的过期键。删除多少过期键、检测多少数据库则由算法决定(随机检测expires中一批键)。

Redis实际上采用的是惰性删除和定期删除两种策略相配合,在cpu时间使用率和内存空间浪费里面取得一个平衡。

Redis持久化

Redis是一个内存型数据库, 说白了就是程序维护了一个结构体,一旦程序重启,那么原来存储在内存中的结构体就会消失,导致数据丢失。

解决这个问题的办法由两种:将内存中数据直接写入到磁盘或者将创建这些数据的命令写入磁盘。等重启的时候直接读取数据或者执行命令来恢复数据。

Rdb策略就是将某个时间点上内存中存储的数据写入到一个经过压缩的二进制文件中,他可以设置成定时写入, 也可以手动执行(可以认为就是一个命令)。

Rdb策略相当于一个 “快照”,将某个时刻内存中的数据写入到磁盘。等重启时再通过这个“快照”恢复数据(恢复速度比较快)。Rdb策略的优点在于恢复数据块,缺点在于redis程序挂掉时可能会有一部分数据没来得及同步到磁盘中,导致数据不准确。

Aof策略则是每次执行会对数据产生改变的命令时, 将命令都记录到aof文件, 下次启动的时候再去执行这个aof文件对数据进行恢复(类似mysql数据迁移中将表结构和数据都导出为sql文件)。Aof策略会定期去重写aof文件(可能对一个数据进行多次改动,但是我们只需要保存最后一次改动的数据就行)。重写的过程与旧的aof文件无关,而是根据内存中当前存储的数据生成新的aof文件, 然后再替换旧的aof文件。Afo策略的优点是数据不会丢失, 缺点是通过执行命令来恢复数据比较慢。

这两种策略的执行都是通过子进程完成的。实际中我们一般将这两种策略混合使用:定时产生rdb快照, 对不在快照中的新数据则写入到aof文件中,重启的时候先执行rdb,再执行aof。

对于redis内存不足的处理:

Redis配置文件中的 maxmemory 参数可以控制redis使用的最大内存(字节)。

当使用的内存超过阈值,则用到配置文件中的maxmemory-policy,其默认值是noeviction(导致无法写入)。

可配置的选项和结果如下:

LRU算法是指最近最少使用算法, redis中随机按上面的配置抽取三个键,删除这三个键中最近最少使用的键,具体抽样大小可以通过maxmeory-samples设置。

也可以通过集群进行扩容,集群有三种方式 : 客户端分片, 代理分片, RedisCluster。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值