Redis 底层数据存储结构

Redis 系列笔记:

第一篇:Redis 基础命令
第二篇:Redis 常见应用场景
第三篇:Redis Cluster集群搭建
第四篇:Redis 主从及哨兵搭建
第五篇:Redis 主从及集群
第六篇:Redis 持久化
第七篇:Redis 分布式锁
第八篇:Redis 底层数据存储结构
第九篇:Redis 面试常问问题



前言

Redis的性能高的原因之一是它每种数据结构都是经过专门设计的,并都有一种或多种数据结构来支持,依赖这些灵活的数据结构,来提升读取和写入的性能,接下来一起了解一下它的数据存储原理。

参考: Redis底层数据结构(图文详解)


提示:以下是本篇文章正文内容,下面案例可供参考

一、Redis的数据是怎么存储的

Redis是一种存储key-value的内存型数据库,它的key都是字符串类型,value支持存储5种类型的数据:String(字符串类型)、List(列表类型)、Hash(哈希表类型、即key-value类型)、Set(无序集合类型,元素不可重复)、Zset(有序集合类型,元素不可重复)。

从Redis内部实现的角度来看,key-value这个映射关系是用一个 dict 来维护的。

1. hash算法

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

#define dictHashKey(d, key) (d)->type->hashFunction(key)
unsigned int h;
dictEntry *he;
h = dictHashKey(ht, key) & ht->sizemask;
he = ht->table[h];

2. hash冲突

如果两个不同的键值经过hash算法计算后,得到了相同的索引值,则说明键冲突了。redis使用拉链法解决冲突。每个节点有一个next指针,新的键值对冲突时,则将新的键值对dictEntry放在单链表的首部(注意不是尾部)。

解决办法: 哈希表的每个节点都有一个 next 指针, 多个节点可以用 next 指针构成一个单向链表, 这就解决了键冲突的问题。为了执行效率没有尾节点指针,新节点将添加到链表的表头位置(复杂度为 O(1)), 排在其他已有节点的前面。

3. rehash

随着操作的不断执行, 哈希表保存的键值对会逐渐地增多或者减少, 为了让哈希表的 负载因子 维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩。

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

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

1、服务器目前没有在执行 bgsave 命令或者 bgrewriteaof 命令、并且哈希表的负载因子大于等于1
 
2、服务器目前正在执行 bgsave 命令或者 bgrewriteaof 命令并且哈希表的负载因子大于等于5

rehash步骤:

1、重新为ht[1]分配空间,大小取决于ht[0].used的值。

  • 扩容:ht[1]大小为(2n) * ht[0].used。
    • 假设 当前的键值对数量为7,那么 7 * 2 = 14,首个大于等于14的数,并且是2的n次幂,这个数就是16 = 24,所以ht[1]的大小就是16。
  • 收缩:ht[1]大小为2n,这个2n是第一个大于ht[0].used的2的n次幂。
     

2、将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面:

  • rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。
     

3、当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。

4. 渐进式rehash

上面的rehash是一次性、集中式地完成的,如果哈希表里保存的键值对数量很多的话,一次性将这些键值对全部 rehash 到 ht[1]上,庞大的计算量可能会导致服务器在一段时间内停止服务。

渐进式rehash 是分多次、渐进式地完成的,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上。这样就很巧妙地将一次性大量拷贝的开销,分摊到多次处理请求的过程中了,避免了耗时操作,保证了数据的快速访问。

渐进式rehash 步骤:

1、为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
 

2、在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
 
3、在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
 
4、随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找。

另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。

5.dict结构图


dict的key固定用一种数据结构来表达就够了,这就是动态字符串SDS(simple dynamic string)。而value则比较复杂,为了在同一个dict内能够存储不同类型的value,这就需要一个通用的数据结构,这个通用的数据结构就是robj(全名是redisObject)。

举个例子:如果value是一个list,那么它的内部存储结构是一个quicklist(quicklist的具体实现我们放在后面的文章讨论);如果value是一个string,那么它的内部存储结构一般情况下是一个sds。当然实际情况更复杂一点,比如一个string类型的value,如果它的值是一个数字,那么Redis内部还会把它转成long型来存储,从而减小内存使用。而一个robj既能表示一个sds,也能表示一个quicklist,甚至还能表示一个long型。

参考:Redis内部数据结构详解(3)——robj

二、RedisObject对象解析

1. RedisObject结构

typedef struct redisObject{
    //类型
    unsigned type:4; // 
 
    //编码
    unsigned encoding:4;
 
    //对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS
 
    //引用计数
    int refcount
 
    //指向底层实现数据结构的指针
    void *ptr;.. 
} 

type: 对象的数据类型。占4个bit。可能的取值有5种(分别对应Redis对外暴露的5种数据结构):

type类型
REDIS_STRING字符串
REDIS_LIST列表
REDIS_SET无序集合
REDIS_ZSET有序集合
REDIS_HASH哈希

encoding: 对象的内部表示方式(也可以称为编码)。占4个bit。可能的取值有10种。

  • OBJ_ENCODING_RAW: 最原生的表示方式。其实只有string类型才会用这个encoding值(表示成sds)。
编码常量(encoding )编码对应的底层数据结构
REDIS_ENCODING_INTlong 类型的整数
REDIS_ENCODING_EMBSTRembstr (编码的简单动态字符串,一种特殊的嵌入式的sds)
REDIS_ENCODING_RAW简单动态字符串
REDIS_ENCODING_LINKEDLIST双端链表
REDIS_ENCODING_ZIPLIST压缩列表
REDIS_ENCODING_HTdict(字典)
REDIS_ENCODING_INTSET整数集合,用于set数据结构
REDIS_ENCODING_SKIPLIST跳跃表和字典,用于sorted set数据结构。
REDIS_ENCODING_QUICKLIST快速列表
REDIS_ENCODING_STREAM

lru: 做LRU替换算法用,占24个bit。这个不是我们这里讨论的重点,暂时忽略。
refcount: 引用计数。它允许robj对象在某些情况下被共享。
ptr: 数据指针。指向真正的数据。

  • 比如,一个代表string的robj,它的ptr可能指向一个sds结构;一个代表list的robj,它的ptr可能指向一个quicklist。


我们来总结一下robj的作用:

  • 为多种数据类型提供一种统一的表示方式。
  • 允许同一类型的数据采用不同的内部表示,从而在某些情况下尽量节省内存。
  • 支持对象共享和引用计数。当对象被共享的时候,只占用一份内存拷贝,进一步节省内存。

2. 不同数据类型存储方式

详细代码分析可参考B站视频学习

每一种redisObject对象对应底层都会有至少2种数据结构

类型编码对象
Stringint整数值实现
Stringembstrsds实现 <=39 字节
Stringrawsds实现 > 39字节
Hashziplist压缩列表实现
Hashhashtable字典使用
Listziplist压缩列表实现
Listlinkedlist双端链表实现
Setintset整数集合使用
Sethashtable字典实现
Sorted setziplist压缩列表实现
Sorted setskiplist跳跃表和字典

2.1 String

String的编码方式有三种:longembstrraw。其中embstrraw 都会使用SDS来保存值,但不同之处在于embstr会通过一次内存分配函数来分配一块连续的内存空间来保存 redisObjectSDS

内部编码条件备注
int满足long取值范围,也就是-9223372036854775808 ~ 9223372036854775807之间如果设置字符串为数组类型操作long的范围,小于44字节。比如值为9223372036854775808则类型会变为embstr
embstr非数组类型,若为数字。则不在long取值范围。且小于44字节。redis 3.2之前则小于39如果大于44字节,则会变为raw类型,连续内存。注:redis3.2版本后,3.2版本中是39字节redis中embstr与raw编码方式之间的界限
raw大于44字节。redis3.2之后满足等于或大于45字节,非连续内存。

为什么是44字节:
假设系统是64位,SDS中没有任何数据的情况下,emstrredisObject 就需要消耗的字节数就有16字节,sdshdr8中的lenallocflags 各占一个字节,最后的buf的结束符’\0’所占用一字节,共16+3+1 = 20字节,剩余 64-20=44字节,所以最终embstr能存储的字符串最大为44字节。
 

embstr和sds的区别在于内存的申请和回收:

  • embstr的创建只需分配一次内存,而raw为两次(一次为sds分配对象,另一次为redisObject分配对象,embstr省去了第一次)。相对地,释放内存的次数也由两次变为一次。
  • embstr的redisObject和sds放在一起,更好地利用缓存带来的优势
  • 缺点:redis并未提供任何修改embstr的方式,即embstr是只读的形式。对embstr的修改实际上是先转换为raw再进行修改

1、int

2、embstr

3、raw

4、sds源码

String 类型的SDS源码实现,redis3.2以后源码 :

// sdshdr5已经弃用,会自动转换成sdshdr8
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
	// buf 存的字符串长度已占用的长度
    uint8_t len; /* used */
	// buf 中未被占用的长度
    uint8_t alloc; /* excluding the header and null terminator */
	// 标志位,用来表明这个是sdshdr几
    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[];
};

SDS的优点:

  1. 获取字符串长度复杂度O(1): 当查询字符串长度的时候,直接返回该字段的值,而不用通过遍历字符数组得到,查询时间复杂度为O(1)。设置和更新 SDS 长度的工作是由 SDS 的 API 在执行时自动完成的,使用 SDS 无须进行任何手动修改长度的工作。

 

  1. **空间预分配:**当 SDS 的 API 对一个 SDS 进行修改,修改后的 SDS 的字符串小于1MB时(也就是 len 的长度小于1MB),那么程序会分配与len属性相同大小的未使用空间(就是再给未使用空间 alloc 也分配与 len 相同的空间);如果修改后SDS 大于1MB时(也就是len的长度大于等于1MB),那么程序会分配1MB的未使用空间。 【通过空间预分配操作,redis有效的减少了执行字符串增长所需要的内存分配次数】

    • 例:字符串大小为600k,那么会分配600k给这个字符串使用,再分配600k的alloc空间在那。
    • 例:字符串大小为3MB,那么会分配3MB给这个字符串使用,再分配1MB的alloc空间在那。
       
  2. 惰性空间释放: 当缩短 SDS 的存储内容时,并不会立即使用内存重分配来回收字符串缩短后的空间,而是通过alloc将空闲的空间记录起来,等待将来使用。真正需要释放内存的时候,通过调用api来释放内存。

    • 释放 未使用空间(alloc) 的条件是,alloc > 10% * len,这样的目的就是为了防止多次 apend 这样的操作导致的多次 realloc调用。
       

注意:redis3.2 之前的版本使用字段叫做 free,表示未使用空间长度;而 redis3.2 以及之后的版本已经改成了 alloc 字段,表示分配的总空间大小`

参考: redis之SDS字符串,到底高效在哪里?(全面分析)

2.2 Hash

Hash 底层实现采用了 ziplisthashtable 两种实现方式。ziplist (压缩链表) 适用于长度较小的值,因为他是由连续的空间实现的。存取的效率高,内存占用小,但由于内存是连续的,在修改的时候要重新分配内存。在数据量比较小的时候使用的是 ziplist。当 hash 对象同时满足以下两个条件是,使用的 ziplist 编码。

内部编码条件备注
zipListhash 中存储的所有元素的 key 和 value 的长度都小于 64byte
hash 中存储的元素个数小于 512
通过修改 hash-max-ziplist-value 配置调节大小
通过修改 hash-max-ziplist-entries 配置调节大小
hashtable上面两个条件一旦有一个条件不满足时就会被转码为 hashtable进行存储在不满足条件时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。
1、ziplist

由于压缩列表基于内存特殊编码实现,源码种并没有数据结构代码定义,但可以通过上文的宏定义函数,结合压缩列表的创建函数来推出压缩列表的大致组成部分。ziplist创建函数:

/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
	// 为压缩列表申请bytes字节空间
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
    unsigned char *zl = zmalloc(bytes);
    // 默认小端模式存储 如果在使用大端存储的机器上运行 需要大小端转换 详见计组
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    // 设置len属性为0 len表示ziplist中节点数量
    ZIPLIST_LENGTH(zl) = 0;
    // 设置ziplist结尾标志0xFF
    zl[bytes-1] = ZIP_END;
    return zl;
}

ziplist中列表节点定义:

typedef struct zlentry {
	// 用于记录内存编码后前一个entry的len占用了多少字节 即prevrawlen占用了多少字节
    unsigned int prevrawlensize; 
    // 用于记录前一个entry占用的长度 
    // 通过当前entry地址 - prevrawlen可以寻找到上一个entry的首地址
    unsigned int prevrawlen;     
    // 用于记录内存编码后当前entry的len占用了多少字节
    unsigned int lensize;        
    // 用于记录当前entry的长度
    unsigned int len;
    // 用于记录当前entry的header大小 即lensize + prevrawsize  
    unsigned int headersize;     
    // 用于记录当前节点编码格式
    unsigned char encoding;
    // 指向当前节点的指针      
    unsigned char *p;       
} zlentry;

在这里插入图片描述
ziplistentry 参数解析:
len :

  • 当前一个节点长度小于254字节时,直接将前一节点存储在当前字节中
  • 当前一个节点长度大于等于254字节时,将prevrawlen的第一个字节设为254,后四个字节用于保存前一节点长度(因为unsigned int固定4字节)。

encoding: entry 被设置允许存放字符串和整数类型数据,由于压缩列表的主要目的是尽量节省空间,所以字符串和整数类型的数据有也自然有他的压缩(编码)方式。存储不同类型不同长度的数据所选用的编码方式也随之改变。
字符串编码

编码编码长度实际存储的数据
00xxxxxx1字节长度小于等于63字节的字节数组
01xxxxxx xxxxxxxx2字节长度小于等于16383字节的字节数组
10_ _ _ _ _ _ xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx5字节长度小于4294 967 295字节的字节数组

整数编码

编码编码长度实际存储的数据
110000001字节int16_t类型的整数
110100001字节int32_t类型的整数
111000001字节int64_t类型的整数
111100001字节24位有符号整数
111111101字节8位有符好整数
1111xxxx1字节encoding的低四位直接保存一个0~12的整数

参考:Redis 5.0数据结构之压缩列表ziplist源码详解

2、hashtable

dict 字典的底层就是通过hashtable来实现的。
源码:

/*
 * dict 字典
 * 大家需要关注的是dictht ht[2]:
 * 这里设计存储两个dictht 的指针是用于Redis的rehash,后文中进行详解
 */
typedef struct dict {
    dictType *type;			/*类型特定函数*/
    void *privdata;			/*私有数据*/
    dictht ht[2];			/*用于存储数据的两个hash表,正常只有一个hash表中有数据,只有在rehash的过程中才会出现两个hash表同时存在数据*/
    long rehashidx; 		/*rehash目前进度,当哈希表进行rehash的时候用到,其他情况下为-1*/
    unsigned long iterators; /*迭代器数量*/
} dict;

/* 
 * 这是我们的哈希表结构。 每个字典都有两个
 * 一个哈希表里面有多个哈希表节点(dictEntry),每个节点表示字典的一个键值对
 */
typedef struct dictht {
    dictEntry **table;		/*哈希表数组指针*/
    unsigned long size; 	/*hashtable 容量 数组大小*/
    unsigned long sizemask;	/*size -1*/
    unsigned long used;		/*hashtable中元素个数,正常情况下当used/size=1时将进行扩容操作*/
} dictht;

/* 
 * 哈希表节点
 */
typedef struct dictEntry {
    void *key;
    union {
        void *val;		/*指向Value值的指针,正常是指向一个redisObject*/
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;	/*当出现hash冲突时使用链表形式保存hashcode相等但是field 不相等的数据,这里就是指向下一条数据的指针*/
} dictEntry;

注:这里的dict具体结构是上面那种还是其他结构还不清楚,欢迎留言补充。

2.3 List

List 列表类型的内部编码3.2 版本之前有两种:ziplist(压缩列表) 和 linkedlist(双向链表),3.2 版本之后,重新引入了一个 quicklist(快速列表) 的数据结构,List 底层都由 quicklist 实现。

内部编码条件备注
ziplistHash 中存储的所有元素的长度都小于 64byte,且存储的元素个数小于 512通过修改 hash-max-ziplist-entries,hash-max-ziplist-entries配置调节大小
linkedlist当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。双向链表便于在表的两端进行 push 和 pop 操作,在插入节点上复杂度很低,但是它的内存开销比较大。
首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
quicklistzipList和linkedList的混合体,是将linkedList按段切分,每一段用zipList来紧凑存储,多个zipList之间使用双向指针链接。quicklist 其实就是综合考虑了时间和空间效率引入的新型数据结构。(使用 ziplist 能提高空间的使用率,使用 linkedlist 能够降低插入元素时的时间)

quicklist 极端情况:

  • 1、当 ziplist 节点过多的时候,quicklist 就会退化为双向链表。效率较差;效率最差时,一个 ziplist 中只包含一个 entry,即只有一个元素的双向链表。(增加了查询的时间复杂度)
  • 2、当 ziplist 元素个数过少时,quicklist 就会退化成为 ziplist,最极端的时候,就是 quicklist 中只有一个 ziplist 节点。(当增加数据时,ziplist 需要重新分配空间)
1、ziplist

参考上面hash的ziplist,点击跳转

2、linkedlist

在使用redis的list数据结构时,存储数据较大时,list对象已经不满足上面描述的ziplist条件,则会使用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;

结构图:

3、quicklist

结构源码:

typedef struct quicklist {
    quicklistNode *head;						/* 指向双向链表的表头 */
    quicklistNode *tail;						/* 指向双向链表的表头 */
    unsigned long count;        				/* 所有的ziplist中一共存了多少个元素,即ziplist中的entry个数 */ /* total count of all entries in all ziplists */
    unsigned long len;          				/* 双向链表的长度,quicklistNode的数量 */ /* number of quicklistNodes */
    int fill : QL_FILL_BITS;              		/* ziplist最大大小,对应list-max-ziplist-size */ /* fill factor for individual nodes */
    unsigned int compress : QL_COMP_BITS; 		/* 压缩深度,对应list-compress-depth */ /* depth of end nodes not to compress;0=off */
    unsigned int bookmark_count: QL_BM_BITS;	/* 4位,bookmarks数组的大小 */
    quicklistBookmark bookmarks[];				/* bookmarks是一个可选字段,quicklist重新分配内存空间时使用,不使用时不占用空间 */
} quicklist;

快速列表节点源码:

typedef struct quicklistNode {
    struct quicklistNode *prev;				/* 指向前一个节点 */
    struct quicklistNode *next;				/* 指向后一个节点 */
    unsigned char *zl;						/* 指向实际的ziplist */
    unsigned int sz;             			/* 当前ziplist占用多少字节 */ /* ziplist size in bytes */
    unsigned int count : 16;     			/* 当前ziplist中存储了多少个元素,占16bit(下同),最大65536个 */ /* count of items in ziplist */
    unsigned int encoding : 2;   			/* 是否采用了LZF压缩算法压缩节点 */ /* RAW==1 or LZF==2 */
    unsigned int container : 2;  			/* 2:ziplist,未来可能支持其它存储结构 */ /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; 			/* 当前ziplist是否已经被解压出来作临时使用 */ /* was this node previous compressed? */
    unsigned int attempted_compress : 1; 	/* 测试用 */ /* node can't compress; too small */
    unsigned int extra : 10; 				/* 预留给未来使用 */ /* more bits to steal for future usage */
} quicklistNode;

结构图:

quicklist 的特点:

1.是一个节点为 ziplist 的双端链表
2.节点采用了 ziplist ,解决了传统链表的内存占用问题
3.能控制 ziplist 的大小,解决连续内存空间申请的效率问题
4.中间节点可以进行压缩,进一步节省了内存空间

2.4 Set

set 的内部编码有 intset(整数集合)整数和 hashtable(哈希表) 两种实现方式:

内部编码条件备注
intset当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时
hashtable当集合类型无法满足 intset 的条件时,会使用 hashtable 作为集合的内部实现
1、intset

intset 内部其实是一个数组(int8_t coentents[]数组),而且存储数据的时候是有序的,因为在查找数据的时候是通过二分查找来实现的。
结构源码:

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

contents 虽然声明为 int8_t 类型,但它实际上并不保存任何 int8_t 类型的值, contents 数组的实际类型取决于 encoding 属性的值:

编码encoding值备注
int16_tINTSET_ENC_INT_6contents存储 int16_t 类型的值
int32_tINTSET_ENC_INT_6contents存储 int16_t 类型的值
int64_tINTSET_ENC_INT_6contents存储 int16_t 类型的值

结构图:

2、hashtable

参考上面hash的hashtable,点击跳转

2.5 Sorted set

set 的内部编码有 ziplist(压缩列表)整数和 skiplist(跳跃表) 两种实现方式:

内部编码条件备注
ziplist当有序集合中所有元素的 key 和 value 的长度都小于 64byte
Hash 中存储的元素个数小于 512
配置项参考上面 hash介绍
skiplist当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时zip的读写效率会下降
1、ziplist

参考上面hash的ziplist,点击跳转
结构图:

2、skiplist

源码:

/* 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;

typedef struct zset {
    dict *dict; //跳表中的所有键值对
    zskiplist *zsl;
} zset;

结构图:
在这里插入图片描述
跳表原理可参考:Redis源码解析:数据结构详解-skiplist


总结

以上我根据源码和图片简单讲解了一下redis的5种数据类型的底层结构,还有一些对数据的修改操作没有讲到,想要具体了解就去看看源码。后面还会陆续补充一些内容。如:redis的rehash渐进式hash

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值