redis源码分析系列文章

Redis的双向链表一文全知道

前言

hello,各位小可爱们,又见面了。今天这篇文章来自去年面试阅文的面试题,结果被虐了。这一part不说了,下次专门开一篇,写下我面试被虐的名场面,尴尬的不行,全程尬聊。哈哈哈哈,话不多说,开始把。

今天要写Redis的Hash类型,如果有对Redis不熟悉,或者对其他数据类型感兴趣的,可以移步上面的系列文章。(最上面的最上面最上面,重要的事情说三遍)

在Redis中Hash类型的应用非常广泛,其中key到value的映射就通过字典结构来维护的。记笔记,此处要考。

API使用

API的使用比较简单,所以以下就粗略的写了。

插入数据hset

使用hset命令往myhash中插入两个key,value的键值对,分别是(name,zhangsan)和(age,20),返回值当前的myhash的长度。

获取数据hget

使用hget命令获取myhash中key为name的value值。

获取所有数据hgetall

使用hgetall命令获取myhash中所有的key和value值。

获取所有key

使用hkeys命令获取myhash中所有的key值。

获取长度

使用hlen命令获取myhash的长度。

获取所有value

使用hvals命令获取myhash中所有的value值。

具体逻辑图

hash的底层主要是采用字典dict的结构,整体呈现层层封装。

首先dict有四个部分组成,分别是dictType(类型,不咋重要),dictht(核心),rehashidx(渐进式hash的标志),iterators(迭代器),这里面最重要的就是dictht和rehashidx。

接下来是dictht,其有两个数组构成,一个是真正的数据存储位置,还有一个用于hash过程,包括的变量分别是真正的数据table和一些常见变量。

最后数据节点,和上篇说的双向链表一样,每个节点都有next指针,方便指向下一个节点,这样目的是为了解决hash碰撞。具体的可以看下图。

这边看不懂没关系,后面会针对每个模块详细说明。(千万不要看到这里就跳过啦)

双向链表的定义

字典结构体dict

我们先看字典结构体dict,其包括四个部分,重点是dictht[2](真正的数据)和rehashidx(渐进式hash的标志)。具体图如下。

具体代码如下:

//字典结构体
typedef struct dict {
dictType *type;//类型,包括一些自定义函数,这些函数使得key和value能够存储
void *privdata;//私有数据
dictht ht[];//两张hash表
long rehashidx; //渐进式hash标记,如果为-1,说明没在进行hash
unsigned long iterators; //正在迭代的迭代器数量
} dict;

数组结构体dictht

dictht主要包括四个部分,1是真正的数据dictEntry类型的数组,里面存放的是数据节点;2是数组长度size;3是进行hash运算的参数sizemask,这个不咋重要,只要记住等于size-1;4是数据节点数量used,当前有多少个数据节点。

具体代码如下:

//hash结构体
typedef struct dictht {
dictEntry **table;//真正数据的数组
unsigned long size;//数组的大小
unsigned long sizemask;//用户将hash映射到table的位置索引,他的值总是等于size-1
unsigned long used;//已用节点数量
} dictht;

数据节点dictEntry

dictEntry为真正的数据节点,包括key,value和next节点。

//每个节点的结构体
typedef struct dictEntry {
void *key; //key
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;//value
struct dictEntry *next; //下一个数据节点的地址
} dictEntry;

扩容过程和渐进式Hash图解

我们先来第一个部分,dictht[2]为什么会要2个数组存放,真正的数据只要一个数组就够了?

这其实和Java的HashMap相似,都是数据加链表的结构,随着数据量的增加,hash碰撞发生的就越频繁,每个数组后面的链表就越长,整个链表显得非常累赘。如果业务需要大量查询操作,因为是链表,只能从头部开始查询,等一个数组的链表全部查询完才能开始下一个数组,这样查询时间将无线拉长。

这无疑是要进行扩容,所以第一个数组存放真正的数据,第二个数组用于扩容用。第一个数组中的节点经过hash运算映射到第二个数组上,然后依次进行。那么过程中还能对外提供服务吗?答案是可以的,因为他可以随时停止,这就到了下一个变量rehashidx。(一点都不生硬的转场,哈哈哈)

rehashidx其实是一个标志量,如果为-1说明当前没有扩容,如果不为-1则表示当前扩容到哪个下标位置,方便下次进行从该下标位置继续扩容。

这样说是不是太抽象了,还是一脸懵逼,贴心的送上扩容过程全解,一定要点赞评论多夸夸我哦。(越来越不要脸了。。。)

步骤1

首先是未扩容前,rehashidx为-1,表示未扩容,第一个数组的dictEntry长度为4,一共有5个节点,所以used为5。

步骤2

当发生扩容了,rahashidx为第一个数组的第一个下标位置,即0。扩容之后的大小为大于used*2的2的n次方的最小值,即能包含这些节点*2的2的倍数的最小值。因为当前为5个数据节点,所以used*2=10,扩容后的数组大小为大于10的2的次方的最小值,为16。从第一个数组0下标位置开始,查找第一个元素,找到key为name,value为张三的节点,将其hash过,找到在第二个数组的下标为1的位置,将节点移过去,其实是指针的移动。这边就简单说了。

步骤3

key为name,value为张三的节点移动结束后,继续移动第一个数组dictht[0]的下标为0的后续节点,移动步骤和上面相同。

步骤4

继续移动第一个数组dictht[0]的下标为0的后续节点都移动完了,开始移动下标为1的节点,发现其没有数据,所以移动下标为2的节点,同时修改rehashidx为2,移动步骤和上面相同。

整个过程的重点在于rehashidx,其为第一个数组正在移动的下标位置,如果当前内存不够,或者操作系统繁忙,扩容的过程可以随时停止。

停止之后如果对该对象进行操作,那是什么样子的呢?

  • 如果是新增,则直接新增后第二个数组,因为如果新增到第一个数组,以后还是要移过来,没必要浪费时间
  • 如果是删除,更新,查询,则先查找第一个数组,如果没找到,则再查询第二个数组。

字典的实现(源码分析)

创建并初始化字典

首先分配内存,接着调用初始化方法_dictInit,主要是赋值操作,重点看下rehashidx赋值为-1(这验证了刚才的图解,-1表示未进行hash扩容),最后返回是否创建成功。

/* 创建并初始化字典 */
dict *dictCreate(dictType *type,
void *privDataPtr)
{
dict *d = zmalloc(sizeof(*d));
_dictInit(d,type,privDataPtr);
return d;
} /* Initialize the hash table */
int _dictInit(dict *d, dictType *type,
void *privDataPtr)
{
_dictReset(&d->ht[]);
_dictReset(&d->ht[]);
d->type = type;
d->privdata = privDataPtr;
d->rehashidx = -;//赋值为-1,表示未进行hash
d->iterators = ;
return DICT_OK;
}

扩容

dict里面有一个静态方法_dictExpandIfNeed,判断是否需要扩容。

首先判断通过dictIsRehashing方法,判断是否处于hash状态,其调用的是宏常量#define dictIsRehashing(d) ((d)->rehashidx != -1),即判断rehashidx是否为-1,如果为-1,即不处于hash状态,if条件为false,可以进行扩容,如果不为-1,即处于hash状态,if条件为true,不可以进行扩容,直接返回常量DICT_OK。

接着判断第一个数组的size是否为0,如果为0,则扩容为默认大小4,如果不为0,则执行下面的代码。

再接着判断是否需要扩容,if中有三个条件,具体的分析如下。

最后就是调用dictExpand扩容方法了,参数为数据节点的双倍大小ht[0].used*2。此处验证了上面扩容过程的数组大小16。

扩容方法比较简单点,获取扩容后的大小,将第二个设置新的大小。

这样讲感觉有点空,看下流程图。

扩容流程图

具体代码:

static int _dictExpandIfNeeded(dict *d)
{
//判断是否处于扩容状态中,通过调用宏常量#define dictIsRehashing(d) ((d)->rehashidx != -1)
//来判断是否可以扩容
if (dictIsRehashing(d)) return DICT_OK; //判断第一个数组size是否为0,如果为0,则调用扩容方法,大小为宏常量
//#define DICT_HT_INITIAL_SIZE 4
if (d->ht[].size == ) return dictExpand(d, DICT_HT_INITIAL_SIZE); //下面先列出if条件中所使用到的参数
// static int dict_can_resize = 1;数值为1表示可以扩容
//static unsigned int dict_force_resize_ratio = 5;
//我们来分析if条件,如果第一个数组的所有节点数量大于等于第一个数组的大小(表示节点数据已经有些多)
//并且可用扩容(数值为1)或者所有节点数量除以数组大小大于5
//这个条件表示扩容那个的条件,第一个就是节点必要大于等于数组长度,
//第二点就再可以扩容和数据太多,超过5两个中选其一
if (d->ht[].used >= d->ht[].size &&
(dict_can_resize ||
d->ht[].used/d->ht[].size > dict_force_resize_ratio))
{
//调用扩容方法
return dictExpand(d, d->ht[].used*);
}
return DICT_OK;
} int dictExpand(dict *d, unsigned long size)
{
dictht n;
//获取扩容后真正的大小,找到比size大的最小值,且是2的倍数
unsigned long realsize = _dictNextPower(size); //一些判断条件
if (dictIsRehashing(d) || d->ht[].used > size)
return DICT_ERR; if (realsize == d->ht[].size) return DICT_ERR; n.size = realsize;
n.sizemask = realsize-;
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.used = ; //第一个hash为null,说明在初始化
if (d->ht[].table == NULL) {
d->ht[] = n;
return DICT_OK;
}
//正在hash,给第二个hash的长度设置新的,
d->ht[] = n;
d->rehashidx = ;//设置当前正在hash
return DICT_OK;
} /* 找到比size大的最小值,且是2的倍数 */
static unsigned long _dictNextPower(unsigned long size)
{
unsigned long i = DICT_HT_INITIAL_SIZE; if (size >= LONG_MAX) return LONG_MAX;
while() {
if (i >= size)
return i;
i *= ;
}
}

渐进式hash

渐进式hash过程已经通过上面图解说明,以下主要看下代码是如何实现的,以及过程是不是对的。

扩容之后就是执行dictRehash方法,参数包括待移动的哈希表d和步骤数字n。

首先判断标志量rehashidx是否等于-1,如果等于-1,则表示hash完成,如果不等于-1,则执行下面的代码。

接着进行循环,遍历第一个数组上的每个下标,每次移动下标位置,都需要更新rehashidx值,每次加1。

再接着进行第二个循环,遍历下标的链表每个节点,完成数据的迁移,主要是指针的移动和一些参数的修改。

最后,返回int数值,如果为0表示整个数据全部hash完成,如果返回1则表示部分hash结束,并没有全部完成,下次可以通过rehashidx值继续hash。

具体代码如下:

//重新hash这个哈希表
// Redis的哈希表结构公有两个table数组,t0和t1,平常只使用一个t0,当需要重hash时则重hash到另一个table数组中
//参数列表
// 1. d: 待移动的哈希表,结构中存有目前已经重hash到哪个桶了
// 2. n: N步进行rehash
// 返回值 返回0说明整个表都重hash完成了,返回1代表未完成
int dictRehash(dict *d, int n) {
int empty_visits = n*;
//如果当前rehashidx=-1,则返回0,表示hash完成
if (!dictIsRehashing(d)) return ;
//分n步,而且ht[0]还有没有移动的节点
while(n-- && d->ht[].used != ) {
dictEntry *de, *nextde;
assert(d->ht[].size > (unsigned long)d->rehashidx);
//第一个循环用来更新 rehashidx 的值,因为有些桶为空,所以 rehashidx并非每次都比原来前进一个位置,而是有可能前进几个位置,但最多不超过 10。
//将rehashidx移动到ht[0]有节点的下标,也就是table[d->rehashidx]非空
while(d->ht[].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == ) return ;
}
de = d->ht[].table[d->rehashidx]; //第二个循环用来将ht[0]表中每次找到的非空桶中的链表(或者就是单个节点)拷贝到ht[1]中 /* 利用循环讲数据节点移过去 */
while(de) {
unsigned int h; nextde = de->next;
/* Get the index in the new hash table */
h = dictHashKey(d, de->key) & d->ht[].sizemask;
de->next = d->ht[].table[h];
d->ht[].table[h] = de;
d->ht[].used--;
d->ht[].used++;
de = nextde;
}
d->ht[].table[d->rehashidx] = NULL;
d->rehashidx++;
} if (d->ht[].used == ) {
zfree(d->ht[].table);
d->ht[] = d->ht[];
_dictReset(&d->ht[]);
d->rehashidx = -;
return ;
} return ;
}

总结

该篇主要讲了Redis的Hash数据类型的底层实现字典结构Dict,先从Hash的一些API使用,引出字典结构Dict,剖析了其三个主要组成部分,字典结构体Dict,数组结构体Dictht,数据节点结构体DictEntry,进而通过多幅过程图解释了扩容过程和rehash过程,最后结合源码对字典进行描述,如创建过程,扩容过程,渐进式hash过程,中间穿插流程图讲解。

如果觉得写得还行,麻烦给个赞,您的认可才是我写作的动力!

如果觉得有说的不对的地方,欢迎评论指出。

好了,拜拜咯。

面试官:说说Redis的Hash底层 我:......(来自阅文的面试题)的更多相关文章

  1. 面试官:Redis中字符串的内部实现方式是什么?

    在面试间里等候时,感觉这可真暖和呀,我那冰冷的出租屋还得盖两层被子才能睡着.正要把外套脱下来,我突然听到了门外的脚步声,随即门被打开,穿着干净满脸清秀的青年走了进来,一股男士香水的淡香扑面而来. 面试 ...

  2. 面试官:Redis中哈希数据类型的内部实现方式是什么?

    面试官:Redis中基本的数据类型有哪些? 我:Redis的基本数据类型有:字符串(string).哈希(hash).列表(list).集合(set).有序集合(zset). 面试官:哈希数据类型的内 ...

  3. 面试官:Redis中集合数据类型的内部实现方式是什么?

    虽然已经是阳春三月,但骑着共享单车骑了这么远,还有有点冷的.我搓了搓的被冻的麻木的手,对着前台的小姐姐说:"您好,我是来面试的."小姐姐问:"您好,您叫什么名字?&quo ...

  4. 面试官:Redis集群有哪些方式,Leader选举又是什么原理呢?

    哈喽!大家好,我是小奇,一位不靠谱的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 作为一名Java程序员,Redi ...

  5. 面试官:Redis中有序集合的内部实现方式是什么?

    面试官:Redis中基本的数据类型有哪些? 我:Redis的基本数据类型有:字符串(string).哈希(hash).列表(list).集合(set).有序集合(zset). 面试官:有序集合的内部实 ...

  6. 面试官:Redis的共享对象池了解吗?

    我正在面试间里焦急地等待着,突然听到了门外的脚步声,随即门被打开,穿着干净满脸清秀的青年走了进来,一股男士香水的淡香扑面而来. 面试官:"平时在工作中用过Redis吗?" 我:&q ...

  7. 面试官:说一下Synchronized底层实现,锁升级的具体过程?

    面试官:说一下Synchronized底层实现,锁升级的具体过程? 这是我去年7,8月份面试的时候被问的一个面试题,说实话被问到这个问题还是很意外的,感觉这个东西没啥用啊,直到后面被问了一波new O ...

  8. 面试官再问你 HashMap 底层原理,就把这篇文章甩给他看

    前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希望对你有所帮助~ ...

  9. 面试官:Redis中列表的内部实现方式是什么?

    在面试间里等候时,感觉这可真暖和呀,我那冰冷的出租屋还得盖两层被子才能睡着.正要把外套脱下来,我突然听到了门外的脚步声,随即门被打开,一位眉毛弯弯嘴唇红红的小姐姐走了进来,甜甜的香水味立刻钻进了我的鼻 ...

随机推荐

  1. Java基础之数据类型

    一.数据类型 基本数据类型介绍 byte 1字节 char 2字节 short 2字节 int 4字节 long 8字节 float 4字节 double 8字节 以上有Java中八大基本类型的7种, ...

  2. 5.7 Go 捕获异常

    5.7 Go 捕获异常 Go语言处理异常不同于其他语言处理异常的方式. 传统语言处理异常: try catch finally go语言 引入了defer.panic.recover 1.Go程序抛出 ...

  3. java排查故障

    java排查故障top -Hp 31327 #或top -p 31327,再按shift+h,-H则是线程开关,传入该参数的话,top界面会显示所有单独的线程列表) ##31327为java进程,拿到 ...

  4. 处理TableVIew SectionHeader悬停问题

    1,plain类型的tableview  才会悬停  grouped类型不会悬停 2,通过改变contentInset达到部分悬停遮罩的处理,对所有的Sectionheader都有效,不可以单独处理某 ...

  5. k3s-安装

    k3s介绍 K3S是一个轻量级的K8S集群,它是Rancher Lab开发的一个新的产品, 目的是在资源有限的设备上面跑K8S.它的最大特点就是小,二进制包只有40MB,只需要512MB的内存就能跑起 ...

  6. 11_ArrayList集合的方法

    class Program { static void Main(string[] args) { //数组:长度不可变,类型单一 //ArrayList集合:长度可以任意改变,类型可以不单一 //创 ...

  7. Python中对文件的读写

    读写文件前,我们先必须了解一下,在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘. 读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系 ...

  8. Redis学习笔记(1)

    一.NoSQL基础知识 1. NoSQL概念 NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库.随着互联网web2.0网站的兴起,传统的关系数据库 ...

  9. Python——关于定义过程

    def sum(a,b): a = a + b return a print(sum(1,2)) s = 3 t = 5 print(sum(s,t)) 题目:你觉得前三行代码会输出什么? 1.输入两 ...

  10. PHP AES加解密(兼容php5,php7)

    最近在工作中负责对接API,对方要求对业务数据进行AES 算法(256,ECB,补码方式:PKCS5Padding)加密. 加密算法要求如下: 算法AES/ECB/PKCS5Padding 密钥长度2 ...