Bootstrap

Redis`数据结构`与`对象`概述

Redis数据结构对象概述


本篇文章通过剖析Redis五种不同类型的对象所使用的底层数据结构,让我们更好的了解这些数据结构是如何深刻地影响对象的功能和性能的

Redis 数据库里面的每个键值对(key-value) 都是由对象(object)组成的:
数据库的键总是一个字符串对象(sds);
数据库的值则可以是字符串对象(string)、列表对象(list)、哈希对象(hash)、集合对象(set)、有序集合(sort set)对象这五种对象中的其中一种。

先用一张图概括:
在这里插入图片描述

Redis用到的所有主要数据结构包括:简单动态字符串、双端链表、字典、跳跃表、整数集合、压缩列表。

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

首先弄清楚Redis数据结构与对象之间的关系:对象是对底层数据结构的二次封装或者抽象,通过编码(每种类型的对象都至少使用了两种不同的编码),使得每种类型的对象都用到了至少一种数据结构。

一、数据结构

Redis用到的所有主要数据结构包括:简单动态字符串、双端链表、字典、跳跃表、整数集合、压缩列表。

1、简单动态字符串(SDS)

Redis 没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string SDS)的抽象类型,并将SDS用作Redis 的默认字符串表示。除了用来保存字符串以外,SDS还被用作缓冲区(buffer)AOF模块中的AOF缓冲区。

SDS结构体定义
typedef struct sdshdr {
    int len;      // 记录已经使用的空间长度
    int free;     // 记录未使用字节的数量
    char buf[];   // 字节数组,用于保存字符串
} listNode;
SDS结构示意图

在这里插入图片描述

使用SDS的五个优点
  • 1、获取一个SDS长度的复杂度为O(1);

    C 语言里获取一个长度为字符串的长度,必须遍历整个字符串。
    SDS 的数据结构中可以通过获取len 属性的值,直接知道字符串长度。

  • 2、杜绝缓冲区溢出;

    SDS空间分配策略完全杜绝了发生缓冲区溢出的可能性:
    当我们需要对一个SDS 进行修改的时候,redis 会在执行拼接操作之前,预先检查给定SDS 空间是否足够,如果不够,会先拓展SDS 的空间,然后再执行拼接操作。

  • 3、减少修改字符串带来的内存重分配次数;

    SDS采用了空间预分配策略惰性空间释放策略来避免内存分配问题。

    空间预分配策略是指,每次 SDS 进行空间扩展时,程序不但为其分配所需的空间,还会为其分配额外的未使用空间,以减少内存再分配次数。额外分配的未使用空间大小取决于空间扩展后SDS 的 len 属性值。

    • 如果 len 属性值小于 1M,那么分配的未使用空间 free 的大小与 len 属性值相同。
    • 如果 len 属性值大于等于 1M ,那么分配的未使用空间 free 的大小固定是 1M。

    惰性空间释放策略是指,SDS 字符串长度如果缩短,那么多出的未使用空间将暂时不释放,而是增加到 free 中。以使后期扩展 SDS 时减少内存 再分配次数。如果要释放 SDS 的未使用空间,则可通过 sdsRemoveFreeSpace()函数来释放。

  • 4、二进制安全;

    不像C语言, 除了能保存文本数据,也能保存像图片,音频,视频,压缩文件这样的二进制数据。

  • 5、兼容部分C函数。

    可以使用一部分<string.h>库中的函数。如果是对比C语言,这并不优点,只能算特点。

C字符串SDS
获取字符串长度的复杂度为O(N)获取字符串长度的复杂度为O(1)
API 是不安全的,可能会造成缓冲区溢出API 是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配修改字符串长度N次最多执行N次内存重分配
只能保存文本数据可以保存二进制数据和文本文数据
可以使用所有<String.h>库中的函数可以使用一部分<string.h>库中的函数

2、双端链表(list)

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。

链表在Redis 中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis 就会使用链表作为列表键的底层实现。

链表结构体定义
// 链表节点
typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

// 链表【通过链表持有链表节点,操作起来更加方便】
typedef struct list {
    listNode *head;  //表头节点
    listNode *tail;  //表尾节点
    void *(*dup)(void *ptr);  //节点值复制函数
    void (*free)(void *ptr);  //节点值释放函数
    int (*match)(void *ptr, void *key);  //节点值对比函数
    unsigned long len;    //链表长度
} list;

// 由链表结构体可以看出链表特性: 双端(有前后指针)、无环、表头和表尾、长度计数器、多态(通过dup、free、match三个属性设置类型特定函数)。
list结构示意图

在这里插入图片描述

3、字典(dict)

字典是一种用于保存键值对的抽象数据结构。

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

字典结构体定义
// 字典
struct dict {
    dictType *type;  // 类型指定的哈希函数,用于计算哈希值
    dictht *ht[2];   // 哈希表
    long rehashidx;  // rehash索引,用于判定当前是否在进行rehash
    void *metadata[];           
};

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

// 哈希表节点
struct dictEntry {
    void *key;     // 键
    union {        // 值
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;     // 链地址法解决hash冲突 
    void *metadata[];          
};
dict结构示意图

在这里插入图片描述

hash算法

当要将一个新的键值对添加到字典里时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。

第一步:先计算出哈希值;
hash=dict→type→hashFunction(key);

第二步:根据计算出来的哈希值 和 哈希表结构的sizemask属性,计算出索引值。
index=hash & dict→ht[x].sizemask;

如何计算哈希值?
当字典被用作数据库的底层实现,或者哈希建的底层实现时,redis使用MurmurHash2算法来计算键的哈希值【MurmurHash2算法不仅计算速度快,而且即使键是有规律的,计算出来的哈希值也是随机的】

渐进式rehash

随着对哈希表的不断操作,哈希表保存的键值用作对会逐渐的发生改变,为了让哈希表的负载因子维持在一个合理的范围之内,我们需要对哈希表的大小进行相应的扩展或者压缩,这时候,我们可以通过 rehash(重新散列)操作来完成。这个rehash 操作并不是一次性、集中式完成的,而是分多次、渐进式地完成的。

哈希表空间分配规则:
如果执行的是拓展操作,那么ht[1] 的大小为第一个大于等于ht[0] 的2的n次幂。
如果执行的是收缩操作,那么ht[1] 的大小为第一个大于等于ht[0] 的2的n次幂。

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

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

4、跳跃表(skiplist)

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

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

跳跃表结构体定义
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;         //成员对象
    double score;    //分值
    struct zskiplistNode *backward;  //后退指针
    struct zskiplistLevel {          //层
        struct zskiplistNode *forward;  //前进指针
        unsigned long span;          //跨度,用于记录两个节点之间的距离
    } level[];
} zskiplistNode;


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

跳跃表在每次创建一个新跳跃表节点的时候,程序都根据幂次定律(power law:越大的数出现的概率越小)随机生成一个介于1到32之间的值作为level数组的大小,这个大小就是层的高度。

跨度,则是用于记录两个节点之间的距离。

skiplist结构示意图

在这里插入图片描述

跳跃表特点

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

5、整数集合(intset)

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

整数集合结构体定义
typedef struct intset {
    uint32_t encoding;  // 编码方式——int16/int32/int64等
    uint32_t length;    // 长度
    int8_t contents[];  // 用于保存元素的数组
} intset;
intset结构示意图

在这里插入图片描述

升级

要将一个新元素添加到整数集合里,并且新元素的类型比整数集合 现有元素的类型都要长时,整数集合需要先进行升级(upgrade),然后才能将新元素添加到整数集合里。

升级步骤:1、分配空间;2、转换类型,排序不变;3、添加新元素。

没有降级操作:一旦整数集合进行了升级,编码类型就会保持升级后的状态。

6、压缩列表(ziplist)

压缩列表是列表键和哈希键的底层实现之一。

压缩列表ziplist结构本身就是一个连续的内存块:由表头、若干个entry节点和压缩列表尾部标识符zlend组成,通过一系列编码规则,提高内存的利用率,使用于存储整数和短字符串

压缩列表ziplist结构的缺点是:每次插入或删除一个元素时,都需要进行频繁的调用realloc()函数进行内存的扩展或减小,然后进行数据”搬移”,甚至可能引发连锁更新,造成严重效率的损失。

ziplist结构示意

在这里插入图片描述

zlbytes:用于记录整个压缩列表占用的内存字节数。
zltail:记录要列表尾节点距离压缩列表的起始地址有多少字节。用于快速定位尾节点。
zllen:记录了压缩列表包含的节点数量。
entryX:压缩列表包含的各个节点。
zlend:用于标记压缩列表的末端。
value:也叫content,可以用来保存一个字节数组或者整数。

压缩表特点

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

previous_entry_length

previous_entry_length用于记录压缩列表前一个节点的长度。

如果前一节点长度<254字节,那么previous_entry_length属性的长度为1字节;如果前一节点长度>=254字节,那么previous_entry_length属性的长度为5字节。

previous_entry_length的作用:从表尾向表头遍历[回溯]。通过指针计算,根据当前节点的起始地址来计算出前一个节点的起始地址。

连锁更新

Redis在某些特殊情况下会产生连续多次的空间扩展操作,这称之为连锁更新。

比如:大多数entry节点的长度都是250~253字节的,如果新增了一个长度超过254字节的节点,则新增节点的后一个节点previous_entry_length要扩展为5字节的,这会导致连锁更新。同理删除也可能会触发连锁更新。

连锁更新的时间复杂度为O(N^2),这会比较占用CPU性能。然而我们不需要过分担心,因为连锁更新真正造成性能问题的几率非常小:连锁更新的前提条件是——恰好有多个连续的介于250~253字节之间的节点。

7、快速列表(quicklist)

quicklist结构是在redis 3.2版本中新加的数据结构,用在列表的底层实现。

quicklistziplist的关系:quicklist是由ziplist组成的双向链表,链表中的每一个节点都以压缩列表ziplist的结构保存着数据,而ziplist有多个entry节点,保存着数据。相当与一个quicklist节点保存的是一片数据,而不再是一个数据

  • quicklist宏观上是一个双向链表,因此,它具有一个双向链表的有点,进行插入或删除操作时非常方便,虽然复杂度为O(n),但是不需要内存的复制,提高了效率,而且访问两端元素复杂度为O(1)。
  • quicklist微观上是一片片entry节点,每一片entry节点内存连续且顺序存储,可以通过二分查找log2(n)log2(n) 的复杂度进行定位
快速列表结构体定义
// 快表
typedef struct quicklist {
    quicklistNode *head;     //指向头部(最左边)quicklist节点的指针
    quicklistNode *tail;     //指向尾部(最右边)quicklist节点的指针
    unsigned long count;     //ziplist中的entry节点计数器
    unsigned long len;       //quicklistNode节点计数器
    // ...    // 一些压缩参数
} quicklist;

// 快表节点
typedef struct quicklistNode {
    struct quicklistNode *prev;  //前驱节点指针
    struct quicklistNode *next;  //后驱节点指针
    unsigned char *entry;        //压缩列表ziplist的总长度
    size_t sz;             /* entry size in bytes */
    // ...                 // 一些压缩参数
} quicklistNode;

// 压缩过的ziplist结构
typedef struct quicklistLZF {
    size_t sz; /* LZF size in bytes*/ //表示被LZF算法压缩后的ziplist的大小
    char compressed[]; //一个柔性数组,保存压缩后的ziplist的数组
} quicklistLZF;

// 管理quicklist中quicklistNode节点中ziplist信息的结构
typedef struct quicklistEntry {
    const quicklist *quicklist;  //指向所属的quicklist的指针
    quicklistNode *node;         //指向所属的quicklistNode节点的指针
    unsigned char *zi;           //指向当前ziplist结构的指针
    unsigned char *value;        //指向当前ziplist结构的字符串vlaue成员
    long long longval;           //指向当前ziplist结构的整数
    size_t sz;                   //保存当前ziplist结构的字节数大小
    int offset;                  //保存相对ziplist的偏移量
} quicklistEntry;
quicklist结构示意图

在这里插入图片描述

二、RedisObject

在前面的文章中,我们介绍了 Redis 用到的主要数据结构,比如简单动态字符串、双端链表、字典、压缩列表、整数集合。
然而 Redis 并没有直接使用这些数据结构来实现键值对的数据库,而是在这些数据结构之上又包装了一层 RedisObject(对象),RedisObject 有五种对象:字符串对象、列表对象、哈希对象、集合对象和有序集合对象。

redisObject结构体定义如下:

struct redisObject {
    unsigned type:4;       // 对象类型(5种)
    unsigned encoding:4;   // 编码(8种),记录对象所使用的编码,也就是底层的数据结构
    unsigned lru:LRU_BITS; // 访问时间(用于记录空转时长、内存回收)
    int refcount;          // 引用计数(用于对象共享、垃圾回收)
    void *ptr;             // 指向底层实现结构的指针,由encoding决定
};

从结构体定义就可以归纳出使用redisObject的优点:

  • 校验:通过不同类型的对象,Redis 可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。
  • 实现灵活:我们可以针对不同的使用场景,为对象设置不同的实现,从而优化内存或查询速度。
  • 引用计数:当一个对象的引用计数为0时,则表示该对象已经不被任何对象引用,则可以进行垃圾回收。另外,Redis还基于引用计数实现对象共享机制。
  • 空转时间:记录访问时间,用于服务器开启maxmemory功能下的key删除[优先删除空转时间较大的]。

在这里插入图片描述

代码示例:

# 字符串对象
127.0.0.1:6379> set msg "hello world!"
OK
127.0.0.1:6379> get msg
"hello world!"
127.0.0.1:6379> type msg
string
# 列表对象
127.0.0.1:6379> rpush numbers 1 3 5
(integer) 3
127.0.0.1:6379> type numbers
list
# 哈希对象
127.0.0.1:6379> hset profile name tom age 28 career programmer
(integer) 3
127.0.0.1:6379> type profile
hash
# 集合对象
127.0.0.1:6379> sadd fruits apple banana cherry
(integer) 3
127.0.0.1:6379> type fruits
set
# 有序集合对象
127.0.0.1:6379> zadd price 8.5 apple 5.0 banana 6.8 cherry
(integer) 3
127.0.0.1:6379> type price
zset

对象与数据结构的关系: 每种类型的对象都至少使用了两种不同的编码,也即每种类型的对象都用到了至少一种数据结构。如下表:

type类型encoding编码对象Object
redis_stringredis_encoding_int使用整数值实现的字符串对象
redis_stringredis_encoding_embstr使用embstr编码的sds实现的字符串对象
redis_stringredis_encoding_raw使用sds实现的字符串对象
redis_listredis_encoding_ziplist使用压缩列表实现的列表对象
redis_listredis_encoding_linkedlist使用双端链表实现的列表对象
redis_hashredis_encoding_ziplist使用字典实现的哈希对象
redis_hashredis_encoding_ht使用整数值实现的哈希对象
redis_setredis_encoding_intset使用整数集合实现的集合对象
redis_setredis_encoding_ht使用字典实现的集合对象
redis_zsetredis_encoding_ziplist使用压缩列表实现的有序集合对象
redis_zsetredis_encoding_skiplist使用跳跃表和字典实现的有序集合对象

1、字符串对象

int

如果一个字符串对象保存的是整数值,并且这个整数值可以用 long 类型标识,那么字符串对象会讲整数值保存在 ptr 属性中,并将 encoding 设置为 int。

在这里插入图片描述

raw

如果字符串对象保存的是一个字符串值,并且这个字符串的长度 > 39 字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为 raw。

在这里插入图片描述

emstr

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

在这里插入图片描述

使用 embstr 的编码方式有一些优点,如下:

  • embstr 编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次。
  • 释放 embstr 编码的字符串对象只需要调用一次内存释放函数,而释放 raw 编码的字符串对象需要调用两次内存释放函数。
  • 因为 embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw ,编码的字符串对象能够更好地利用缓存带来的优势。
编码转换
  • bint转raw

    127.0.0.1:6379> set num 10086
    OK
    127.0.0.1:6379> object encoding num
    "int"
    127.0.0.1:6379> append num " is a good number!"
    (integer) 23
    127.0.0.1:6379> get num
    "10086 is a good number!"
    127.0.0.1:6379> object encoding num
    "raw"
    
    
  • embstr转raw

    127.0.0.1:6379> set msg "hello world!"
    OK
    127.0.0.1:6379> object encoding msg
    "embstr"
    127.0.0.1:6379> append msg "Im redis!"
    (integer) 22
    127.0.0.1:6379> get msg
    " hello world!Im redis!"
    127.0.0.1:6379> object encoding msg
    "raw"
    
    

注:

  • 用 long 类型保存的整数在redis中是字符串对象,编码类型是int;用 long double类型的浮点数在redis中也是字符串对象,编码类型是embstr或者raw;在对它们进行运算时,会将它们转化为对应的类型再运算,保存时再转化为字符串对象。
  • embstr编码的字符串对象是只读的,所以对其进行修改,总是会转化为raw编码。
  • 另外,字符串对象stringObject是redis五种类型的对象中唯一一种会被其它四种对象嵌套的对象。

字符串在redis有如下命令:
set、get;append;incrbyfloat、incrby、decrby;strlen;setrange、getrange等

2、列表对象

ziplist

ziplist(压缩列表)主要是为节省内存而设计的内存结构,它的优点就是节省内存,但缺点就是比其他结构要消耗更多的时间,所以 Redis 在列表对象的数据量小的时候使用压缩列表存储。
在这里插入图片描述

linkedlist

当列表的长度小于 512,并且所有元素的长度都小于 64 字节时,使用压缩列表存储;否则使用 linkedlist 存储。

在这里插入图片描述

编码转换

使用ziplist编码必须满足两个条件:
条件1:列表对象保存所有的字符串元素的长度都小于64字节。
条件2:列表对象保存的元素数量小于512个。

  • ziplist转linkedlist:破坏条件1

    127.0.0.1:6379> rpush blabla "hello" "world"
    (integer) 2
    127.0.0.1:6379> llen blabla
    (integer) 2
    127.0.0.1:6379> object encoding blabla
    "ziplist"
    # 新增列表值的字符串长度65
    127.0.0.1:6379> rpush blabla wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww
    (integer) 3
    127.0.0.1:6379> object encoding blabla
    "linkedlist"
    
    
  • ziplist转linkedlist:破坏条件2

    127.0.0.1:6379> eval "for i=1,512 do redis.call('RPUSH',KEYS[1],i) end" 1 "integers"
    (nil)
    127.0.0.1:6379> llen integers
    (integer) 512
    127.0.0.1:6379> object encoding integers
    "ziplist"
    127.0.0.1:6379> rpush integers 513
    (integer) 513
    127.0.0.1:6379> object encoding integers
    "linkedlist"
    
    

列表在redis有如下命令:
lpush、rpush、lpop、rpop;lindex;llen;linsert、lrem、ltrim、lset

3、哈希对象

ziplist

ziplist(压缩列表)主要是为节省内存而设计的内存结构,它的优点就是节省内存,但缺点就是比其他结构要消耗更多的时间,所以 Redis 在哈希对象的数据量小的时候使用压缩列表存储。

在这里插入图片描述

hashtable

当哈希对象保存的键值对数量小于 512,并且所有键值对的长度都小于 64 字节时,使用压缩列表存储;否则使用 hashtable 存储。

在这里插入图片描述

编码转换

使用ziplist编码必须满足两个条件:
条件1:哈希对象所有键值对的key和value的字符串元素的长度都小于64字节。
条件2:哈希对象保存的键值对数量小于512个。

  • ziplist转hashtable:破坏条件1

    127.0.0.1:6379> hset anotherbook  hello world
    (integer) 1
    127.0.0.1:6379> object encoding anotherbook
    "ziplist"
    # 新增哈希对象值的字符串长度65
    127.0.0.1:6379> hset book author wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww
    (integer) 1
    127.0.0.1:6379> object encoding book
    "hashtable"
    
    
  • ziplist转hashtable:破坏条件2

    127.0.0.1:6379> eval "for i=1, 511 do redis.call('hset', KEYS[1], i ,i)end" 1 "anotherbook"
    (nil)
    127.0.0.1:6379> object encoding anotherbook
    "ziplist"
    127.0.0.1:6379> hset anotherbook  hello1 world1
    (integer) 1
    127.0.0.1:6379> object encoding anotherbook
    "hashtable"
    
    

哈希在redis有如下命令:
hset、hget;hexists;hdel、hlen、hgetall

4、集合对象

intset

intset(整数集合)主要是为节省内存而设计的内存结构,它的优点就是节省内存,但缺点就是比其他结构要消耗更多的时间,所以 Redis 在集合对象的数据量小的时候使用整数集合存储。

在这里插入图片描述

hashtable

当集合的长度小于 512,并且所有元素都是整数时,使用整数集合存储;否则使用 hashtable 存储。

在这里插入图片描述

编码转换

集合对象使用intset编码必须满足两个条件:
条件1:集合的所有元素都是整数,使用整数集合存储。
条件2:集合的长度小于 512。

  • intset转hashtable:破坏条件1

    127.0.0.1:6379> sadd elements 1 3 5
    (integer) 3
    127.0.0.1:6379> object encoding elements
    "intset"
    127.0.0.1:6379> sadd elements "seven"
    (integer) 1
    127.0.0.1:6379> object encoding elements
    "hashtable"
    
    
  • intset转hashtable:破坏条件2

    127.0.0.1:6379> eval "for i=1, 511 do redis.call('sadd', KEYS[1], i)end" 1 "anotherelements"
    (nil)
    127.0.0.1:6379> scard anotherelements
    (integer) 511
    127.0.0.1:6379> object encoding anotherelements
    "intset"
    127.0.0.1:6379> sadd anotherelements 512
    (integer) 1
    127.0.0.1:6379> scard anotherelements
    (integer) 512
    127.0.0.1:6379> object encoding anotherelements
    "hashtable"
    
    

集合在redis有如下命令:
sadd、spop;scard;sismember、smembers、srandmember;srem

5、有序集合对象

ziplist

ziplist(压缩列表)主要是为节省内存而设计的内存结构,它的优点就是节省内存,但缺点就是比其他结构要消耗更多的时间,所以 Redis 在有序集合对象的数据量小的时候使用压缩列表存储。

在这里插入图片描述

skiplist

当有序集合的长度小于 128,并且所有元素的长度都小于 64 字节时,使用压缩列表存储;否则使用 skiplist 存储。
在这里插入图片描述

编码转换

有序集合对象使用ziplist编码必须满足两个条件:
条件1:有序集合所有元素的长度都小于 64 字节。
条件2:有序集合的长度小于 128个。

  • ziplist转skiplist:破坏条件1

    127.0.0.1:6379> zadd orderslen 2 www
    (integer) 1
    127.0.0.1:6379> object encoding orderslen
    "ziplist"
    127.0.0.1:6379> zadd orderslen 3 wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww
    (integer) 1
    127.0.0.1:6379> object encoding orderslen
    "skiplist"
    
    
  • ziplist转skiplist:破坏条件2

    127.0.0.1:6379> eval "for i=1,128 do redis.call('zadd',KEYS[1],i,i)end" 1 "orders"
    (nil)
    127.0.0.1:6379> zcard orders
    (integer) 128
    127.0.0.1:6379> object encoding orders
    "ziplist"
    127.0.0.1:6379> zadd orders 3.14 pai
    (integer) 1
    127.0.0.1:6379> zcard orders
    (integer) 129
    127.0.0.1:6379> object encoding orders
    "skiplist"
    
    

有序集合在redis有如下命令:
zadd;zcard;zcount、zrange、zrevrange;zrank、zrevrank;zrem;zscore

五种对象在redis中命令的实现方法与该对象的编码方式有关。
Redis除了会根据值对象的类型来判断键是否能够执行指定命令【即类型检查】之外【基于类型Type的多态】,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令。【基于编码的多态】

附注:
redis用于操作键的命令基本上可以分为两大类。
1、对任何类型的键都可以执行,如:del、expire、rename、type、object等命令
2、只能对特定类型的键执行的命令。如前面提到的set、append等只能对字符串键执行,hset、hdel等只能对哈希键执行,等等。这些指定命令在执行前会进行类型检查

  • 内存回收:Redis在自己的对象中基于引用计数【refcount】实现的内存回收机制
  • 对象共享:Redis通过对象共享机制来节约内存。例如:Redis会在初始化服务器时,创建1W个字符串对象【0到9999】的所有整数值。后面用到这些字符串对象时就会直接使用这些对象,而不是新创建对象

object refcount 命令可以查看值对象的引用次数。

  • 对象的空转时长:Redis中对象的的空转时长=当前时间-键的值对象的lru时间【记录最后一次被命令[除了object idletime命令]程序访问的时间】。它还有另外一个重要作用:当服务器打开maxmemory选项,并且服务器用于回收内存的算法为volatile-lru货allkeys-lru时,那么当服务器占用的内存超过了maxmemory选项所设置的上限值时,空转时间长的键就会被服务器优先释放,从而实现回收内存。

object idletime 命令可以查看值对象的lru时间。

;