引言

Redis是一个高性能的内存数据库,它可以存储不同类型的数据结构,如字符串、哈希、列表、集合和有序集合。但是,Redis的内存模型并不是简单地将数据放入内存中,而是涉及到一些复杂的细节,如内存分配器、对象编码、内存碎片等。本文将介绍Redis的内存模型的基本原理和应用,帮助读者更好地理解和使用Redis。

Redis内存统计

要了解Redis占用了多少内存,可以使用info memory命令,它会返回一些关于内存使用情况的参数,如下所示:

# Memory
used_memory:1048576
used_memory_human:1.00M
used_memory_rss:2097152
used_memory_rss_human:2.00M
used_memory_peak:1048576
used_memory_peak_human:1.00M
used_memory_peak_perc:100.00%
used_memory_overhead:0
used_memory_startup:0
used_memory_dataset:1048576
used_memory_dataset_perc:100.00%
allocator_allocated:1048576
allocator_active:2097152
allocator_resident:2097152
total_system_memory:8589934592
total_system_memory_human:8.00G
used_memory_lua:37888
used_memory_lua_human:37.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
allocator_frag_ratio:2.00
allocator_frag_bytes:1048576
allocator_rss_ratio:1.00
allocator_rss_bytes:0
rss_overhead_ratio:1.00
rss_overhead_bytes:0
mem_fragmentation_ratio:2.00
mem_fragmentation_bytes:1048576
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:49694
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0

其中,比较重要的几个参数的含义如下:

  • used_memory:Redis分配器分配的内存总量(单位是字节),包括使用的虚拟内存(即swap)。Redis分配器可以是libc、jemalloc或者tcmalloc,默认是jemalloc。
  • used_memory_rss:Redis进程占据操作系统的内存(单位是字节),与top及ps命令看到的值是一致的。除了分配器分配的内存之外,还包括进程运行本身需要的内存、内存碎片等,但是不包括虚拟内存。
  • mem_fragmentation_ratio:内存碎片比率,该值是used_memory_rss / used_memory的比值。该值一般大于1,且越大表示内存碎片比例越大。如果该值小于1,说明Redis使用了虚拟内存,这会降低Redis的性能,应该及时排查原因并处理。
  • mem_allocator:Redis使用的内存分配器,在编译时指定。jemalloc在控制内存碎片方面做得比较好。

Redis内存划分

Redis作为一个内存数据库,在内存中存储的内容主要是数据(键值对)。但是,除了数据以外,Redis还有其他部分也会占用内存。Redis的内存占用主要可以划分为以下几个部分:

  • 数据:作为数据库,数据是最主要的部分。这部分占用的内存会统计在used_memory中。Redis使用键值对存储数据,其中的值(对象)包括5种类型,即字符串、哈希、列表、集合和有序集合。这5种类型是Redis对外提供的,实际上,在Redis内部,每种类型可能有2种或更多的内部编码实现。此外,Redis在存储对象时,并不是直接将数据放入内存,而是会对对象进行各种包装:如redisObject、SDS等。这些细节将在后面介绍。
  • 进程本身运行需要的内存:Redis主进程本身运行肯定需要占用内存,如代码、常量池等等。这部分内存大约几兆,在大多数生产环境中与Redis数据占用的内存相比可以忽略。这部分内存不是由jemalloc分配,因此不会统计在used_memory中。除了主进程外,Redis创建的子进程运行也会占用内存,如Redis执行AOF、RDB重写时创建的子进程。当然,这部分内存不属于Redis进程,也不会统计在used_memoryused_memory_rss中。
  • 缓冲内存:缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等。其中,客户端缓冲区存储客户端连接的输入输出缓冲;复制积压缓冲区用于部分复制功能;AOF缓冲区用于在进行AOF重写时,保存最近的写入命令。这部分内存由jemalloc分配,因此会统计在used_memory中。
  • 内存碎片:内存碎片是Redis在分配、回收物理内存过程中产生的。例如,如果对数据的更改频繁,而且数据之间的大小相差很大,可能导致Redis释放的空间在物理内存中并没有释放,但Redis又无法有效利用,这就形成了内存碎片。内存碎片不会统计在used_memory中。内存碎片的产生与对数据进行的操作、数据的特点等都有关;此外,与使用的内存分配器也有关系:如果内存分配器设计合理,可以尽可能的减少内存碎片的产生。jemalloc便在控制内存碎片方面做得很好。如果Redis服务器中的内存碎片已经很大,可以通过安全重启的方式减小内存碎片:因为重启之后,Redis重新从备份文件中读取数据,在内存中进行重排,为每个数据重新选择合适的内存单元,减小内存碎片。

Redis数据结构及编码

redisObject

redisObject是Redis中最基本的对象类型,它是一个结构体,定义如下:

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;

其中:

  • type字段表示对象的类型,可以是字符串(REDIS_STRING)、哈希(REDIS_HASH)、列表(REDIS_LIST)、集合(REDIS_SET)或有序集合(REDIS_ZSET)。
  • encoding字段表示对象的编码方式,即对象在底层实际使用什么样的数据结构来保存数据。例如,字符串对象可以使用简单动态字符串(SDS)或整数来编码;列表对象可以使用链表或压缩列表来编码;哈希对象可以使用字典或压缩列表来编码;集合对象可以使用字典或整数集合来编码;有序集合对象可以使用跳跃表或压缩列表来编码。
  • lru字段表示对象最近一次被访问的时间戳(相对于全局LRU时钟),用于实现LRU算法;或者表示对象的访问频率和最近一次被访问的时间戳(最低8位为频率,最高16位为时间),用于实现LFU算法。这两种算法都是Redis用于淘汰内存中的对象的方法,它们各有优劣,具体如下:
    • LRU算法是最近最少使用算法,它根据对象的最近访问时间来决定对象的淘汰顺序,即越久没有被访问的对象越容易被淘汰。这种算法可以保证热点数据(经常被访问的数据)不会被淘汰,但是也可能导致冷数据(不经常被访问的数据)被过早地淘汰,从而降低缓存命中率。
    • LFU算法是最不经常使用算法,它根据对象的访问频率和最近访问时间来决定对象的淘汰顺序,即访问频率越低且最近访问时间越早的对象越容易被淘汰。这种算法可以保证热点数据和冷数据都有一定的保留机会,但是也可能导致一些突发流量(短时间内被大量访问的数据)占用过多的内存,从而影响内存利用率。
  • refcount字段表示对象的引用计数,用于实现对象的共享机制。当一个对象被创建时,它的引用计数为1;当一个对象被其他对象引用时,它的引用计数加1;当一个对象不再被其他对象引用时,它的引用计数减1;当一个对象的引用计数为0时,它会被释放。
  • ptr字段是一个指针,指向对象实际保存数据的地方。根据不同的编码方式,ptr可以指向不同的数据结构,如SDS、链表、字典、跳跃表等。

SDS

SDS(simple dynamic string)是Redis中字符串对象的默认编码方式,它是一个结构体,定义如下:

struct sdshdr {
    int len;
    int free;
    char buf[];
};

其中:

  • len字段表示字符串的长度,即buf数组中已使用的字节数。
  • free字段表示字符串的剩余空间,即buf数组中未使用的字节数。
  • buf字段是一个字符数组,用于存储字符串的内容。注意,这里使用了C99标准中的柔性数组(flexible array member),即数组长度不固定,可以根据需要动态分配。

SDS相比于C语言中的字符串有以下优点:

  • 常数复杂度获取字符串长度:由于SDS记录了字符串的长度,因此获取字符串长度只需要访问len字段,而不需要遍历整个字符串。
  • 防止缓冲区溢出:由于SDS记录了字符串的剩余空间,因此在进行字符串拼接或修改时,可以检查是否有足够的空间,如果没有,则进行扩容操作。而C语言中的字符串没有这样的机制,可能导致缓冲区溢出。
  • 减少修改字符串时带来的内存重分配次数:由于SDS在进行扩容操作时,并不是只分配刚好满足需求的空间,而是会预留一些额外的空间。这样,在下次修改字符串时,如果额外空间足够,则无需再次分配内存。SDS采用了不同的扩容策略,根据原有字符串长度和需要增加的长度来决定分配多少额外空间。具体如下:
    • 如果修改后的字符串长度小于1MB,则额外分配与修改后字符串长度相同大小的空间。
    • 如果修改后的字符串长度大于等于1MB,则额外分配1MB大小的空间。
    • 如果修改后的字符串长度超过了Redis最大内存限制,则不会进行扩容操作。
  • 二进制安全:由于SDS不以\0作为字符串结束标志,因此可以存储任意二进制数据,如图片、音频等。而C语言中的字符串以\0作为结束标志,因此不能存储包含\0的二进制数据。
  • 兼容部分C语言函数:由于SDS在buf数组末尾保留了一个\0字符(但不计入len和free),因此可以直接使用部分C语言函数来处理SDS,如printf等。但是,并不是所有C语言函数都适用于SDS,如strlen等。

链表

链表是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;

其中:

  • prevnext字段分别指向前一个节点和后一个节点,实现双向链接。
  • value字段是一个指针,指向节点的值。节点的值可以是任意类型的对象,如字符串、哈希、列表等。
  • headtail字段分别指向链表的头节点和尾节点,方便进行头尾插入或删除操作。
  • dupfreematch字段分别是函数指针,用于复制、释放和比较节点的值。这些函数可以根据不同的场景进行自定义,提高了链表的灵活性。
  • len字段表示链表的长度,即节点的数量。

链表相比于数组有以下优点:

  • 插入和删除操作快速:由于链表是由指针连接的,因此在插入或删除节点时,只需要修改相邻节点的指针即可,不需要移动其他节点。而数组在插入或删除元素时,需要移动后面的所有元素,效率较低。
  • 空间利用率高:由于链表是动态分配内存的,因此可以根据需要增加或减少节点,不会造成空间浪费。而数组是静态分配内存的,如果数组长度过大或过小,都会导致空间浪费或不足。

链表相比于数组有以下缺点:

  • 随机访问效率低:由于链表是由指针连接的,因此在访问某个节点时,需要从头或尾开始遍历整个链表,效率较低。而数组可以通过下标直接访问某个元素,效率较高。
  • 额外占用空间:由于链表需要为每个节点分配额外的空间来存储指针,因此会占用更多的内存。而数组只需要为元素本身分配空间即可。

压缩列表

压缩列表是Redis中列表、哈希和有序集合对象的一种编码方式,它是一种紧凑的顺序存储结构,定义如下:

typedef struct ziplist {
    uint32_t zlbytes; /* Total number of bytes in the ziplist */
    uint32_t zltail; /* Offset of the last entry */
    uint16_t zllen; /* Number of entries */
    unsigned char *zlend; /* End of ziplist marker */
    unsigned char *zldata; /* Pointer to the first entry */
} ziplist;

其中:

  • zlbytes字段表示压缩列表占用的总字节数,包括自身的大小。
  • zltail字段表示最后一个节点的偏移量,即从压缩列表的起始地址到最后一个节点的起始地址的距离。这样可以方便地从尾部开始遍历压缩列表。
  • zllen字段表示压缩列表中节点的数量。如果该值小于65535,则表示准确的节点数量;如果该值等于65535,则表示节点数量可能超过65535,需要遍历整个压缩列表才能得到准确的数量。
  • zlend字段表示压缩列表的结束标志,固定为一个字节,值为255(十六进制为0xFF)。如果遇到这个值,说明已经到达压缩列表的末尾。
  • zldata字段是一个指针,指向压缩列表中第一个节点的起始地址。

压缩列表中每个节点都由以下三部分组成:

  • previous entry length:前一个节点的长度,占用1字节或5字节。如果前一个节点的长度小于254(十六进制为0xFE),则占用1字节;如果前一个节点的长度大于等于254,则占用5字节,其中第1字节为254,后4字节为前一个节点的实际长度。这样可以方便地从后向前遍历压缩列表。
  • encoding:当前节点值的类型和长度,占用1字节或2字节或5字节。根据不同的类型和长度,有不同的编码方式。具体如下:
    • 如果当前节点值是字符串类型,并且长度小于64(十六进制为0x40),则使用1字节来编码,其中第1位为00,后6位为字符串长度。
    • 如果当前节点值是字符串类型,并且长度大于等于64并且小于16384(十六进制为0x4000),则使用2字节来编码,其中第1位和第2位为01,后14位为字符串长度。
    • 如果当前节点值是字符串类型,并且长度大于等于16384,则使用5字节来编码,其中第1字节为0x80,后4字节为字符串长度。
    • 如果当前节点值是整数类型,并且可以用4位来表示,则使用1字节来编码,其中第1位到第4位为1100,后4位为整数值。
    • 如果当前节点值是整数类型,并且可以用8位来表示,则使用2字节来编码,其中第1字节为0xC1,第2字节为整数值。
    • 如果当前节点值是整数类型,并且可以用16位来表示,则使用3字节来编码,其中第1字节为0xC2,后2字节为整数值。
    • 如果当前节点值是整数类型,并且可以用32位来表示,则使用5字节来编码,其中第1字节为0xC3,后4字节为整数值。
    • 如果当前节点值是整数类型,并且可以用64位来表示,则使用9字节来编码,其中第1字节为0xC4,后8字节为整数值。
  • content:当前节点值的内容。根据不同的类型和长度,占用不同的空间。如果是字符串类型,则占用与字符串长度相同的空间;如果是整数类型,则占用0字节,因为整数值已经存储在encoding中。

压缩列表相比于链表有以下优点:

  • 空间利用率高:由于压缩列表是一种紧凑的顺序存储结构,因此不需要为每个节点分配额外的空间来存储指针,也不需要为每个节点分配额外的空间来存储类型和长度信息。而链表需要为每个节点分配额外的空间来存储指针,还需要为每个节点的值分配额外的空间来存储redisObject结构体。
  • 缓存友好:由于压缩列表是一种顺序存储结构,因此在访问节点时,可以利用CPU缓存的局部性原理,提高访问效率。而链表是一种非顺序存储结构,因此在访问节点时,可能会导致缓存失效,降低访问效率。

压缩列表相比于链表有以下缺点:

  • 插入和删除操作效率低:由于压缩列表是一种顺序存储结构,因此在插入或删除节点时,可能需要移动后面的所有节点,效率较低。而链表在插入或删除节点时,只需要修改相邻节点的指针即可,效率较高。
  • 随机访问效率低:由于压缩列表没有记录每个节点的偏移量,因此在访问某个节点时,需要从头或尾开始遍历整个压缩列表,效率较低。而链表可以通过指针直接访问某个节点,效率较高。

由于压缩列表和链表各有优劣,因此Redis会根据不同的场景选择不同的编码方式。具体如下:

  • 列表对象:如果列表对象中所有字符串的长度都小于64字节,并且列表对象的长度小于512个节点,则使用压缩列表作为编码方式;否则使用链表作为编码方式。
  • 哈希对象:如果哈希对象中所有键和值的长度都小于64字节,并且哈希对象的长度小于512个键值对,则使用压缩列表作为编码方式;否则使用字典作为编码方式。
  • 有序集合对象:如果有序集合对象中所有元素的分数都可以用整数表示,并且所有元素的长度都小于64字节,并且有序集合对象的长度小于128个元素,则使用压缩列表作为编码方式;否则使用跳跃表和字典作为编码方式。

字典

字典是Redis中哈希和集合对象的一种编码方式,它是一种基于哈希表的键值对存储结构,定义如下:

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

其中:

  • type字段是一个指针,指向一个dictType结构体,该结构体定义了一些函数指针,用于对字典的键和值进行复制、释放、比较、哈希等操作。这些函数可以根据不同的场景进行自定义,提高了字典的灵活性。
  • privdata字段是一个指针,指向任意类型的数据,用于传递给type字段中的函数。这样可以在函数中使用一些额外的信息,如数据库号等。
  • ht字段是一个数组,包含两个dictht结构体,每个结构体都是一个哈希表。通常情况下,只使用ht[0]作为主要的哈希表,而ht[1]只在进行哈希表扩容或缩容时使用,作为渐进式rehash的目标哈希表。渐进式rehash是一种分多次将旧哈希表中的键值对迁移到新哈希表中的方法,可以避免一次性rehash造成的性能损失。
  • rehashidx字段表示当前正在进行渐进式rehash的索引位置。如果该值为-1,则表示没有进行渐进式rehash;如果该值大于等于0,则表示从旧哈希表中该位置开始的键值对需要被迁移到新哈希表中。
  • iterators字段表示当前正在运行的迭代器的数量。如果该值大于0,则表示不能进行渐进式rehash;如果该值等于0,则表示可以进行渐进式rehash。

dictht结构体定义如下:

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

其中:

  • table字段是一个指针数组,每个元素都是一个指针,指向一个dictEntry结构体,该结构体表示一个键值对节点。由于不同的键可能会映射到同一个索引位置,因此每个索引位置都可能有多个节点,形成一个链表。这就是哈希表中常见的解决冲突的方法:链地址法。
  • size字段表示哈希表的大小,即table数组的长度。该值总是等于2的幂次方。
  • sizemask字段表示哈希表大小掩码,即size - 1。该值用于计算键在哈希表中的索引位置,方法是将键经过哈希函数得到一个整数值,然后与sizemask进行按位与运算(bitwise AND),得到一个介于0和size - 1之间(包括两端)的整数,作为索引位置。
  • used字段表示哈希表已使用的节点数量。如果该值接近或等于size值,则说明哈希表需要进行扩容操作;如果该值远小于size值,则说明哈希表需要进行缩容操作。

dictEntry结构体定义如下:

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

其中:

  • key字段是一个指针,指向键对象。键对象可以是任意类型的对象,如字符串、整数等。
  • v字段是一个联合体,用于存储值对象。值对象可以是任意类型的对象,如字符串、整数、双精度浮点数等。如果值对象是一个指针,则使用val成员来存储;如果值对象是一个64位无符号整数,则使用u64成员来存储;如果值对象是一个64位有符号整数,则使用s64成员来存储;如果值对象是一个双精度浮点数,则使用d成员来存储。
  • next字段是一个指针,指向下一个键值对节点,用于解决哈希冲突。

字典相比于压缩列表有以下优点:

  • 随机访问效率高:由于字典使用哈希表作为底层实现,因此在访问某个键值对时,只需要计算键的哈希值和索引位置,然后在对应的链表中查找即可,效率较高。而压缩列表在访问某个节点时,需要从头或尾开始遍历整个压缩列表,效率较低。
  • 插入和删除操作效率高:由于字典使用链地址法来解决哈希冲突,因此在插入或删除键值对时,只需要修改对应的链表即可,不需要移动其他节点。而压缩列表在插入或删除节点时,可能需要移动后面的所有节点,效率较低。

字典相比于压缩列表有以下缺点:

  • 空间利用率低:由于字典需要为每个键值对分配额外的空间来存储指针和类型信息,还需要为哈希表分配额外的空间来存储索引数组,因此会占用更多的内存。而压缩列表不需要为每个节点分配额外的空间来存储指针和类型信息,也不需要为顺序存储结构分配额外的空间来存储索引数组,因此占用更少的内存。
  • 缓存不友好:由于字典使用非顺序存储结构,因此在访问键值对时,可能会导致缓存失效,降低访问效率。而压缩列表使用顺序存储结构,因此在访问节点时,可以利用CPU缓存的局部性原理,提高访问效率。

整数集合

整数集合是Redis中集合对象的一种编码方式,它是一种有序的整数数组,定义如下:

typedef struct intset {
    uint32_t encoding; /* The encoding used for the elements */
    uint32_t length; /* The number of elements in the intset */
    int8_t contents[]; /* The actual elements, in sorted order */
} intset;

其中:

  • encoding字段表示整数集合中元素的编码方式,即每个元素占用的字节数。该值可以是2的幂次方,如1、2、4、8等。根据不同的编码方式,contents数组中的元素可以是int8_t、int16_t、int32_t或int64_t类型。
  • length字段表示整数集合中元素的数量,即contents数组的长度。
  • contents字段是一个数组,用于存储整数集合中的元素。该数组中的元素是按照从小到大的顺序排列的,且不重复。这样可以方便地进行二分查找和插入操作。

整数集合相比于字典有以下优点:

  • 空间利用率高:由于整数集合是一种紧凑的顺序存储结构,因此不需要为每个元素分配额外的空间来存储指针和类型信息,也不需要为哈希表分配额外的空间来存储索引数组,因此占用更少的内存。而字典需要为每个键值对分配额外的空间来存储指针和类型信息,还需要为哈希表分配额外的空间来存储索引数组,因此占用更多的内存。
  • 缓存友好:由于整数集合是一种顺序存储结构,因此在访问元素时,可以利用CPU缓存的局部性原理,提高访问效率。而字典使用非顺序存储结构,因此在访问键值对时,可能会导致缓存失效,降低访问效率。

整数集合相比于字典有以下缺点:

  • 随机访问效率低:由于整数集合没有记录每个元素的偏移量,因此在访问某个元素时,需要从头或尾开始遍历整个整数集合,或者使用二分查找法,在最坏情况下需要遍历logN次,效率较低。而字典可以通过计算键的哈希值和索引位置,然后在对应的链表中查找即可,效率较高。
  • 插入和删除操作效率低:由于整数集合是一种顺序存储结构,并且要求元素有序且不重复,因此在插入或删除元素时,可能需要移动后面的所有元素,效率较低。而字典使用链地址法来解决哈希冲突,因此在插入或删除键值对时,只需要修改对应的链表即可,不需要移动其他节点。

由于整数集合和字典各有优劣,因此Redis会根据不同的场景选择不同的编码方式。具体如下:

  • 集合对象:如果集合对象中所有元素都是整数,并且集合对象的长度小于512个元素,则使用整数集合作为编码方式;否则使用字典作为编码方式。

跳跃表

跳跃表是Redis中有序集合对象的一种编码方式,它是一种基于多层链表的有序数据结构,定义如下:

typedef struct zskiplistNode {
    struct zskiplistLevel {
        struct zskiplistNode *forward; /* Pointer to the next node */
        unsigned int span; /* The number of nodes between the current node and the next node */
    } level[]; /* The level array, variable length */
    struct zskiplistNode *backward; /* Pointer to the previous node */
    double score; /* The score of the element, used for sorting */
    robj *obj; /* The element object */
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header; /* The header node of the skiplist */
    struct zskiplistNode *tail; /* The tail node of the skiplist */
    unsigned long length; /* The number of elements in the skiplist */
    int level; /* The current level of the skiplist */
} zskiplist;

其中:

  • forward字段是一个指针,指向当前层的下一个节点。
  • span字段表示当前层的跨度,即当前节点和下一个节点之间的节点数量。这样可以方便地计算排名和区间。
  • level字段是一个数组,表示当前节点的层数。数组的长度是可变的,根据随机算法来决定。每个元素都是一个zskiplistLevel结构体,表示当前节点在某一层的信息。
  • backward字段是一个指针,指向前一个节点。这样可以方便地从后向前遍历跳跃表。
  • score字段表示元素的分数,用于对元素进行排序。分数可以是任意的双精度浮点数,相同分数的元素按照字典序进行排序。
  • obj字段是一个指针,指向元素对象。元素对象可以是任意类型的对象,如字符串、整数等。
  • header字段是一个指针,指向跳跃表的头节点。头节点不存储任何元素,只用于方便地访问跳跃表的各个层。
  • tail字段是一个指针,指向跳跃表的尾节点。尾节点存储最大分数的元素,只用于方便地从后向前遍历跳跃表。
  • length字段表示跳跃表中元素的数量,即除了头节点以外的节点数量。
  • level字段表示跳跃表的当前层数。该值总是小于等于32。

跳跃表相比于压缩列表有以下优点:

  • 随机访问效率高:由于跳跃表是一种多层链表结构,因此在访问某个元素时,可以从高层开始查找,然后逐渐降低层级,直到找到目标元素,效率较高。而压缩列表在访问某个节点时,需要从头或尾开始遍历整个压缩列表,效率较低。
  • 插入和删除操作效率高:由于跳跃表是一种有序结构,并且每个节点都记录了当前层的跨度信息,因此在插入或删除元素时,只需要修改相关节点的指针和跨度即可,不需要移动其他节点。而压缩列表在插入或删除节点时,可能需要移动后面的所有节点,效率较低。

跳跃表相比于压缩列表有以下缺点:

  • 空间利用率低:由于跳跃表需要为每个节点分配额外的空间来存储指针和跨度信息,并且每个节点可能有多个指针和跨度信息(根据随机算法决定),因此会占用更多的内存。而压缩列表不需要为每个节点分配额外的空间来存储指针和跨度信息,因此占用更少的内存。
  • 缓存不友好:由于跳跃表使用非顺序存储结构,因此在访问元素时,可能会导致缓存失效,降低访问效率。而压缩列表使用顺序存储结构,因此在访问节点时,可以利用CPU缓存的局部性原理,提高访问效率。

总结

Redis是一个高性能的内存数据库,它可以存储不同类型的数据结构,如字符串、哈希、列表、集合和有序集合。为了实现这些数据结构,Redis使用了一些特殊的内存模型,如redisObject、SDS、链表、压缩列表、字典、整数集合和跳跃表等。这些内存模型都有各自的优劣,因此Redis会根据不同的场景选择不同的编码方式,以达到最佳的性能和空间利用率。本文介绍了Redis的内存模型的基本原理和应用,希望对读者有所帮助。如果你想了解更多关于Redis的内容,你可以访问Redis官网或者Redis中文网。谢谢你的阅读!😊