Redis字典介绍

Redis是K-V型数据库, 整个数据库是用字典来存储的, 对Redis数据库进行任何增、 删、 改、 查操作, 实际就是对字典中的数据进行增、 删、 改、 查操作

字典需要的特征

1、O(1)的时间复杂度取出或插入关联值

2、key 唯一,key类型可以是各种类型, value也可以是各种类型

根据特征如何设计

1、实现O(1)时间复杂度,那就得用数组,因为Redis是C实现的,所以用数组是最好的选择

2、有了数组,应该怎么将不同的key映射到数组的下标呢?

哈希算法:

1) 相同的输入经Hash计算后得出相同输出;
2) 不同的输入经Hash计算后一般得出不同输出值, 但也可能会出现相同输出值。

所以需要有一个强随机分布的hash函数

Daniel J.Bernstein在comp.lang.c上发布的“times 33”散列函数

Redis 哈希函数的实现使用siphash


uint64_t dictGenHashFunction(const void *key, int len) {return siphash(key,len,dict_hash_function_seed);
}uint64_t siphash(const uint8_t *in, const size_t inlen, const uint8_t *k) {
#ifndef UNALIGNED_LE_CPUuint64_t hash;uint8_t *out = (uint8_t*) &hash;
#endifuint64_t v0 = 0x736f6d6570736575ULL;uint64_t v1 = 0x646f72616e646f6dULL;uint64_t v2 = 0x6c7967656e657261ULL;uint64_t v3 = 0x7465646279746573ULL;uint64_t k0 = U8TO64_LE(k);uint64_t k1 = U8TO64_LE(k + 8);uint64_t m;const uint8_t *end = in + inlen - (inlen % sizeof(uint64_t));const int left = inlen & 7;uint64_t b = ((uint64_t)inlen) << 56;v3 ^= k1;v2 ^= k0;v1 ^= k1;v0 ^= k0;for (; in != end; in += 8) {m = U8TO64_LE(in);v3 ^= m;SIPROUND;v0 ^= m;}switch (left) {case 7: b |= ((uint64_t)in[6]) << 48; /* fall-thru */case 6: b |= ((uint64_t)in[5]) << 40; /* fall-thru */case 5: b |= ((uint64_t)in[4]) << 32; /* fall-thru */case 4: b |= ((uint64_t)in[3]) << 24; /* fall-thru */case 3: b |= ((uint64_t)in[2]) << 16; /* fall-thru */case 2: b |= ((uint64_t)in[1]) << 8; /* fall-thru */case 1: b |= ((uint64_t)in[0]); break;case 0: break;}v3 ^= b;SIPROUND;v0 ^= b;v2 ^= 0xff;SIPROUND;SIPROUND;b = v0 ^ v1 ^ v2 ^ v3;
#ifndef UNALIGNED_LE_CPUU64TO8_LE(out, b);return hash;
#elsereturn b;
#endif
}

这时,无论是什么类型的键值,在redis都是字符串,这样都能通过哈希函数获取到对应的64位的数值,这个数值再对数组求余就是数组的下标。

冲突解决:

如果出现了冲突,那么就需要键的结构是个链表

typedef struct dictEntry {void *key;void *val;struct dictEntry *next;
} dictEntry;

大概的字典的存储结构:

所以就可以大概定义字典哈希表的结构了

typedef struct dictht {dictEntry **table;      // 指针数组, 用于存储键值对,是个二维的unsigned long size;     // 大小为2^nunsigned long sizemask; // 2^n -1unsigned long used;     // 已经存在的元素个数,二维格子中已存在的键值对个数
} dictht;

一个数对size求余,相当于对sizemask 位&,位运算速度快于求余。这就是sizemask的作用,加速

这就是hash表的定义。

rehash是怎么实现?

数组的大小需要可变,首次需要扩容,第一次的大小位4,后续根据每次都是加倍扩容。如果没有使用,需要缩容,这就是rehash。

具体实现:

1、新分配一个新的ht[0] 两倍大小的内存给ht[1],并置为dict的rehash标志

2、新添加的键添加到新的hash表ht[1]中,而查找,删除,修改,则都要到两个中检查,因为有可能再ht[0],也有可能再ht[1]中

3、ht[0]的所有值重新计算放到ht[1]中,并删除就的ht[0]的键值

4、完成后对调ht[0]和ht[1]指针。

rehash如果容量很大的情形,则要利用分治思想,不能一次性把所有数据rehash,这样会导致redis服务不可用。

dictRehashStep提供了对一个键值的rehash

static void _dictRehashStep(dict *d) {if (d->iterators == 0) dictRehash(d,1);
}

真正的rehash实现

int dictRehash(dict *d, int n) {int empty_visits = n*10; // 最大可以访问多少个空桶if (!dictIsRehashing(d)) return 0;while(n-- && d->ht[0].used != 0) {dictEntry *de, *nextde;/* Note that rehashidx can't overflow as we are sure there are more* elements because ht[0].used != 0 */assert(d->ht[0].size > (unsigned long)d->rehashidx);while(d->ht[0].table[d->rehashidx] == NULL) {d->rehashidx++;if (--empty_visits == 0) return 1;}de = d->ht[0].table[d->rehashidx];/* Move all the keys in this bucket from the old to the new hash HT */while(de) {uint64_t h;nextde = de->next;/* Get the index in the new hash table */h = dictHashKey(d, de->key) & d->ht[1].sizemask;de->next = d->ht[1].table[h];d->ht[1].table[h] = de;d->ht[0].used--;d->ht[1].used++;de = nextde;}d->ht[0].table[d->rehashidx] = NULL;d->rehashidx++;}/* Check if we already rehashed the whole table... */if (d->ht[0].used == 0) {zfree(d->ht[0].table);d->ht[0] = d->ht[1];_dictReset(&d->ht[1]);d->rehashidx = -1;return 0;}/* More to rehash... */return 1;
}

其中empty_visits,是防止如果再rehash的过程中,比如由一片连续的空捅,也就是空数组,而如果这时候是步进的访问的话,可能很多步都访问不到数据,而如果一直查找下去的话,又有可能在数据量大的时候会查找很久,所以就是入参n的10倍,比如当前要rehash 1个,但是走了10个桶,发现还是空的,就返回了,不一直找下去。这也是为了兼顾性能的折中方案。

incrementallyRehash

N次rehash操作后, 整个ht[0]的数据都会迁移到ht[1]中, 这样做的好处就把是本应集中处理
的时间分散到了上百万、 千万、 亿次操作中, 所以其耗时可忽略不计

int dictRehashMilliseconds(dict *d, int ms) {long long start = timeInMilliseconds();int rehashes = 0;while(dictRehash(d,100)) {rehashes += 100;if (timeInMilliseconds()-start > ms) break;}return rehashes;
}

有了hash的概念,实现字典就比较容易了

字典实现

typedef struct dict {dictType *type;void *privdata;    // 该字典依赖的数据,配合type使用dictht ht[2];      // 字典扩容缩容使用long rehashidx;    // rehash标识。 默认值为-1, 代表没进行rehash操作unsigned long iterators;  // 当前运行的迭代器数
} dict;

其中:

terators字段, 用来记录当前运行的安全迭代器数, 当有安全迭代器绑定到该字典时, 会暂停rehash操作。 Redis很多场景下都会用到迭代器, 例如: 执行keys命令会创建一个安全迭代器, 此时iterators会加1, 命令执行完毕则减1, 而执行sort命令时会创建普通迭代器,该字段不会改变

一个典型的字典图

看下来,其中数据结构的定义就到这里。

实现:

1、创建dictCreate,初始化。

dict *dictCreate(dictType *type,void *privDataPtr)
{dict *d = zmalloc(sizeof(*d));_dictInit(d,type,privDataPtr);return d;
}static int _dictInit(dict *ht, dictType *type, void *privDataPtr) {_dictReset(ht);ht->type = type;ht->privdata = privDataPtr;return DICT_OK;
}static void _dictReset(dict *ht) {ht->table = NULL;ht->size = 0;ht->sizemask = 0;ht->used = 0;
}

2、添加

其中应该有个概念就是rehash,这是我们首先假定我们一开始就是数组,数组总有不够的时候,不够的时候扩容或者太多的时候缩容,都是交rehash操作。

rehash会使用到了dict结构的ht[1],这就是为什么dict结构里面需要两个哈希表的原因。

int dictAdd(dict *d, void *key, void *val)
{dictEntry *entry = dictAddRaw(d,key,NULL);if (!entry) return DICT_ERR;dictSetVal(d, entry, val);return DICT_OK;
}dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{long index;dictEntry *entry;dictht *ht;/*该字典是否在进行rehash操作 进行一步rehash*/if (dictIsRehashing(d)) _dictRehashStep(d);/* 获取新节点的洗标 如果等于-1 则代表存在,返回空,并把老节点存入existing*/if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)return NULL;// 分配新节点的内存,加入到节点的top,假定新加入的节点使用的频率更高ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];entry = zmalloc(sizeof(*entry));entry->next = ht->table[index];ht->table[index] = entry;ht->used++;/* Set the hash entry fields. */dictSetKey(d, entry, key);return entry;
}

3、查找元素

dictEntry *dictFind(dict *d, const void *key)
{dictEntry *he;uint64_t h, idx, table;if (d->ht[0].used + d->ht[1].used == 0) return NULL; /* dict is empty */if (dictIsRehashing(d)) _dictRehashStep(d);h = dictHashKey(d, key);for (table = 0; table <= 1; table++) {idx = h & d->ht[table].sizemask;he = d->ht[table].table[idx];while(he) {if (key==he->key || dictCompareKeys(d, key, he->key))return he;he = he->next;}if (!dictIsRehashing(d)) return NULL;}return NULL;
}

查找过程如果发现正在rehash,则进行一次rehash,分别在两个哈希表中取值

void dbOverwrite(redisDb *db, robj *key, robj *val) {dictEntry *de = dictFind(db->dict,key->ptr);serverAssertWithInfo(NULL,key,de != NULL);dictEntry auxentry = *de;robj *old = dictGetVal(de);if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {val->lru = old->lru;}dictSetVal(db->dict, de, val);if (server.lazyfree_lazy_server_del) {freeObjAsync(old);dictSetVal(db->dict, &auxentry, NULL);}dictFreeVal(db->dict, &auxentry);
}

4、删除

int dictDelete(dict *ht, const void *key) {return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {uint64_t h, idx;dictEntry *he, *prevHe;int table;if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;if (dictIsRehashing(d)) _dictRehashStep(d);h = dictHashKey(d, key);for (table = 0; table <= 1; table++) {idx = h & d->ht[table].sizemask;he = d->ht[table].table[idx];prevHe = NULL;while(he) {if (key==he->key || dictCompareKeys(d, key, he->key)) {/* Unlink the element from the list */if (prevHe)prevHe->next = he->next;elsed->ht[table].table[idx] = he->next;if (!nofree) {dictFreeKey(d, he);dictFreeVal(d, he);zfree(he);}d->ht[table].used--;return he;}prevHe = he;he = he->next;}if (!dictIsRehashing(d)) break;}return NULL; /* not found */
}

Redis 源码分析-数据结构及实现(字典dict)相关推荐

  1. Redis源码分析:基础概念介绍与启动概述

    Redis源码分析 基于Redis-5.0.4版本,进行基础的源码分析,主要就是分析一些平常使用过程中的内容.仅作为相关内容的学习记录,有关Redis源码学习阅读比较广泛的便是<Redis设计与 ...

  2. redis源码分析 -- cs结构之服务器

    服务器与客户端是如何交互的 redis客户端向服务器发送命令请求,服务器接收到客户端发送的命令请求之后,读取解析命令,并执行命令,同时将命令执行结果返回给客户端. 客户端与服务器交互的代码流程如下图所 ...

  3. Redis源码分析(一)redis.c //redis-server.c

    Redis源码分析(一)redis.c //redis-server.c 入口函数 int main() 4450 int main(int argc, char **argv) {4451 init ...

  4. 10年大厂程序员是如何高效学习使用redis的丨redis源码分析丨redis存储原理

    10年大厂程序员是怎么学习使用redis的 1. redis存储原理分析 2. redis源码学习分享 3. redis跳表和B+树详细对比分析 视频讲解如下,点击观看: 10年大厂程序员是如何高效学 ...

  5. Redis源码分析 —— 发布与订阅

    前言 通过阅读Redis源码,配合GDB和抓包等调试手段,分析Redis发布订阅的实现原理,思考相关问题. 源码版本:Redis 6.0.10 思考问题 发布订阅基本概念介绍 订阅频道 -- SUBS ...

  6. Redis源码分析(一)--Redis结构解析

    从今天起,本人将会展开对Redis源码的学习,Redis的代码规模比较小,非常适合学习,是一份非常不错的学习资料,数了一下大概100个文件左右的样子,用的是C语言写的.希望最终能把他啃完吧,C语言好久 ...

  7. Redis 源码分析之故障转移

    在 Redis cluster 中故障转移是个很重要的功能,下面就从故障发现到故障转移整个流程做一下详细分析. 故障检测 PFAIL 标记 集群中每个节点都会定期向其他节点发送 PING 消息,以此来 ...

  8. Redis源码分析之PSYNC同步

    Redis master-slave 同步源码分析 (1)slave 流程分析 (2)master 流程分析 Slave 分析 当Redis 启动后,会每隔 1s 调用 replicationCron ...

  9. Redis源码分析之工具类util

    在redis源码中的辅助工具类中,主要包括大小端转换.SHA算法以及util.h中对应的算法. 大小端转换: LittleEndian:低位字节数据存放于低地址,高位字节数据存放于高地址. BigEn ...

最新文章

  1. boost::hana::index_if用法的测试程序
  2. 注意指针修饰符的准确含义
  3. Java线程:保留的内存分析
  4. “入乡随俗,服务为主” 发明者量化兼容麦语言啦!
  5. 基于深度学习的场景分割算法研究综述
  6. Cake Frosting:更具可维护性的C#DevOps
  7. linux安装多版本php_linux如何安装多个php版本
  8. 关于vue2.0+hbuilder打包移动端app之后空白页面的解决方案
  9. [转载] NumPy 基本操作(ndarray通用函数 / 常用函数)
  10. Pycharm 设置python文件自动生成头部信息模板
  11. 大 Θ记号、大 Ω记号、空间复杂度、时间复杂度
  12. 教你如何安装字体包 ——思源免费商用字体
  13. Longest Common Prefix_LeetCode
  14. C# 图片位深度转至8位灰度图像,8位灰度图像转为1位灰度图像
  15. NPV(净现值)是什么以及其对应的概念及公式是什么?使用Python计算NPV是如何实现的?
  16. Java栈的实现数组和链表
  17. 【TCP专题】TCP连接断开
  18. 【资源】OpenCV3编程入门_毛星云
  19. 论遇到事情的沉着与冷静
  20. java去除图片水印的解决办法

热门文章

  1. 【大咖云集!】第四届计算机信息科学与应用技术国际学术会议(CISAT 2021)诚邀参与...
  2. css鼠标经过图片镜像翻转,css代码实现鼠标指向图片翻转效果
  3. [东邪西毒][程序员版][原版][剧情]
  4. 学习Linux命令(32)
  5. 淘宝/天猫买家信息 API接口
  6. 硬盘只要存入大文件就会卡住,然后就读取不出了,这是怎么回事?
  7. 怀念与我同龄的月季花
  8. leetcode系列-1002.查找共用字符
  9. CFA课程打卡-2019.11.20
  10. win10 设备管理器中的黄色感叹号(华硕)