redis底层结构

SDS 简单动态字符串

每个 sds.h/sdshdr 结构表示一个 SDS 值:

struct sdshdr {

    // 记录 buf 数组中已使用字节的数量
    // 等于 SDS 所保存字符串的长度
    int len;
    // 记录 buf 数组中未使用字节的数量
    int free;
    // 字节数组,用于保存字符串
    char buf[];
};

为什么不使用c语言简单字符串

原因有如下

获取长度复杂度为O(1)
避免缓冲区溢出
优化策略

redis非常注重性能,于是优化是非常必要的。

减少内存重分配
空间预分配
内存的分配设计到复杂算法或可能的系统调用,比较耗时

于是,每次再字符串增长操作时,不仅仅会扩容到刚好所需的空间,而是会通过一定策略额外分配空间

惰性空间释放
字符串的缩短操作,不会进行内存重分配,而是会通过free记录起来,为未来可能存在的增长提供优化

2.链表

typedef struct listNode {

    // 前置节点
    struct listNode *prev;

    // 后置节点
    struct listNode *next;

    // 节点的值
    void *value;

} listNode;

还有有一个头节点指向表头和表尾

typedef struct listNode {	

	listNode *prev;

	listNode *next;

	void *value;

}listNode;

typedef struct list{

	listNode *head;

	listNode *tail;

	unsigned  long len;

}list;

字典

/*
* 字典
** 每个字典使用两个哈希表,用于实现渐进式 rehash
*/
typedef struct dict {
// 特定于类型的处理函数
dictType *type;
// 类型处理函数的私有数据
void *privdata;
// 哈希表(2 个)
dictht ht[2];
// 记录 rehash 进度的标志,值为-1 表示 rehash 未进行
int rehashidx;
// 当前正在运作的安全迭代器数量
int iterators;
} dict;
/*
* 哈希表
*/
typedef struct dictht {
// 哈希表节点指针数组(俗称桶,bucket)
dictEntry **table;
// 指针数组的大小
unsigned long size;
// 指针数组的长度掩码,用于计算索引值
unsigned long sizemask;
// 哈希表现有的节点数量
unsigned long used;
} dictht;

table 属性是一个数组,数组的每个元素都是一个指向 dictEntry 结构的指针。
每个 dictEntry 都保存着一个键值对,以及一个指向另一个 dictEntry 结构的指针:

/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 链往后继节点
struct dictEntry *next;
} dictEntry;
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 链往后继节点
struct dictEntry *next;
} dictEntry;

next 属性指向另一个 dictEntry 结构,多个 dictEntry 可以通过 next 指针串连成链表,从
这里可以看出,dictht 使用链地址法来处理键碰撞:当多个不同的键拥有相同的哈希值时,哈
希表用一个链表将这些键连接起来。
整个字典结构可以表示如下:
字典
1.新创建的两个哈希表都没有为 table 属性分配任何空间:
• ht[0]->table 的空间分配将在第一次往字典添加键值对时进行;
• ht[1]->table 的空间分配将在 rehash 开始时进行;
2.根据字典所处的状态,将一个给定的键值对添加到字典可能会引起一系列复杂的操作:
• 如果字典为未初始化(也即是字典的 0 号哈希表的 table 属性为空),那么程序需要对 0
号哈希表进行初始化;
• 如果在插入时发生了键碰撞,那么程序需要处理碰撞;
• 如果插入新元素使得字典满足了 rehash 条件,那么需要启动相应的 rehash 程序;
添加键的示意图

跳跃表

跳跃表
表头(head):负责维护跳跃表的节点指针。
• 跳跃表节点:保存着元素值,以及多个层。
• 层:保存着指向其他元素的指针。高层的指针越过的元素数量大于等于低层的指针,为了
提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小,慢慢降低层
次。
• 表尾:全部由 NULL 组成,表示跳跃表的末尾

typedef struct zskiplist {
// 头节点,尾节点
struct zskiplistNode *header, *tail;
// 节点数量
unsigned long length;
// 目前表内节点的最大层数
int level;
} zskiplist;
typedef struct zskiplistNode {
// member 对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 这个层跨越的节点数量
unsigned int span;
} level[];
} zskiplistNode;

整数集合

整数集合(intset)用于有序、无重复地保存多个整数值,它会根据元素的值,自动选择该用什
么长度的整数类型来保存元素。
举个例子,如果在一个 intset 里面,最长的元素可以用 int16_t 类型来保存,那么这个 intset
的所有元素都以 int16_t 类型来保存。
另一方面,如果有一个新元素要加入到这个 intset ,并且这个元素不能用 int16_t 类型来保存
——比如说,新元素的长度为 int32_t ,那么这个 intset 就会自动进行“升级” :先将集合中现
有的所有元素从 int16_t 类型转换为 int32_t 类型,接着再将新元素加入到集合中。
根据需要,intset 可以自动从 int16_t 升级到 int32_t 或 int64_t ,或者从 int32_t 升级到
int64_t 。
整数集合

压缩列表

Ziplist 是由一系列特殊编码的内存块构成的列表,一个 ziplist 可以包含多个节点(entry),每
个节点可以保存一个长度受限的字符数组(不以 \0 结尾的 char 数组)或者整数,包括:
• 字符数组
– 长度小于等于 63 (26 − 1)字节的字符数组
– 长度小于等于 16383 (2
14 − 1)字节的字符数组
– 长度小于等于 4294967295 (2
32 − 1)字节的字符数组
• 整数
– 4 位长,介于 0 至 12 之间的无符号整数
– 1 字节长,有符号整数
– 3 字节长,有符号整数
– int16_t 类型整数
– int32_t 类型整数
– int64_t 类型整数
因为 ziplist 节约内存的性质,它被哈希键、列表键和有序集合键作为初始化的底层实现来使用

ziplist的构成
ziplistnode
entry
pre_entry_length 记录了前一个节点的长度,通过这个值,可以进行指针计算,从而跳转到上一个节点。
encoding 和 length 两部分一起决定了 content 部分所保存的数据的类型(以及长度)。
content 部分保存着节点的内容,它的类型和长度由 encoding 和 length 决定。

### Redis底层实现的数据结构详解 Redis 是一种高性能的内存数据库,其底层数据结构的设计直接影响到它的性能表现。以下是 Redis 底层主要使用的几种核心数据结构及其作用。 #### 1. `redisObject` 结构 Redis 中的对象通过 `redisObject` 进行封装,这种设计使得 Redis 能够灵活地支持多种类型的值[^1]。 - **字段说明**: - `type`: 表示对象的具体类型(如字符串、列表、集合等),占用 4 位。 - `encoding`: 表示对象的实际存储方式(如整数、压缩列表、哈希表等),也占用 4 位。 - `lru`: 记录对象最近一次被访问的时间戳,用于 LRU 缓存淘汰策略。 - `refcount`: 引用计数器,记录当前对象被引用的次数。 - `ptr`: 指向实际存储数据的指针,具体指向的内容取决于 `type` 和 `encoding` 的组合。 #### 2. 列表键的底层实现 当一个列表键包含较少的元素,并且这些元素是小整数值或者较短的字符串时,Redis 使用压缩列表作为底层实现[^3]。随着版本更新,最新的 Redis 已经将压缩列表替换为更高效的 `listpack` 数据结构。对于较大的列表,则采用双向链表进行存储。 #### 3. 哈希键的底层实现 Redis 的哈希键有两种可能的底层实现形式: - 如果哈希键较小,Redis 可能会选择使用 `ziplist` 或者新的 `listpack` 来节省空间[^2]。 - 对于较大规模的哈希键,Redis 使用哈希表来提高查询效率。哈希表由 `dictht` 定义,其中包含了两个重要的部分——散列函数和冲突处理机制[^4]。 ##### 关于哈希表的细节 - **`dictht` 结构**: 描述了一个具体的哈希表实例,包括以下几个重要成员变量: - `table[]`: 存储哈希表节点的数组。 - `size`: 数组的最大容量。 - `sizemask`: 掩码值,用来快速定位索引位置,总是等于 `size - 1`。 - `used`: 当前已经分配并正在使用的槽位数量。 - **`dictEntry` 结构**: 单个哈希表条目对应的结构体,保存了键值对以及解决碰撞所需的链接信息。 - `key`: 键名。 - `v.val`, `v.u64`, `v.s64`: 多种可选的值表示方法。 - `next`: 链接至同一桶内的其他节点,构成单向链表以应对 hash 冲突的情况[^5]。 #### 4. 字符串键的底层实现 Redis 的字符串是最基础也是最常用的一种数据类型,在内部它既可以是一个简单的 C-style string (char array),也可以是以共享整数的形式存在,或者是基于 SDS (Simple Dynamic String) 实现的大尺寸动态字符串。 --- ### 示例代码展示 以下展示了如何创建一个基本的 Redis 哈希表: ```c #include <stdio.h> #include "dict.h" int main() { dictType ht; dict *d = dictCreate(&ht, NULL); /* 添加一些键值对 */ dictAdd(d, "key1", "value1"); dictAdd(d, "key2", "value2"); printf("Dictionary has %lu elements\n", d->ht[0].used); return 0; } ``` 此代码片段仅作示意用途,实际应用需引入完整的 Redis 源码库文件。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值