当前位置:首页 > 问答 > 正文

钻研Redis源码里那个Dict结构到底是咋回事,想搞懂它得先看这些细节

要搞懂Redis源码里的Dict结构,你不能一上来就扎进dict.cdict.h文件里那几百行代码中,那样很容易迷失,你得先把它拆开,像看一个玩具的零件一样,一个一个看明白,这个Dict,说白了就是Redis自己实现的一个字典,也就是键值对集合,它是Redis整个系统的顶梁柱,像我们常用的SET, GET, HSET, HGET这些命令,底层都直接或间接地用到了它。

钻研Redis源码里那个Dict结构到底是咋回事,想搞懂它得先看这些细节

你得知道这个Dict结构体本身是由哪几块“积木”拼成的,根据源码(参考dict.h中的定义),最主要的是这三样东西:dict结构体、dictht结构体和dictEntry结构体,它们仨的关系是这样的:

钻研Redis源码里那个Dict结构到底是咋回事,想搞懂它得先看这些细节

  1. dictEntry:这是最小单位,代表一个键值对。 你可以把它想象成一个小盒子,里面装着三样东西:你的key(键)、你的value(值),还有一个神奇的next指针,这个next指针是关键,它用来指向下一个dictEntry,为啥需要指向下一个?因为Redis的Dict在处理键冲突时,用的是“链地址法”,也就是当两个不同的键通过哈希函数算出来的位置一样时,它们会被串成一条链表,这个next就是用来串起这条链的,一个dictEntry可能就是一条链表的头节点。

    钻研Redis源码里那个Dict结构到底是咋回事,想搞懂它得先看这些细节

  2. dictht(Dictionary Hash Table):这是哈希表本体。 它就像一个有很多个格子的柜子,这个结构体里主要包含:一个指针**table,它指向一个数组,这个数组里存放的就是一个个的dictEntry指针(也就是一条条链表的头指针);size表示这个数组有多大;sizemask(大小掩码),它总是等于size-1,这个掩码是用来做位运算,快速计算键应该落在数组的哪个索引上的;used表示这个哈希表里已经存放了多少个键值对。

  3. dict:这是字典的“总管”,它封装了哈希表和一些关键操作。 它里面最核心的是包含了两个dictht,通常叫ht[0]ht[1],为啥要两个?这就涉及到Redis Dict最精妙的设计之一——渐进式重新哈希(incremental rehashing),平时,数据主要放在ht[0]里,当ht[0]里的数据太多了(比如数量接近数组大小的5倍时,这个因子可配),就需要扩容(reshashing),但Redis是单线程的,如果一次性把ht[0]里所有键值对都搬到新的、更大的ht[1]里,这个操作可能会阻塞服务器很长时间,导致服务卡顿,所以Redis想了个巧妙的办法:它不会一次性搬完,而是慢慢地、分批地搬。 这个“总管”dict结构里还有一个非常重要的成员叫rehashidx,当没有在进行重新哈希时,它的值是-1,当开始重新哈希时,它会被设置为0,表示接下来要从ht[0]数组的第0个索引开始迁移数据,每次有对字典的增删改查操作时,Redis除了完成操作本身,还会“顺便”从ht[0]rehashidx位置开始,迁移一小部分(比如一个索引对应的整条链表)到ht[1]中,然后rehashidx加1,这样,就把一次性的庞大工作量,打散到了无数次小的客户端请求中,避免了服务停顿,这就是“渐进式”的由来,在重新哈希期间,查找一个键需要同时查ht[0]ht[1]两张表;而新插入的键值对则会直接放到ht[1]里,保证ht[0]的数据只减不增。

除了这三个核心结构,dict这个“总管”身上还“挂”着几个函数指针,比如typeprivdata,这体现了Redis设计上的灵活性。type指向一个dictType结构,这个结构里定义了一组用于操作特定类型键值对的函数指针,比如计算哈希值的函数hashFunction、复制键的函数keyDup、比较键的函数keyCompare等,这样,Redis的Dict就不是一个死板的、只能处理一种数据类型的字典,通过设置不同的dictType,它可以灵活地管理各种不同类型的键和值,这为Redis支持多种数据结构提供了基础。

你要钻研Redis的Dict,就得抓住这几个细节脉络:dictEntry如何组成链表解决冲突;dictht如何作为基础的哈希表存储桶;而最关键的,是dict如何通过维护两个dicthtrehashidx指标,实现了平滑无感的渐进式重新哈希,这是Redis保证高性能不卡顿的秘诀之一。 把这些基础打牢了,再去看源码里具体的插入、查找、删除操作的实现,你就会发现思路清晰很多,知道每一步操作在哪个结构上、针对哪种状态(是否在rehashing)进行,就不会一头雾水了。