数据结构论坛

首页 » 分类 » 常识 » redis62源码解析hash字典
TUhjnbcbe - 2021/8/19 18:00:00

hash字典,又称hash表,也叫映射(Map)或者散列表,主要存储键值对,是程序开发语言中最最最常用的数据结构.也是我们大学计算机专业的必修课.一般的高级语言都会内置hash字典的实现.但是C并没有内置,所以Redis需要自己实现一套hash字典.

redis关于字典的实现,主要由dict.h和dict.c两个文件实现.

1C语言基础static关键字的作用

static修饰局部变量,此变量只能在作用域内访问,但是变量存储在静态区,函数执行完后变量不会销毁.普通局部变量在函数执行完后,立即销毁

static修饰全局变量,表示此变量只能在当前文件内访问

static修饰函数,表示此函数只能在当前文件内访问

具体细节可以参考黄裕玲的

union(共用体)与struct(结构体)的区别

结构体的各个成员会占用不同的内存,互相之间没有影响.

共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员.

结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。

2hash字典的结构

hash表一般的实现都是数组+链表来实现.数组来做hash散列,链表用来解决hash冲突的问题.如果我们了解过java的hashMap的话,不难发现,java7的hashMap的实现跟redis的hash表的实现如出一辙,java8关于hashMap的实现会略复杂一些,增加了链表转红黑树的内容.数据结构都是通用的,如果看过jdk的hashMap的源码,再看redis的hash表,基本没什么难度.

redis的hash字典大体结构图如下:

2.1hash表节点的结构

//hash表的节点

typedefstructdictEntry{

//节点的key

void*key;

//节点的值v,v可以是指针,也可以是uint64整数,也可以是int64整数,还可以是浮点数

union{

void*val;

uint64_tu64;

int64_ts64;

doubled;

}v;

//下一个节点

structdictEntry*next;

}dictEntry;

hash表节点,主要是包含了k,v键值对和下一个节点的指针,从结构体我们可以看出,这是一个hash表节点会组成一个单向链表

hash表的key是Void*指针类型,也就相当于java的Object,值v是一个共用体,也可以是uint64整数,也可以是int64整数,还可以是浮点数

2.2hash表结构

//hash表结构体

typedefstructdictht{

//hash表的指针数组,dictEntry*表示hash节点的指针,再加一个*,也就是dictEntry**表示数组的首地址

dictEntry**table;

//hash数组大小,一般为2^n

unsignedlongsize;

//hash数组长度掩码,sizemask=size-1

unsignedlongsizemask;

//hash表的kv对的个数

unsignedlongused;

}dictht;

由hash表结构,我们可以看出,redis也是通过数组做hash散列的.

hash表数组的size长度一般为2^n的大小,保证这样的大小可以通操作来计算出hash槽的位置,如果数组容量不是2^n的大小,那么就需要通过取模来获取hash表的槽索引,取模操作相对操作性能会差一些.

szimmask表示数组掩码,一般是size-1,如:假如数组长度为8,那么掩码就为7,掩码转换成二进制就为...

used表示hash表中kv的个数

2.3字典类型的结构

//字典类型

typedefstructdictType{

//键的hash函数

uint64_t(*hashFunction)(constvoid*key);

//复制键的函数

void*(*keyDup)(void*privdata,constvoid*key);

//值的复制函数

void*(*valDup)(void*privdata,constvoid*obj);

//键的比较函数

int(*keyCompare)(void*privdata,constvoid*key1,constvoid*key2);

//键的销毁函数

void(*keyDestructor)(void*privdata,void*key);

//值的销毁函数

void(*valDestructor)(void*privdata,void*obj);

//根据扩容后的数组的内存和负载因子判断是否可以扩容

int(*expandAllowed)(size_tmoreMem,doubleusedRatio);

}dictType;

字典类型的结构体主要提供相关函数的指针,用于做扩展,类似java的抽象方法和接口

这样做就相当于实现多态了,不同的dictType对应的函数指针都不一样

2.4redis字典的结构

//字典的结构体

typedefstructdict{

//字典类型的指针

dictType*type;

//携带的私有数据

void*privdata;

//hash字典的数组,长度为2,主要是用于渐进式hash时使用

dicththt[2];

//rehash下一个要迁移的桶索引,rehash不进行时为-1

longrehashidx;/*rehashingnotinprogressifrehashidx==-1*/

//pauserehash0表示rehash是暂停的.安全的迭代需要停止

int16_tpauserehash;/*If0rehashingispaused(0indicatescodingerror)*/

}dict;

由上面的结构体,我们可以看出redis的字典是有两个hash表组成的.常用的hash表是ht[0],当进行rehash时,会使用到ht[1]做渐进式重hash.

privdata和type主要是为了实现多态字典而已设置的,privdata主要是携带了特定函数需要的一些可选参数,type则保存了一批可自定义特定函数的指针,redis会根据字典的用途,在type中设置不同的特定函数

rehashidx当字典正在rehash时,会将下一个要rehash的槽索引记录到当前字段

pauserehash表示暂停当前的rehash状态,安全的迭代和扫描都需要暂停rehash的,当pauserehash大于0时,则表示rehash暂停,等于0时表示可以rehash.为什么暂停需要计数?因为安全迭代器和扫描都有可能同时存在多个,所以pauserehash的值是很有可能大于1的

3字典通用操作实现3.1创建字典

/*Createanewhashtable*/

//创建空字典的指针

dict*dictCreate(dictType*type,

void*privDataPtr)

{

//分配字典内存

dict*d=zmalloc(sizeof(*d));

//初始化字典

_dictInit(d,type,privDataPtr);

//返回字段指针

returnd;

}

/*Initializethehashtable*/

//初始化hash表

int_dictInit(dict*d,dictType*type,

void*privDataPtr)

{

//重置第0个hash表

_dictReset(d-ht[0]);

//重置第1个hash表

_dictReset(d-ht[1]);

//设置字典类型

d-type=type;

//设置私有数据

d-privdata=privDataPtr;

//不进行rehash时,为-1

d-rehashidx=-1;

d-pauserehash=0;

returnDICT_OK;

}

//hash字典重置

staticvoid_dictReset(dictht*ht)

{

//清空

ht-table=NULL;

ht-size=0;

ht-sizemask=0;

ht-used=0;

}

创建字典主要的操作是,分配字典内存,并且对字典类型和私有数据进行赋值,注意,此时字典的hash表还没初始化的,其他统计数据都是0

3.2根据键查询节点

//根据key查询节点

dictEntry*dictFind(dict*d,constvoid*key)

{

dictEntry*he;

uint64_th,idx,table;

//如果字典为空,则直接返回NULL

if(dictSize(d)==0)returnNULL;/*dictisempty*/

//如果字典正在rehash,则执行一次rehash步聚

if(dictIsRehashing(d))_dictRehashStep(d);

//计算key的hash值

h=dictHashKey(d,key);

//遍历字典的hash表

for(table=0;table=1;table++){

//计算出key所有的hash槽索引

idx=hd-ht[table].sizemask;

//根据hash槽获取首节点

he=d-ht[table].table[idx];

//遍历链表,直到找到key一样的节点

while(he){

//如果找到,则直接返回找到的节点

if(key==he-key

dictCompareKeys(d,key,he-key))

returnhe;

he=he-next;

}

//如果没找到,且字典没有rehash,则直接返回NULL

if(!dictIsRehashing(d))returnNULL;

}

//没找到,返回NULL

returnNULL;

}

//获取key对应的值

void*dictFetchValue(dict*d,constvoid*key){

dictEntry*he;

//根据key查询节点

he=dictFind(d,key);

//如果节点存在,则获取节点的值,否则返回NULL

returnhe?dictGetVal(he):NULL;

}

大概的思路是,计算出给定键key的hash值,然后先在第一个hash表中获取对应的槽索引,如果槽存在链表首节点,则遍历链表,直到找到节点的key与传入的key相等的节点

如果字典正在rehash,那么第一个hash表没找到,则会在第二张hash表中查询

这里注意key的比较,优先比较内存地址是否一样,再用

1
查看完整版本: redis62源码解析hash字典