Redis底层数据结构

  • 时间:
  • 浏览:0
  • 来源:跟我学网络

Redis底层数据结构类型

简单动态字符串(simple dynamic string)SDS

Redis 没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string SDS)的抽象类型,并将SDS用作Redis 的默认字符串表示;

除了用来保存字符串以外,SDS还被用作缓冲区(buffer)AOF(持久化)模块中的AOF缓冲区

SDS的定义

区别于C语言字符串,具有良好的伸缩性,在获取字符串长度,字符串修改,防止缓存区溢出等性能都比C语言字符串好

Redis 中定义动态字符串的结构:

/*  
 * 保存字符串对象的结构  
 */  
struct sdshdr {  

    // buf 中已占用空间的长度  
    int len;  

    // buf 中剩余可用空间的长度(初次分配空间,一般没有空余,在对字符串修改的时候,会有剩余空间出现)-预分配  
    int free;  

    // 数据空间  
    char buf[];  
};

SDS 与 C 字符串的区别

  • 获取字符串长度(SDS O(1)/C 字符串 O(n)

    传统的C字符串: 使用长度为N+1 的字符串数组来表示长度为N 的字符串,所以为了获取一个长度为C字符串的长度,必须遍历整个字符串。

    SDS:SDS 的数据结构中,有专门用于保存字符串长度的变量,我们可以通过获取len 属性的值,直接知道字符串长度

杜绝缓冲区溢出

C 字符串 不记录字符串长度,除了获取的时候复杂度高以外,还容易忘了为字符串重新分配足够的空间,从而导致缓冲区溢出

Redis 中SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性:

当我们需要对一个SDS进行修改的时候,redis会在执行拼接操作之前,预先检查给定SDS空间是否足够,如果不够,会先拓展SDS 的空间,然后再执行拼接操作

减少修改字符串时带来的内存重分配次数

C语言字符串在进行字符串的扩充和收缩的时候,都会面临着内存空间的重新分配问题
  1. 字符串拼接会产生字符串的内存空间的扩充,在拼接的过程中,原来的字符串的大小很可能小于拼接后的字符串的大小,那么这样的话,就会导致一旦忘记申请分配空间,就会导致内存的溢出。
  2. 字符串在进行收缩的时候,内存空间会相应的收缩,而如果在进行字符串的切割的时候,没有对内存的空间进行一个重新分配,那么这部分多出来的空间就成为了内存泄露

SDS:对SDS进行拓展,则需要进行空间的拓展,这时候redis 会将SDS的长度修改为N字节,并且将未使用空间同样修改为N字节,此时如果再次进行修改,因为在上一次修改字符串的时候已经拓展了空间,再次进行修改字符串的时候如果发现空间足够使用,因此无须进行空间拓展

通过这种预分配策略,SDS将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次

惰性空间释放

SDS 提供了相应的API,让我们可以在有需要的时候,自行释放SDS的空余空间

通过惰性空间释放,SDS避免了缩短字符串时所需的内存重分配操作,并未将来可能有的增长操作提供了优化

二进制安全



  • C 字符串中的字符必须符合某种编码,并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存想图片,音频,视频,压缩文件这样的二进制数据


  • 在Redis中,不是靠空字符来判断字符串的结束的,而是通过len这个属性。那么,即便是中间出现了空字符对于SDS来说,读取该字符仍然是可以的

兼容部分C字符串函数

虽然SDS 的API 都是二进制安全的,但他们一样遵循C字符串以空字符串结尾的惯例

对比总结

C 字符串 | SDS

:-:|:-:

获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1)

API 是不安全的,可能会造成缓冲区溢出 | API 是安全的,不会造成缓冲区溢出

修改字符串长度N次必然需要执行N次内存重分配 | 修改字符串长度N次最多执行N次内存重分配

只能保存文本数据 | 可以保存二进制数据和文本文数据

可以使用所有库中的函数 | 可以使用一部分库中的函数

链表

顺序存储对象信息,有用于缓存链表长度的属性,在插入删除对象功能中有良好性能,避免环的产生

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。链表在Redis 中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis 就会使用链表作为列表键的底层实现。

链表的数据结构

每个链表节点使用一个 listNode结构表示(adlist.h/listNode):

typedef struct listNode{
      struct listNode *prev;
      struct listNode * next;
      void * value;  
}

多个链表节点组成的双端链表:

</svg>" width="844">

通过直接操作list 来操作链表会更加方便:

typedef struct list{
    //表头节点
    listNode  * head;
    //表尾节点
    listNode  * tail;
    //链表长度
    unsigned long len;
    //节点值复制函数
    void *(*dup) (void *ptr);
    //节点值释放函数
    void (*free) (void *ptr);
    //节点值对比函数
    int (*match)(void *ptr, void *key);
}
  • dup 函数用于复制链表节点所保存的值;
  • free 函数用于释放链表节点所保存的值;
  • match 函数则用于对比链表节点所保存的值和另一个输入值是否相等。

    list 组成的结构图:
</svg>" width="647">

特性

  • 双端:链表节点带有prev 和next 指针,获取某个节点的前置节点和后置节点的时间复杂度都是O(N)
  • 无环:表头节点的 prev 指针和表尾节点的next 都指向NULL,对立案表的访问时以NULL为截止
  • 表头和表尾:因为链表带有head指针和tail 指针,程序获取链表头结点和尾节点的时间复杂度为O(1)
  • 长度计数器:链表中存有记录链表长度的属性 len
  • 多态:链表节点使用 void* 指针来保存节点值,并且可以通过list 结构的dup 、 free、 match三个属性为节点值设置类型特定函数。

字典

key-value 存储方式,通过hash值计算,判断key的存储,当容量过大,会通过rehash重新分配字典大小

字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对的抽象数据结构。

在字典中,一个键(key)可以和一个值(value)进行关联,字典中的每个键都是独一无二的。在C语言中,并没有这种数据结构,但是Redis 中构建了自己的字典实现

字典的定义

  1. 哈希表

    Redis 字典所使用的哈希表由 dict.h/dictht 结构定义:
typedef struct dictht {
   //哈希表数组
   dictEntry **table;
   //哈希表大小
   unsigned long size;

   //哈希表大小掩码,用于计算索引值
   unsigned long sizemask;
   //该哈希表已有节点的数量
   unsigned long used;
}

一个空的字典的结构图如下:

</svg>" width="385">



可以看到,在结构中存有指向dictEntry 数组的指针,而我们用来存储数据的空间既是dictEntry

  1. 哈希表节点( dictEntry )

    dictEntry 结构定义:
typeof struct dictEntry{
   //键
   void *key;
   //值
   union{
      void *val;
      uint64_tu64;
      int64_ts64;
   }
   struct dictEntry *next;

}

在数据结构中,我们清楚key 是唯一的,但是我们存入里面的key 并不是直接的字符串,而是一个hash 值,通过hash 算法,将字符串转换成对应的hash 值,然后在dictEntry 中找到对应的位置。

这时候我们会发现一个问题,如果出现hash 值相同的情况怎么办?Redis 采用了链地址法:

</svg>" width="755">



当k1 和k0 的hash 值相同时,将k1中的next 指向k0 想成一个链表。

  1. 字典
typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privedata;
    // 哈希表
    dictht  ht[2];
    // rehash 索引
    in trehashidx;

}

type 属性 和privdata 属性是针对不同类型的键值对,为创建多态字典而设置的。

ht 属性是一个包含两个项(两个哈希表)的数组

普通状态下的字典:

</svg>" width="724">

解决哈希冲突

在上述分析哈希节点的时候我们有讲到:在插入一条新的数据时,会进行哈希值的计算,如果出现了hash值相同的情况,Redis 中采用了连地址法(separate chaining)来解决键冲突。每个哈希表节点都有一个next 指针,多个哈希表节点可以使用next 构成一个单向链表,被分配到同一个索引上的多个节点可以使用这个单向链表连接起来解决hash值冲突的问题。

举个栗子:

现在哈希表中有以下的数据:k0 和k1

</svg>" width="598">

我们现在要插入k2,通过hash 算法计算到k2 的hash 值为2,即我们需要将k2 插入到dictEntry[2]中:

</svg>" width="662">



在插入后我们可以看到,dictEntry指向了k2,k2的next指向了k1,从而完成了一次插入操作(这里选择表头插入是因为哈希表节点中没有记录链表尾节点位置)

Rehash

随着对哈希表的不断操作,哈希表保存的键值对会逐渐的发生改变,为了让哈希表的负载因子维持在一个合理的范围之内,我们需要对哈希表的大小进行相应的扩展或者压缩,这时候,我们可以通过 rehash(重新散列)操作来完成
  1. 目前的哈希表状态

    我们可以看到,哈希表中的每个节点都已经使用到了,这时候我们需要对哈希表进行拓展

</svg>" width="783">
  1. 为哈希表分配空间

    哈希表空间分配规则:

    如果执行的是拓展操作,那么ht[1] 的大小为第一个大于等于ht[0] 的2的n次幂

    如果执行的是收缩操作,那么ht[1] 的大小为第一个大于等于ht[0] 的2的n次幂

    因此这里我们为ht[1] 分配 空间为8,

</svg>" width="815">
  1. 数据转移

    将ht[0]中的数据转移到ht[1]中,在转移的过程中,需要对哈希表节点的数据重新进行哈希值计算

    数据转移后的结果:

</svg>" width="719">
  1. 释放ht[0]

    将ht[0]释放,然后将ht[1]设置成ht[0],最后为ht[1]分配一个空白哈希表:

</svg>" width="681">
  1. 渐进式 rehash

    上面我们说到,在进行拓展或者压缩的时候,可以直接将所有的键值对rehash 到ht[1]中,这是因为数据量比较小。在实际开发过程中,这个rehash 操作并不是一次性、集中式完成的,而是分多次、渐进式地完成的。

    渐进式rehash 的详细步骤:
1、为ht[1] 分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
2、在几点钟维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash 开始
3、在rehash 进行期间,每次对字典执行CRUD操作时,程序除了执行指定的操作以外,
还会将ht[0]中的数据rehash 到ht[1]表中,并且将rehashidx加一
4、当ht[0]中所有数据转移到ht[1]中时,将rehashidx 设置成-1,表示rehash 结束

采用渐进式rehash 的好处在于它采取分而治之的方式,避免了集中式rehash 带来的庞大计算量

哈希表的扩展与收缩

当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作

服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;

其中哈希表的负载因子可以通过公式:

# 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size

跳跃表

概述

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表是一种随机化的数据,跳跃表以有序的方式在层次化的链表中保存元素,效率和平衡树媲美——查找、删除、添加等操作都可以在对数期望时间下完成,并且比起平衡树来说,跳跃表的实现要简单直观得多。

Redis 只在两个地方用到了跳跃表,一个是实现有序集合键,另外一个是在集群节点中用作内部数据结构

跳跃表的定义

跳跃表的完整结构:

</svg>" width="567">

Redis 的跳跃表 主要由两部分组成:zskiplist(链表)和zskiplistNode (节点)

  1. zskiplistNode(节点) 数据结构:
typedef struct zskiplistNode{
   //层
     struct zskiplistLevel{
     //前进指针
        struct zskiplistNode *forward;
    //跨度
        unsigned int span;
    } level[];
  //后退指针
    struct zskiplistNode *backward;
  //分值
    double score;
  //成员对象
    robj *obj;
}
  • 层:level 数组可以包含多个元素,每个元素都包含一个指向其他节点的指针。
  • 前进指针:用于指向表尾方向的前进指针
  • 跨度:用于记录两个节点之间的距离
  • 后退指针:用于从表尾向表头方向访问节点


  • 分值和成员:跳跃表中的所有节点都按分值从小到大排序。成员对象指向一个字符串,这个字符串对象保存着一个SDS值


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

}zskiplist;
</svg>" width="530">



从结构图中我们可以清晰的看到,header,tail分别指向跳跃表的头结点和尾节点。level 用于记录最大的层数,length 用于记录我们的节点数量。

总结

  1. 跳跃表是有序集合的底层实现之一
  1. 主要有zskiplist 和zskiplistNode两个结构组成
  1. 每个跳跃表节点的层高都是1至32之间的随机数
  1. 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的对象必须是唯一的
  1. 节点按照分值的大小从大到小排序,如果分值相同,则按成员对象大小排序

整数集合

整数集合是集合建的底层实现之一,当一个集合中只包含整数,且这个集合中的元素数量不多时,redis就会使用整数集合intset作为集合的底层实现

【其实就是一个特殊的集合,里面存储的数据只能够是整数,并且数据量不能过大】

结构

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

}
1、encoding:用于定义整数集合的编码方式
2、length:用于记录整数集合中变量的数量
3、contents:用于保存元素的数组,虽然我们在数据结构图中看到,intset将数组定义为int8_t,
但实际上数组保存的元素类型取决于encoding

整数集合的升级

intset 在默认情况下会帮我们设定整数集合中的编码方式,但是当我们存入的整数不符合整数集合中的编码格式时,就需要使用到Redis 中的升级策略来解决
  • Intset 中升级整数集合并添加新元素共分为三步进行:
  • 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
  • 将底层数组现有的所有元素都转换成新的编码格式,重新分配空间


  • 将新元素加入到底层数组中


  • 整数集合升级的好处
  • 提升灵活性
  • 节约内存

总结

  • 整数集合是集合建的底层实现之一
  • 整数集合的底层实现为数组,这个数组以有序,无重复的范式保存集合元素,在有需要时,程序会根据新添加的元素类型改变这个数组的类型
  • 升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存
  • 整数集合只支持升级操作,不支持降级操作

压缩列表

压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值, 要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。

构成

压缩列表是 Redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。

一个压缩列表可以包含任意多个节点(entry), 每个节点可以保存一个字节数组或者一个整数值。

</svg>" width="509">

属性 | 类型 | 长度 | 用途

:-: | :-: | :-: | :-:

zlbytes | uint32_t | 4 字节 | 记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。

zltail | uint32_t | 4 字节 | 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节: 通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址。

zllen | uint16_t | 2 字节 | 记录了压缩列表包含的节点数量: 当这个属性的值小于 UINT16_MAX (65535)时, 这个属性的值就是压缩列表包含节点的数量; 当这个值等于 UINT16_MAX 时, 节点的真实数量需要遍历整个压缩列表才能计算得出。

entryX | 列表节点 | 不定 | 压缩列表包含的各个节点,节点的长度由节点保存的内容决定。

zlend | uint8_t | 1 字节 | 特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。

连锁更新

添加新节点可能会引发连锁更新之外, 删除节点也可能会引发连锁更新

总结

  • 压缩列表是一种为了节约内存而开发的顺序型数据结构
  • 压缩列表被用作列表键和哈希键的底层实现之一
  • 压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值
  • 添加新节点到压缩列表,可能会引发连锁更新操作,删除节点也可能会引发连锁更新

对象

字符串对象

如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于39字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值

embstr 编码是专门用于保存短字符串的一种优化编码方式, 这种编码和 raw 编码一样, 都使用 redisObject 结构和 sdshdr 结构来表示字符串对象, 但 raw 编码会调用两次内存分配函数来分别创建 redisObject 结构和 sdshdr 结构, 而 embstr 编码则通过调用一次内存分配函数来分配一块连续的空间, 空间中依次包含 redisObject 和 sdshdr 两个结构

</svg>" width="372">

embstr 编码的字符串对象在执行命令时, 产生的效果和 raw 编码的字符串对象执行命令时产生的效果是相同的,但使用embstr 编码的字符串对象来保存短字符串值有以下好处:

embstr 编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次。 释放 embstr 编码的字符串对象只需要调用一次内存释放函数, 而释放 raw 编码的字符串对象需要调用两次内存释放函数。



* 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存带来的优势。

字符串对象保存各类型值的编码方式:

值 | 编码

:-: | :-:

可以用 long 类型保存的整数。 | int

可以用 long double 类型保存的浮点数。 | embstr 或者 raw

字符串值, 或者因为长度太大而没办法用 long 类型表示的整数, 又或者因为长度太大而没办法用 long double 类型表示的浮点数。 | embstr 或者 raw

int 编码的字符串对象和 embstr 编码的字符串对象在条件满足的情况下, 会被转换为 raw 编码的字符串对象。

对于 int 编码的字符串对象来说, 如果我们向对象执行了一些命令, 使得这个对象保存的不再是整数值, 而是一个字符串值, 那么字符串对象的编码将从 int 变为 raw 。

列表对象

列表对象的编码可以是 ziplist 或者 linkedlist 。
  1. ziplist 编码的列表对象使用压缩列表作为底层实现, 每个压缩列表节点(entry)保存了一个列表元素
  1. linkedlist 编码的列表对象使用双端链表作为底层实现, 每个双端链表节点(node)都保存了一个字符串对象, 而每个字符串对象都保存了一个列表元素

当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist 编码:

列表对象保存的所有字符串元素的长度都小于 64 字节; 列表对象保存的元素数量小于 512 个;

以上两个条件的上限值是可以修改的, 具体请看配置文件中关于 list-max-ziplist-value 选项和 list-max-ziplist-entries 选项的说明

不能满足这两个条件的列表对象需要使用 linkedlist 编码。

哈希对象

哈希对象的编码可以是 ziplist 或者 hashtable
  1. ziplist 编码的哈希对象使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾, 因此:
  1. 保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;


  1. 先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。


  1. hashtable 编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存

当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码:

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节;
  • 哈希对象保存的键值对数量小于 512 个;
这两个条件的上限值是可以修改的, 具体请看配置文件中关于 hash-max-ziplist-value 选项和 hash-max-ziplist-entries 选项的说明

不能满足这两个条件的哈希对象需要使用 hashtable 编码。

集合对象

集合对象的编码可以是 intset 或者 hashtable
  1. intset 编码的集合对象使用整数集合作为底层实现, 集合对象包含的所有元素都被保存在整数集合里面
  1. hashtable 编码的集合对象使用字典作为底层实现, 字典的每个键都是一个字符串对象, 每个字符串对象包含了一个集合元素, 而字典的值则全部被设置为 NULL

当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码:

  • 集合对象保存的所有元素都是整数值;
  • 集合对象保存的元素数量不超过 512 个;
第二个条件的上限值是可以修改的, 具体请看配置文件中关于 set-max-intset-entries 选项的说明。

不能满足这两个条件的集合对象需要使用 hashtable 编码。

有序集合对象

有序集合的编码可以是 ziplist 或者 skiplist


  1. ziplist 编码的有序集合对象使用压缩列表作为底层实现

    每个集合元素使用两个紧挨在一起的压缩列表节点来保存, 第一个节点保存元素的成员(member), 而第二个元素则保存元素的分值(score)

    压缩列表内的集合元素按分值从小到大进行排序, 分值较小的元素被放置在靠近表头的方向, 而分值较大的元素则被放置在靠近表尾的方向


  1. skiplist 编码的有序集合对象使用 zset 结构作为底层实现

    一个 zset 结构同时包含一个字典和一个跳跃表

内存回收

因为 C 语言并不具备自动的内存回收功能, 所以 Redis 在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制, 通过这一机制, 程序可以通过跟踪对象的引用计数信息, 在适当的时候自动释放对象并进行内存回收

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

  • 在创建一个新对象时, 引用计数的值会被初始化为 1 ;
  • 当对象被一个新程序使用时, 它的引用计数值会被增一;
  • 当对象不再被一个程序使用时, 它的引用计数值会被减一;
  • 当对象的引用计数值变为 0 时, 对象所占用的内存会被释放。

对象的整个生命周期可以划分为创建对象、操作对象、释放对象三个阶段。

对象共享

除了用于实现引用计数内存回收机制之外, 对象的引用计数属性还带有对象共享的作用

在 Redis 中, 让多个键共享同一个值对象需要执行以下两个步骤:

1. 将数据库键的值指针指向一个现有的值对象;

2. 将被共享的值对象的引用计数增一。

目前来说, Redis 会在初始化服务器时, 创建一万个字符串对象, 这些对象包含了从 0 到 9999 的所有整数值, 当服务器需要用到值为 0 到 9999 的字符串对象时, 服务器就会使用这些共享对象, 而不是新创建对象

创建共享字符串对象的数量可以通过修改 redis.h/REDIS_SHARED_INTEGERS 常量来修改

如果我们创建一个值为 100 的键 A , 并使用 OBJECT REFCOUNT 命令查看键 A 的值对象的引用计数, 我们会发现值对象的引用计数为 2 :

redis> SET A 100
OK

redis> OBJECT REFCOUNT A
(integer) 2

引用这个值对象的两个程序分别是持有这个值对象的服务器程序, 以及共享这个值对象的键 A

</svg>" width="550">

如果这时我们再创建一个值为 100 的键 B , 那么键 B 也会指向包含整数值 100 的共享对象, 使得共享对象的引用计数值变为 3

redis> SET B 100
OK

redis> OBJECT REFCOUNT A
(integer) 3

redis> OBJECT REFCOUNT B
(integer) 3
</svg>" width="550">

这些共享对象不单单只有字符串键可以使用, 那些在数据结构中嵌套了字符串对象的对象(linkedlist 编码的列表对象、 hashtable 编码的哈希对象、 hashtable 编码的集合对象、以及 zset 编码的有序集合对象)都可以使用这些共享对象

为什么 Redis 不共享包含字符串的对象?

当服务器考虑将一个共享对象设置为键的值对象时, 程序需要先检查给定的共享对象和键想创建的目标对象是否完全相同, 
只有在共享对象和目标对象完全相同的情况下, 程序才会将共享对象用作键的值对象,
 而一个共享对象保存的值越复杂, 验证共享对象和目标对象是否相同所需的复杂度就会越高, 消耗的 CPU 时间也会越多:

  如果共享对象是保存整数值的字符串对象, 那么验证操作的复杂度为 O(1) ;
  如果共享对象是保存字符串值的字符串对象, 那么验证操作的复杂度为 O(N) ;
  如果共享对象是包含了多个值(或者对象的)对象, 比如列表对象或者哈希对象, 那么验证操作的复杂度将会是 O(N^2) 。
因此, 尽管共享更复杂的对象可以节约更多的内存, 但受到 CPU 时间的限制, Redis 只对包含整数值的字符串对象进行共享。

总结

  • Redis 数据库中的每个键值对的键和值都是一个对象。
  • Redis 共有字符串、列表、哈希、集合、有序集合五种类型的对象,每种类型的对象至少都有两种或以上的编码方式,不同的编码可以在不同的使用场景上优化对象的使用效率。
  • 服务器在执行某些命令之前, 会先检查给定键的类型能否执行指定的命令, 而检查一个键的类型就是检查键的值对象的类型。
  • Redis 的对象系统带有引用计数实现的内存回收机制, 当一个对象不再被使用时, 该对象所占用的内存就会被自动释放。
  • Redis 会共享值为 0 到 9999 的字符串对象。
  • 对象会记录自己的最后一次被访问的时间, 这个时间可以用于计算对象的空转时间

参考文档传送门