Redis的五种基本数据类型

Redis 内部提供了多种数据结构,比如简单动态字符串(SDS)、双端链表、字典、压缩列表、整数集合等等。但 Redis 并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象。

通过这五种不同类型的对象,Redis 可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。使用对象的另一个好处是,我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。

除此之外,Redis 的对象系统还实现了基于引用计数的内存回收机制,当程序不再使用某个对象时,这个对象所占用的内存就会被自动释放;另外,Redis 还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。最后,Redis 的对象带有访向时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了 maxmemory 功能的情况下,空转时长较大的那些键可能会优先被服务器刪除。

对象结构

Redis 使用对象来表示数据库中的键和值,每次当我们在 Redis 的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。Redis 中的每个对象都由一个 redisObject 结构表示,该结构包含属性如下:

#define LRU_BITS 24

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;

由对象定义可知:一个 redisObject 包含了 8 字节的元数据(4+4+24)和一个 8 字节指针。抛开其内部存储的元素,光对象定义就消耗了 16 个字节。其元数据作用如下:

  • type:记录了对象的类型,即字符串对象、列表对象等等
  • encoding:记录了对象所使用的编码,即这个对象使用了什么数据结构作为对象的底层实现
  • lru:记录了对象最后一次被访问的时间
  • refcount:对象引用计数,Redis 通过跟踪对象的引用计数信息,在适当时候自动释放对象并回收内存
  • *ptr:指向对象的底层数据结构的指针,这些数据结构由 encoding 属性决定

字符串对象

字符串对象的编码可以是 int、raw 或者 embstr。

1. int

如果一个字符串对象保存的是整数值,并且这个整数值可以用 long 类型来表示,那么字符串对象会将整数值直接保存在字符串对象结构的 ptr 属性里(将 void* 转换成 long)并将字符串对象的编码设置为 int。这种方式还节省了指针的空间开销。

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

2. raw

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

简单动态字符串(Simple Dynamic String,SDS)是 Redis 自己实现的一种字符串表示类型,并且为了尽可能优化 SDS 的内存使用,根据字符串的大小会为其创建不同的 SDS 实现:

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
  • buf:字节数组,用来保存实际的字符数据。Redis 会自动在数组最后加一个 "\0" 表示字节数组的结束,这会额外占用 1 个字节,但保证了存储二进制数据是安全的(可以安全的存储空字符)。

  • len:表示 buf 数组中已使用字节的数量,即字符串长度,不包括最后的空字符。这样可以直接获取字符串的长度而无需进行遍历。

  • alloc:表示 buf 的实际分配长度,一般大于 len,可以包含未使用的空间。通过 SDS 的空间预分配和惰性空间释放来减少内存重分配次数:
    • 空间预分配:SDS 增长时,分配额外的未使用空间
    • 惰性空间释放:SDS 缩短时,不会立即回收内存

3. embstr

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

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

实际上,Redis 并没有为 embstr 编码的字符串对象编写任何相应的修改函数,所以 embstr 编码的字符串对象是只读的。当我们对 embstr 编码的字符串对象执行任何修改命令时,程序会先将对象的编码从 embstr 转换成 raw 编码,然后再执行修改命令。因为这个原因,embstr 编码的字符串对象在执行修改命令后,总会变成一个 raw 编码的字符串对象。

为什么编码的阈值为 44 个字节?

在 Redis 中,如果 SDS 的存储值大于 64 字节时,Redis 的内存分配器会认为此对象为大字符串,并使用 raw 类型来存储,当数据小于 64 字节时会使用 embstr 类型存储。既然内存分配器的判断标准是 64 字节,那为什么 embstr 类型和 raw 类型的存储判断值是 44 字节?

这是因为 Redis 在存储对象时,会创建此对象的关联信息,redisObject 对象头和 SDS 自身属性信息。embstr 是一块连续的内存区域,由 redisObject 和 sdshdr 组成。其中 redisObject 固定占 16 个字节,而对于 sdshdr 来说,本身就是针对短字符串的 embstr 自然会使用最小的 sdshdr8,其占用的大小为:

sdshdr8 = uint8_t * 2 + unsigned char = 1 * 2 + 1 = 3

再加上 buf 数组的结束字符 '\0' 占用的一个字节,总共消耗了 16 + 1 + 3 = 20 字节。当 buf 数组内的字符串长度是 44 时,字符串对象的总大小加起来刚好 64。

列表对象

列表对象的编码可以是 ziplist 或者 linkedlist。当列表对象中的元素可以同时满足以下两个条件时,列表对象会使用 ziplist 编码:

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

不能满足这两个条件的列表对象需要使用 linkedlist 编码。以上两个条件的上限值是可以修改的,具体参考配置文件中关于 list-max-ziplist-valve 选项和 list-max-ziplist-entries 选项的说明。

对于使用 ziplist 编码的列表对象来说,当使用 ziplist 编码所需的两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行,原本保存在压缩列表里的所有列表元素都会被转移并保存到双端链表里面,对象的编码也会从 ziplist 变为 linkedlist。

1. ziplist

ziplist 编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点(entry)保存了一个列表元素。压缩列表是 Redis 为了节约内存而开发的,由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或一个整数值。

压缩列表的表头有三个字段 zlbytes、zltail 和 zllen,分别表示整个压缩列表占用的内存字节数、压缩列表尾节点的偏移量(用于确定表尾节点的位置),以及列表中的 entry 个数。压缩列表尾还有一个 zlend,用于标记压缩列表的末端。

压缩列表之所以能够节省内存开销,就在于它是用一系列连续的 entry 保存数据的。每个 entry 的元数据包括下面这几个部分:

  • prev_len:记录了压缩列表中前一个 entry 的长度。prev_len 有两种取值情况:1 字节或 5 字节。取值 1 字节时,表示前一个 entry 的长度小于 254 字节,否则就取值为 5 字节。通过该值,程序可以根据当前节点的起始地址来计算出前一个节点的起始地址。
  • len:表示自身长度,4 字节。
  • encoding:表示 content 属性所保存数据的编码方式,1 字节。
  • content:保存实际数据。

这些 entry 会连续放置在内存中,不需要再用额外的指针进行连接,这样就可以节省指针所占用的空间了。当添加键值对时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾。因此,保存了同一键值对的两个节点总是紧挨在一起,且先添加的键值对会被放在压缩列表的表头方向,后添加的键值对会被放在压缩列表的表尾方向。

在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时就没有这么高效了,只能逐个查找,此时复杂度就是 O(N) 了。

2. linkedlist

linkedlist 编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。

linkedlist 的结构如下:

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;

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

多个 listNode 可以通过 prev 和 next 指针来组成双端链表:

3. quicklist

考虑到链表的附加空间相对太高,prev 和 next 指针就要占用 16 字节(64 位系统的指针是 8 字节)且每个节点的内存都是单独分配,加剧了内存的碎片化,影响内存管理效率。所以 Redis 3.2 版本之后对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。

如下图所示:quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用了 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。既满足了快速的插入删除性能,又不会出现太大的空间冗余。

quicklist 内部默认单个 ziplist 长度为 8k 字节,超出了这个字节数,就会新起一个 ziplist。ziplist 的长度由配置参数 list-max-ziplist-size 决定。

哈希对象

哈希对象的编码可以是 ziplist 或者 hashtable。

1. ziplist

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

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节;
  • 哈希对象保存的键值对数量小于 512 个;

不能满足这两个条件的哈希对象需要使用 hashtable 编码。这两个条件的上限值也是可以修改的,具体参考配置文件中关于 hash-max-ziplist-value 选项和 hash-max-ziplist-entries 选项的说明。

对于使用 ziplist 编码的列表对象来说,当使用 ziplist 编码所需的两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行,原本保存在压缩列表里的所有键值对都会被转移并保存到字典里面,对象的编码也会从 ziplist 变为 hashtable 编码。一旦从压缩列表转为了哈希表,Hash 类型就会一直用哈希表进行保存,而不会再转回压缩列表了

2. hashtable

hashtable 编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对保存,字典的键和值都是一个字符串对象。hashtable 的读写时间复杂度为 O(1),但它的内存占用要比 ziplist 高。

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

typedef struct dict {
    // 类型特定函数,用于计算key的哈希值
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash索引,当rehash未进行时该值为-1
    long rehashidx;
    unsigned long iterators; /* number of iterators currently running */
} dict;

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

typedef struct dictEntry {
    // 键
    void *key;
    // 值(三种可选类型)
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // 指向下个哈希表节点,形成链表,以此解决哈希冲突
    struct dictEntry *next;
} dictEntry;

dict 结构中的 ht 属性是一个包含两个项的数组,数组中的每一个项都是一个 dictht 哈希表结构。一般情况下,hashtable 只使用 ht[0] 哈希表,ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时才使用。

键冲突的解决:

当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时,我们称这些键发生了冲突。Redis 的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个 next 指针,多个哈希表可以用 next 指针构成一个单向链表,同时为了速度考虑,程序总是将新节点添加到链表的表头位置,这样的复杂度为 O(1)。

rehash:

随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围内,当哈希表保存的键值对数量太多或太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。Redis 通过对字典的哈希表执行 rehash 操作来完成哈希表的扩展和收缩,具体步骤如下:

  • 为字典的 ht[1] 哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及 ht[0] 当前包含的键值对数量:
    • 如果执行的是扩展操作,那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n;
    • 如果执行的是收缩操作,那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n。
  • 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面,rehash 指的是重新计算键的哈希值和索引值,然后将键值对放置到 ht[1] 哈希表的指定位置上。
  • 当 ht[0] 包含的所有键值对都迁移到 ht[1] 后,释放 ht[0] 并将 ht[1] 设置为 ht[0],并在 ht[1] 新建一个空白哈希表,为下一次 rehash 做准备。

在进行 RDB 生成和 AOF 重写时,哈希表的 rehash 是被禁止的,这是为了避免对 RDB 和 AOF 的重写造成影响。如果此时,Redis 没有在生成 RDB 和重写 AOF,那么就可以进行 rehash。否则再有数据写入时,哈希表就要开始使用查询较慢的链式哈希了。

渐进式 rehash:

rehash 的过程看似简单,但第二步涉及大量的数据拷贝,如果一次性把哈希表 ht[1] 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求。为了避免这个问题,Redis 采用了 渐进式 rehash。以下是哈希表渐进式 rehash 的详细步骤:

  • 为 ht[1] 分配空间,让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
  • 在字典中维持一个索引计数器变量 rehashidx,并将它的值设置为 0,表示 rehash 工作正式开始。
  • 期间每次对字典执行增删改查时,程序除了执行指定操作外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1],然后 rehashidx 属性值加一。
  • 随着字典操作的不断进行,最终 ht[0] 的所有键值对都会被 rehash 到 ht[1],这时程序将 rehashidx 属性值设为 -1,表示 rehash 操作完成。

渐进式 rehash 的好处在于它采取分而治之的方式,将 rehash 的工作量均摊到对字典的每个增删改查操作上,避免了集中 rehash 带来的庞大计算量。因为在渐进式 rehash 过程中,字典会同时使用 ht[0] 和 ht[1] 这两个哈希表,所以期间字典的删除、查找、更新操作会在两个哈希表上进行。同时,新增操作一律会保存到 ht[1] 里。

除了根据键值对的操作来进行数据迁移,Redis 本身还会有一个定时任务执行 rehash,如果没有键值对操作时这个定时任务会周期性地迁移一些数据到新的哈希表中,这样可以缩短整个 rehash 过程。

集合对象

集合对象的编码可以是 intset 或者 hashtable。当集合对象中的元素可以同时满足以下两个条件时,集合对象会使用 intset 编码:

  • 集合对象保存的所有元素都是整数值;
  • 集合对象保存的元素数量小于 512 个;

不能满足这两个条件的集合对象需要使用 hashtable 编码。第二个条件的上限值是可以修改的,具体参考配置文件中关于 set-max-intset-entries 选项的说明。

对于使用 intset 编码的集合对象来说,当使用 intset 编码所需的两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行,原本保存在整数集合中的所有元素都会被转移并保存到字典里面,并且对象的编码也会从 intset 变为 hashtable。

1. intset

intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。整数集合可以保存类型为 int16_t、int32_t 或 int64_t 的整数值,并且保证集合中不会出现重复元素。结构如下:

typedef struct intset {
  	// 编码方式
    uint32_t encoding;
  	// 集合包含的元素数量
    uint32_t length;
  	// 保存元素的数组
    int8_t contents[];
} intset;
  • contents 数组是整数集合的底层实现:整数集合的每个元素都是 contents 数组的一个数组项,各个项在数组中按值的大小从小到大有序排列,并且数组中不包含任何重复项。
  • length 属性记录了整数集合包含的元素数量,即 contents 数组的长度。
  • contents 数组的类型取决于 encoding 属性的值。

2. hashtable

hashtable 编码的集合对象使用哈希表作为底层实现,哈希表的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而哈希表的值则全部被设置为 NULL。

有序集合对象

有序集合保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据,score 可以重复。

有序集合对象的编码可以是 ziplist 或者 skiplist。当有序集合对象中的元素可以同时满足以下两个条件时,有序集合对象会使用 ziplist 编码:

  • 有序集合对象保存的所有元素成员的长度都小于 64 字节;
  • 有序集合对象保存的元素数量小于 128 个;

不能满足这两个条件的集合对象需要使用 skiplist 编码。以上两个条件的上限值是可以修改的,具体参考配置文件中关于 zset-max-ziplist-valuezset-max-ziplist-entries 选项的说明。

1. ziplist

ziplist 编码的压缩列表对象使用压缩列表做为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素靠近表头,而分值较大的元素靠近表尾方向。

由于这个 ziplist 是需要按照 score 排序的,所以在插入一个元素时,需要先根据 score 找到对应的位置,然后把 member 和 score 插入进去,而这也会降低写入性能。

2. skiplist

skiplist 编码的有序集合对象使用 zset 结构作为底层实现,一个 zset 结构包含一个哈希表和一个跳跃表:

typedef struct zset {
    // 字典表
    dict *dict;
    // 跳跃表
    zskiplist *zsl;
} zset;

跳跃表:

跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,达到快速访问节点的目的。不同的节点层高可能不一样,层数越高的节点越少,同一层的节点会使用指针串起来。跳跃表支持平均 O(logN) 复杂度的节点查找,还可以通过顺序性操作来批量处理节点。

zset 结构中的 zsl 跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存一个集合元素:跳跃表节点的 object 属性保存了元素的成员,而跳跃表节点的 score 属性则保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围型操作,比如 zrange、zrank 等命令。

  1. 跳表每一层都是有序的链表
  2. 跳表的查找次数近似于层数,时间复杂度O(log n),插入删除为O(log n)
    1. 上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到 O(logn)
  1. 最底层的链表包含所有元素
  2. 跳表是一种随机化的数据结构
  3. 跳表空间复杂度为O(n)
typedef struct zskiplist {
    // 跳表头节点和跳表尾节点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
} zskiplist;

typedef struct zskiplistNode {
    // 成员
    sds ele;
    // 分数
    double score;
    // 后退指针
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针,用于访问位于表尾方向的其他指针
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;
  • zskiplistLevel(层):每个层都带有两个属性:前进指针(forward)和跨度(span)。每次创建一个新跳跃表节点时,程序会随机生成一个介于 1~32 之间的值作为 level 数组的大小。
    • 前进指针(forward):指向表尾方向的其他指针,用于遍历跳跃表和快速访问。
    • 跨度(span)记录了前进指针所指向节点和当前节点的距离,节点跨度越大,相聚越远。跨度是用于计算排位的,在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,就是目标节点在跳跃表中的排位。
  • backward(后退指针):它指向位于当前节点的前一个节点,后退指针在程序从表尾向表头遍历时使用。跟一次可以跳过多个节点的前进指针不同,每个节点只有一个后退指针,所以每次只能后退至前一节点。

与红黑树比较

红黑树插入、删除节点时,是通过调整结构(左旋、右旋)来保持红黑树的平衡,比起跳跃表直接通过一个随机数来决定跨越几层,在时间复杂度的花销上要高于跳表

与二叉查找树比较

二叉查找树如果数据是有序的,且都大于根节点或都小于根节点,会出现极端情况偏向一边,这种情况会导致二叉查找树的时间复杂度降为O(n)。

字典表:

zset 结构中的 dict 字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典键保存元素的成员,而字典值则保存元素的分值。通过这个字典,程序可以用 O(1) 复杂度查找给定成员的分值,比如 zscore 命令。

有序集合每个元素的成员都是一个字符串对象,而每个元素的分值都是一个 double 类型的浮点数。值得一提的是,虽然 zset 结构同时使用跳表和字典来保存有序集合元素,但这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生任何重复成员或者分值,也不会因此而浪费额外的内存。

为什么有序集合需要同时使用跳跃表和字典来实现?

理论上,有序集合可以单独使用字典或跳跃表的其中一种数据结构来实现,但无论单独使用字典还是跳跃表,在性能上对比起同时使用字典和跳跃表都会有所降低。

如果我们只使用字典来实现有序集合,那么虽然以 O(1) 复杂度查找成员的分值这一特性会被保留,但因为字典以无序的方式来保存集合元素,所以执行范围型操作时,程序需要对字典保存的所有元素进行排序;但如果我们只使用跳跃表来实现有序集合,虽然跳跃表执行范围型操作的所有优点都会被保留,但根据成员查找分值这一操作的复杂度将从 O(1) 上升为 O(logN)。所以,为了让有序集合的查找和范围型操作都尽可快执行,Redis 选择了同时使用字典和跳跃表两种数据结构来实现有序集合。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值