Redis的数据类型(一)dict篇

本文深入探讨了Redis中dict数据结构的实现原理及其rehash机制。重点介绍了如何通过负载因子来决定何时进行扩容与缩容,并详细解释了渐进式rehash的过程。

reids是什么就不过多描述了,咱直接进入主题

我们都知道redis的5大数据类型:string,list,hash,set与zset。但其实就redis而言,它的整体是一个dict类型的,是redis中所有key到value的映射。

一、dict

下面写到:每个字典都有两个这样的对旧表到新表执行增量重新散列。

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dict {
    dictType *type;    //dictType结构中包含自定义的函数,这些函数使得key和value能够存储任何类型的数据
    void *privdata;    //私有数据,保存着dictType结构中函数
    dictht ht[2];      //两张哈希表
    long rehashidx;    //rehash的标记,rehashidx == -1,表示没有进行 rehash
    int16_t pauserehash;    //正在迭代的迭代器数量
} dict;
typedef struct dictht {
    dictEntry **table;        //存放一个数组的地址,数组中存放哈希节点dictEntry的地址
    unsigned long size;       //哈希表table的大小,出始大小为4
    unsigned long sizemask;   //用于将hash值映射到table位置的索引,大小为(size-1)
    unsigned long used;       //记录哈希表已有节点(键值对)的数量
} dictht;

既然是hash,那么就一定会有扩容的操作。看看具体是怎么做的

这里着重说一下两张哈希表,先说结论,dict数据与java中的map相似,主要是根据负载因子(源码中的参数load_factor)进行扩容的操作。

  1. 负载因子
    1. load_factor = ht[0].used / ht[0].size,这是计算负载因子的公式
    2. 在正常情况下,当 load_factor 大于1时,会进行扩容操作
    3. 在RDB/AOF(做持久化,可以理解为mysql的binlog)阶段,load_factor 默认大于5才会进行扩容操作。
      1. redis会fork一个子进程,在子进程期间,来提高负载因子,可以避免在子进程中进行rehash,规避不必要的内存写入,节约内存,另外也是为了尽可能减少内存页过多分离,系统需要更多的开销去回收内存。
  2. 扩容
    1. 扩容操作就是rehash的操作,redis在rehash是占用主线程的,所以采用渐进式rehash(即每次只移动一个链表)。这样避免了客户端的指令堆积与阻塞
    2. 是在重新rehash的时候,ht[0]和ht[1]都有效。在平常,就只有ht[0]生效,而ht[1]是空的
    3. 当rehash完成,则会将0进行释放,将1设置为0,在将1初始化成新的hash表
    4. 在开始和完成rehash,会去更新rehashidx = 0(表示rehash开始工作,每进行一次rehash,rehashidx += 1)和 rehashidx = -1(结束rehash)
  3. 缩容
    1. 负载因子小于0.1
  4. reash时的CURD
    1. 插入。只会在ht[1]上进行新增
    2. 查询,更新与删除:会在ht[0]和ht[1]上同时执行,如果ht[0]上没找到,则会在ht[1]上操作。

注:触发rehash的操作有查询、插入和删除元素。插入与删除很好理解,但是查询又是为什么呢?

其原因就在于当哈希桶中的链表过长时,那么查询性能会显著降低(链表的查找时间复杂度为O(N)),Redis为了避免类似的问题从而会进行Rehash操作

粗略的画了下rehash的流程,详细的可以看redis的rehash,这里讲的是老版本的redis,但是rehash基本没变,所以我给他放上来

 

<think>我们被要求对Redis数据类型的源码进行详细分析,并且需要参考提供的引用内容。根据引用内容,我们知道Redis数据类型包括String、Hash、List、Set、Sorted Set等,并且这些数据类型是通过不同的底层数据结构组合实现的。主要的底层数据结构有SDS动态字符串、双向链表、压缩列表(ziplist)、哈希表(hashtable)、跳表(skiplist)、整数集合(intset)、快速列表(quicklist)和紧凑列表(listpack)等。 接下来,我们将按照数据类型分析其源码实现,并参考引用中的信息。 1. String(字符串) - 引用[4]提到字符串的编码格式有raw、embstr和int三种。 - 在源码中,字符串类型是通过SDS(Simple Dynamic String)实现的。SDS的结构定义在`sds.h`文件中。 - 三种编码格式: - int编码:当存储的值是整数并且可以用long表示时,会直接存储整数在redisObject的ptr位置。 - embstr编码:当字符串长度小于等于44字节(Redis5.0版本)时,使用embstr编码,该编码将redisObject和SDS结构连续存储,减少内存分配次数。 - raw编码:当字符串长度大于44字节时,使用raw编码,此时redisObject和SDS是分开存储的。 2. List(列表) - 引用[2]提到List的底层数据结构是快速列表(quicklist)。quicklist是ziplist和双向链表的结合,每个节点是个ziplist。 - 在Redis3.2之后,List的底层实现统使用quicklist。源码在`quicklist.h`和`quicklist.c`中。 - 结构定义:quicklist包含指向头尾节点的指针,以及节点数量等信息。每个节点(quicklistNode)包含个ziplist。 3. Hash(哈希) - 引用[4]提到Hash的底层实现是ziplist或hashtable。 - 当哈希中的元素个数较少且元素长度较短时,使用ziplist存储;否则使用hashtable。 - ziplist存储方式是连续的键值对(key1, value1, key2, value2, ...)。 - hashtable则使用字典(dict)实现,字典的键和值都是SDS字符串。 4. Set(集合) - 引用[4]提到Set的底层实现是intset或hashtable。 - 当集合中元素都是整数且元素数量较少时,使用intset(整数集合)存储。 - 否则使用hashtable,此时字典的键为集合元素,值为NULL。 5. Sorted Set(有序集合) - 引用[4]和引用[5]提到有序集合的底层实现是压缩列表(ziplist)或跳表(skiplist)和字典(dict)的组合(zset结构)。 - 当元素数量较少且元素长度较小时,使用ziplist存储(按分值排序,元素和分值依次存储)。 - 否则使用zset结构,该结构包含个跳表(zskiplist)和个字典(dict)。跳表用于支持范围操作(如ZRANGE),字典用于支持按成员快速查找分值(O(1))。 - 跳表结构定义如引用[5]所示,其中zskiplistNode包含元素(sds)、分值(double)、后退指针和层级数组(每个层级有前进指针和跨度)。zskiplist包含头尾指针、长度和最大层级。zset结构则包含dict个zskiplist。 6. 其他类型:如HyperLogLog、Geo和Streams等,由于问题集中在常用数据类型,这里暂不展开。 对于源码分析,我们需要定位到Redis源码中的具体文件。以下是些关键文件: - sds.h, sds.c: 字符串实现 - t_hash.c: 哈希表实现 - t_list.c: 列表实现(使用quicklist) - t_set.c: 集合实现 - t_zset.c: 有序集合实现 - 以及底层数据结构的实现:ziplist.c, dict.h, dict.c, intset.c, skiplist.c(实际在server.h中定义结构,在zset操作中使用), quicklist.c, listpack.c(新版本中可能替代ziplist) 由于引用内容较多,我们尽量参考引用中的信息,并结合Redis源码(以Redis 6.0版本为例)进行分析。 下面我们分别详细分析每个数据类型的源码实现,包括其底层数据结构、编码转换条件等。由于幅限制,我们将重点放在核心结构和关键函数上。 </think> ### Redis 数据类型源码详细分析 以下从源码层面对 Redis 核心数据类型进行深度解析,结合 Redis 6.0 源码结构,重点分析底层数据结构实现机制。 #### 1. String(字符串) **核心结构**: 在 `sds.h` 中定义 SDS (Simple Dynamic String) 动态字符串结构: ```c struct sdshdr { uint64_t len; // 字符串长度 uint64_t alloc; // 分配的内存空间 unsigned char flags; // 类型标志(SDS_TYPE_8/16/32/64) char buf[]; // 柔性数组存储实际数据 }; ``` **编码方式**(源码 `object.c`): - **INT 编码**:当值为整型时,直接存储在 `redisObject.ptr` 中(指针复用) - **EMBSTR 编码**:字符串 ≤44 字节时,`redisObject` 与 SDS 内存连续分配(减少内存碎片) - **RAW 编码**:字符串 >44 字节时,独立分配 SDS 内存空间 **关键特性**: 预分配策略(减少内存重分配)、二进制安全(`len` 记录长度而非 `\0` 终止)[^2][^4] --- #### 2. Hash(哈希表) **底层结构**(源码 `dict.h`): ```c typedef struct dict { dictType *type; // 类型特定函数 dictht ht[2]; // 双哈希表(用于渐进式rehash) long rehashidx; // rehash进度 } dict; typedef struct dictht { dictEntry **table; // 桶数组 unsigned long size; // 桶数量 unsigned long sizemask; // 掩码(size-1) unsigned long used; // 已用节点数 } dictht; ``` **编码转换**(源码 `t_hash.c`): - **ZIPLIST 编码**:元素数量 ≤512 且值长度 ≤64 字节时,使用连续内存存储键值对 - **HASHTABLE 编码**:超过阈值时转换为哈希表 **优化机制**:渐进式 rehash(操作时分步迁移数据,避免阻塞)[^2] --- #### 3. List(列表) **核心结构**(源码 `quicklist.h`): ```c typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; // 元素总数 unsigned long len; // quicklistNode数量 int fill : 16; // ziplist大小限制 unsigned int compress : 16; // LZF压缩深度 } quicklist; typedef struct quicklistNode { struct quicklistNode *prev; struct quicklistNode *next; unsigned char *zl; // 指向ziplist size_t sz; // ziplist字节大小 unsigned int count : 16; // ziplist元素计数 unsigned int encoding : 2; // 编码类型(RAW/LZF) } quicklistNode; ``` **设计优势**: - 结合双向链表与 ziplist:降低小元素内存开销 - 支持节点压缩(LZF算法)[^2][^5] --- #### 4. Set(集合) **底层实现**(源码 `setpro.c`): - **INTSET 编码**:当元素均为整数时,使用整数数组: ```c typedef struct intset { uint32_t encoding; // 编码类型(INTSET_ENC_INT16/32/64) uint32_t length; // 元素数量 int8_t contents[]; // 柔性数组存储数据 } intset; ``` - **HASHTABLE 编码**:非整型元素或元素数量 >512 时转为哈希表(值设为 NULL) **自动升级**:插入新整数导致类型溢出时,自动扩展编码位数(如 INT16 → INT32) --- #### 5. Sorted Set(有序集合) **核心结构**(源码 `server.h`): ```c typedef struct zset { dict *dict; // 哈希表(维护 member->score 映射) zskiplist *zsl; // 跳表(维护有序性) } zset; 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; ``` **协同机制**: - 跳表提供 $O(\log N)$ 范围查询(ZRANGE) - 哈希表提供 $O(1)$ 的单元素分值查询(ZSCORE)[^5] --- #### 6. 类型统管理 **redisObject 结构**(源码 `redis.h`): ```c typedef struct redisObject { unsigned type:4; // 数据类型(STRING/LIST/HASH等) unsigned encoding:4; // 编码方式(RAW/INT/HASHTABLE等) unsigned lru:24; // LRU时间戳 int refcount; // 引用计数 void *ptr; // 指向实际数据结构 } robj; ``` **关键操作**: - 类型检查:通过 `type` 字段实现命令多态 - 内存回收:引用计数+LRU 淘汰策略 --- ### 编码转换触发条件 | 数据类型 | 编码1 | 阈值条件 | 编码2 | |---------|-------|----------|-------| | Hash | ziplist | field数>512 或 值>64字节 | hashtable | | List | ziplist | 元素>8KB 或 节点压缩深度>2 | quicklist | | Set | intset | 元素>512 或 非整数 | hashtable | | ZSet | ziplist | 元素>128 或 值>64字节 | skiplist+dict | > 阈值可通过 `redis.conf` 中 `hash-max-ziplist-entries` 等参数调整 --- ### 性能优化关键点 1. **小数据压缩**:ziplist 减少内存碎片 2. **写时复制**:robj.refcount 实现共享对象(如 0-9999 的整数字符串) 3. **惰性删除**:异步释放大对象内存 4. **渐进式扩容**:dict.rehashidx 避免大规模 rehash 阻塞[^3]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值