哈希函数简介

哈希函数(hash function),又叫散列函数,哈希算法。散列函数把数据“压缩”成摘要,有的也叫”指纹“,它使数据量变小且数据格式大小也固定。

哈希函数将数据打乱混合,重新创建一个散列值。

我们经常用到的对用户登录密码加密,比如 md5 算法,其实就是一个散列函数。

value = hash_function(input_data),value 这个计算出来的值是大小固定的。

md5("hashmd5") = 46BD4AA9F79D359530D3D873BAC6F3DC,32 位的 md5 值。

当然也有 16 位的 md5 值。

经过哈希函数计算的散列值,会不会出现散列值相同情况?

当然会,这个就是散列值冲突

所以一个好的哈希函数就很重要,要尽量避免出现散列值冲突。

常用的哈希算法:md5,sha-1,sha-256,sha-512 等等。

哈希表简介

哈希表可以有很多英文名称,比如 hashtable,hashmap,symbol table,map 等等,英文名称虽然不同,但是数据结构基本差不多。

在 map 中,就是一种映射关系。一般保存 key:value 的键值对映射关系。

在哈希表中,key 经过哈希函数计算后存储到哈希表中,然后与 value 值关联对应。

哈希表的结构组成:数组array + 链表list。是一个组合结构。

比如:key:value 值,数组用来存储 key 经过哈希函数计算后的值与数组长度取余后的值,链表存储 key:value 值。

如下图:

上图为什么是 2 个 key:val 在一起?

其实这就是 hash 冲突了,用链地址表来解决哈希冲突的问题。

Redis中的哈希表和字典dict

1. 哈希表各结构定义

哈希表dictht

redis3.0 中的哈希表叫 dictht,dictht 的定义:

// https://github.com/redis/redis/blob/3.0/src/dict.h#L69

/* 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 dictht { // 哈希表
dictEntry **table; // 哈希表的数组,数组中每个元素都是指针,指向 dictEntry 结构
unsigned long size; // 哈希表的大小,table 数组的大小
unsigned long sizemask; // 哈希表掩码,用于计算索引值,等于 size-1
unsigned long used; // 哈希表已有的节点(键值对)数量
} dictht;

哈希表节点dictEntry

哈希表节点,有的地方取名为哈希桶 bucket,节点 Node 等等,不过表达意思是一样的。

上面 redis3.0 哈希表 dictht 里的节点 dictEntry 是怎么定义? 代码如下:

// https://github.com/redis/redis/blob/3.0/src/dict.h#L47
typedef struct dictEntry {
void *key; // 键 key
union { // 值 val
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 指向下一个哈希表节点,链表法解决hash冲突
} dictEntry;

key 属性保存键值对中的键,v 属性保存键值对中的值,其中这个 v 值可能是一个指针,或者是一个 uint64_t 整数,或者是 int64_t 整数,或是 double 类型浮点数。

dictEnty 表节点和 dictht 哈希表结构关系如下图:

next:指向下一个哈希节点,用链表法来解决哈希冲突。

hash冲突:

上面的 dictEntry 结构里的属性 next 就是解决这个哈希键冲突问题的。

有冲突的值,就用链表来记录下一个值。

哈希算法

Redis 中计算哈希值的哈希函数有好几个。

  1. dictIntHashFunction 计算整型类型哈希值的哈希函数

    unsigned int dictIntHashFunction(unsigned int key)
  2. dictGenHashFunction MurmurHash2 哈希算法, by Austin Appleby,用于计算字符串的哈希值的哈希函数

    unsigned int dictGenHashFunction(const void *key, int len)
  3. dictGenCaseHashFunction djb 哈希算法,大小写敏感的哈希函数

    /* And a case insensitive hash function (based on djb hash) */
    unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len)

2. 字典dict

字典dict

上面我们已经了解,在 Redis 中用 dictht 来表示哈希表,但是,在使用哈希表时,Redis 又定义了一个字典 dict 的数据结构。

为什么要再定义一个 dict 结构?

  • 为了扩展哈希表(rehash)的时候,能够方面的操作哈希表。为此里面定义了 2 个哈希表 ht[2]。

字典 dict.h/dict 结构定义:

typedef struct dict {
dictType *type; // 指针,指向dictType 结构,dictType 中包含很多自定义函数,见下面
void *privdata; // 私有数据,保存dictType结构中的函数参数
dictht ht[2]; // hash表,ht[2] 表示有2张表
long rehashidx; /* rehashing not in progress if rehashidx == -1 *///rehash 标识,rehashidx=-1,没进行rehash
int iterators; /* number of iterators currently running */// 正在运行的迭代器数量
} dict;

*type:保存了很多函数,这些函数是操作特定类型键值对的函数,Redis 会为用途不同的字典设置不同类型特定函数。

ht[2]:包含 2 个 dictht哈希表,为什么有2张表?rehash 时会用到 ht[1]。一般情况下只使用 ht[0]。

rehashidx:这个属性与 rehash 有关,记录 rehash 目前的进度,如果目前没有进行 rehash,那么 rehashidx=-1。

dict.h/dictType 结构:

typedef struct dictType {
unsigned int (*hashFunction)(const void *key); // 计算哈希值的函数
void *(*keyDup)(void *privdata, const void *key);// 复制键的函数
void *(*valDup)(void *privdata, const void *obj); // 复制值函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 对比键的函数
void (*keyDestructor)(void *privdata, void *key); // 销毁键的函数
void (*valDestructor)(void *privdata, void *obj); // 销毁值的函数
} dictType;

字典 dict 图示:

3. rehash

a. 什么是 rehash ?

  • 扩大或缩小哈希表容量。

b. 为什么有 rehash ?

  • 当哈希表的数据量持续增长,而哈希表容量大小固定时,就可能会有 2 个或以上数量的键被分配到哈希表数组的同一个索引上,于是就发生了冲突(collision)。
  • 当然冲突可以用链表法(separate chaining)解决,但是为了哈希表的性能,要尽量避免冲突,就要对哈希表进行扩容或缩容。

哈希表中有一个负载因子(load factor)的概念:

负载因子 = 哈希表已保存的键值对数量(使用的数量) / 哈希表的长度

load_factor = ht[0].used / ht[0].size

这个负载因子的概念是用来衡量哈希表容量大小情况的。哈希表中的键值对数量少,负载因子也小。

当负载因子超过某个阙值时,为了维持哈希的容量在一定合理范围,就会对哈希表容量进行 resize 操作:

  1. 扩大哈希表容量
  2. 缩小哈希表容量

c. 什么时候进行扩容和缩容操作?

  • 扩容条件

    满足下面任一条件都会触发哈希表扩容

    1. 服务器目前没有执行 bgsave 命令,或 bgrewriteaof 命令,并且哈希表的负载因子 >=1

    2. 服务器目前在执行 bgsave 命令,或 bgrewriteaof 命令并且哈希表的负载因子 >5

  • 缩容条件

    1. 哈希表的负载因子 < 0.1

d. 怎么操作扩容和缩容?

也就是说扩容和缩容的操作步骤是什么?

  1. 为字典 ht[1] 分配内存空间,空间大小取决于要执行的操作,以及当前 ht[0] 的键值对数量

    • 如果是扩容操作,那么 ht[1] 的空间大小等于第一个 ht[0].used * 2 的 2^n(2的n次幂)

    • 如果是缩容操作,那么 ht[1] 的空间大小等于第一个 ht[0].used 的 2^n(2的n次幂)

  2. 将 ht[0] 上所有键值重新计算哈希值和索引值后存放到 ht[1] 对应位置上

  3. 当 ht[0] 上所有的键值移动到 ht[1] 后,释放 ht[0],将 ht[1] 变成 ht[0],并在 ht[1] 上新建一个空哈希表

扩容代码简析:

_dictExpandIfNeeded

// https://github.com/redis/redis/blob/3.0/src/dict.c#L923

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
/* Incremental rehashing already in progress. Return. */
if (dictIsRehashing(d)) return DICT_OK; // 如果正在进行rehash,则返回 /* If the hash table is empty expand it to the initial size. */
// 如果 ht[0] 为空,则创建并初始化ht[0],然后返回
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); /* If we reached the 1:1 ratio, and we are allowed to resize the hash
* table (global setting) or we should avoid it but the ratio between
* elements/buckets is over the "safe" threshold, we resize doubling
* the number of buckets. */
/*当 (ht[0].used/ht[0].size)>=1 并且,
满足dict_can_resize=1或ht[0].used/ht[0].size>5时,对字典进行扩容*/
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
return dictExpand(d, d->ht[0].used*2);
}
return DICT_OK;
} // https://github.com/redis/redis/blob/3.0/src/dict.c#L58
static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;

dictExpand:

// https://github.com/redis/redis/blob/3.0/src/dict.c#L204
/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
dictht n; /* the new hash table 新建一个哈希表*/
unsigned long realsize = _dictNextPower(size); // 计算扩容或缩容新版哈希表大小 /* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
// 如果哈希表正在rehash或新建哈希表大小小于现已使用的,则返回错误
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR; /* Rehashing to the same table size is not useful. */
if (realsize == d->ht[0].size) return DICT_ERR; /* Allocate the new hash table and initialize all pointers to NULL */
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.used = 0; /* Is this the first initialization? If so it's not really a rehashing
* we just set the first hash table so that it can accept keys. */
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
} /* Prepare a second hash table for incremental rehashing */
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}

缩容操作:

dictResize

// https://github.com/redis/redis/blob/3.0/src/dict.c#L192
int dictResize(dict *d)
{
int minimal; // dict_can_resize 在 https://github.com/redis/redis/blob/3.0/src/dict.c#L58 这里是设置为 1,如果为0就返回,不进行后面操心
// 或者 dictIsRehashig() 真正进行rehash操心,也返回不rehash操作
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used; // 获得已经使用ht的数量
if (minimal < DICT_HT_INITIAL_SIZE) // 这个最小值不能小于 DICT_HT_INITIAL_SIZE = 4
minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal); // 用dictExpand函数调整字典大小
} // https://github.com/redis/redis/blob/3.0/src/dict.h#L100
/* This is the initial size of every hash table */
#define DICT_HT_INITIAL_SIZE 4

参考

Redis原理再学习04:数据结构-哈希表hash表(dict字典)的更多相关文章

  1. Redis原理再学习05:数据结构-整数集合intset

    intset介绍 intset 整数集合,当一个集合只有整数元素,且元素数量不多时,Redis 就会用整数集合作为集合键的底层实现. redis> SADD numbers 1 3 5 7 9 ...

  2. 聊聊Mysql索引和redis跳表 ---redis的有序集合zset数据结构底层采用了跳表原理 时间复杂度O(logn)(阿里)

    redis使用跳表不用B+数的原因是:redis是内存数据库,而B+树纯粹是为了mysql这种IO数据库准备的.B+树的每个节点的数量都是一个mysql分区页的大小(阿里面试) 还有个几个姊妹篇:介绍 ...

  3. 【Redis】命令学习笔记——键(key)(20个超全字典版)

    安装完redis和redis-desktop-manager后,开始学习命令啦!本篇基于redis 4.0.11版本,从对键(key)开始挖坑! 准备工作,使用db1(默认db0,由于之前练习用db0 ...

  4. Redis 常用命令学习三:哈希类型命令

    1.赋值与取值命令 127.0.0.1:6379> hset stu name qiao (integer) 1 127.0.0.1:6379> hset stu sex man (int ...

  5. TCPL学习毕节:第六章hash表

    对于P126的哈希表构成: struct nlist *install(char *name, char *defn) { struct nlist *np; unsigned hashval; if ...

  6. 【Redis】命令学习笔记——字符串(String)(23个超全字典版)

    Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合). 本篇基于redis 4.0.11版本,学习字符串( ...

  7. 【数据结构】Hash表

    [数据结构]Hash表 Hash表也叫散列表,是一种线性数据结构.在一般情况下,可以用o(1)的时间复杂度进行数据的增删改查.在Java开发语言中,HashMap的底层就是一个散列表. 1. 什么是H ...

  8. redis为何单线程 效率还这么高 为何使用跳表不使用B+树做索引(阿里)

    如果想了解 redis 与Memcache的区别参考:Redis和Memcache的区别总结 阿里的面试官问问我为何redis 使用跳表做索引,却不是用B+树做索引 因为B+树的原理是 叶子节点存储数 ...

  9. 哈希表(Hash)的应用

    $hs=@() #定义数组 $hs=@{} #定义Hash表,使用哈希表的键可以直接访问对应的值,如 $hs["王五"] 或者 $hs.王五 的值为 75 $hs=@''@ #定义 ...

随机推荐

  1. JS调用堆栈

    调用栈 JavaScript 是一门单线程的语言,这意味着它只有一个调用栈,因此,它同一时间只能做一件事.如果我们运行到一个函数,它就会将其放置到栈顶.当从这个函数返回的时候,就会将这个函数从栈顶弹出 ...

  2. 【刷题-LeetCode】122 Best Time to Buy and Sell Stock II

    Best Time to Buy and Sell Stock II Say you have an array for which the ith element is the price of a ...

  3. Cesium中级教程7 - Geometry and Appearances 几何图形和外观

    Cesium中文网:http://cesiumcn.org/ | 国内快速访问:http://cesium.coinidea.com/ 本教程将向您介绍提供使用Primitive API的几何图形和外 ...

  4. java有四种访问权限

    Java面向对象的封装性是通过对成员变量和方法进行访问控制实现的,访问控制分为4个等级:私有.默认.保护和公有,具体规则如下表:

  5. cnpm安装教程

    安装cnpm,输入以下命令: sudo npm install -g cnpm --registry=https://registry.npm.taobao.org 输入cnpm -v ,检测是否正常 ...

  6. mongo用户认证

    mongo@rayos:/opt/mongodb$ mongo --port 28017 MongoDB shell version v4.0.13 connecting to: mongodb:// ...

  7. MySQL开发篇,存储引擎的选择真的很重要吗?

    前言 谁说MySQL查询千万级别的数据很拉跨?我今天就要好好的和你拉拉家常,畅谈到深夜,一起过除夕!这篇文章也是年前的最后一篇,希望能带给大家些许收获,不知不觉查找文档和参考实体书籍就写了这么多,自己 ...

  8. python matplotlib通过 plt.scatter在图上画圆

    import matplotlib.pyplot as plt lena = mpimg.imread(r'C:\Users\Administrator.WIN-QV9HPTF0DHS\Desktop ...

  9. APC 篇——总结与提升

    写在前面   此系列是本人一个字一个字码出来的,包括示例和实验截图.由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新. 如有好的建议,欢迎反馈.码字不易, ...

  10. Java程序设计(2021春)——第五章输入输出笔记与思考

    Java程序设计(2021春)--第五章输入输出笔记与思考 本章概览: 异常处理简介 程序运行时,环境.操作等可能出现各种错误.故障,我们希望程序具有容错能力,给出错误信息.面向对象的程序设计里有异常 ...