【引入】如果有如下键值对,如何存储和查询呢?
1:西瓜
2:苹果
3:桔子
使用数组arr[N]简单存储:
西瓜 | 苹果 | 桔子 |
存储时,将key值作为index,通过偏移,直接得到存储地点,将其赋值:
arr[1] = "西瓜";
arr[2] = "苹果";
arr[3] = "桔子";
查询时,将key值作为index,直接获取数据数据:
return arr[index];
这是非常高效的查询方式,无需逐个比较,直接偏移index个元素。
如果存储内容复杂一点,比如,记录下小朋友喜欢吃的水果,那上面的方法就不好用了:
小明:西瓜
小红:苹果
小猴:桔子
key已经不再是简单的index,不能直接用作数组的index了。
于是,需要通过某种方法,将key转换成简单的index:
PUT:
index = Hash("小明");
arr[index] = "西瓜";
GET:
index = Hash("小明");
return arr[index];
以上就是hashMAP的本质。
“Hash()”这个方法实现,暂时不做深究,这里列举常用的方法,有兴趣可以深入了解其实现原理:分离连接法、开放定址法、线性探测法、平方探测法。
通常,hash得到的是一串很大的数,而数组容量有限,通常处理如下:
PUT:
index = Hash("小明");
index = index % ARR_MAX_LEN;
arr[index] = "西瓜";
GET:
index = Hash("小明");
index = index % ARR_MAX_LEN;
return arr[index];
有几个问题:
一、不同的key最终可能得到相同的index,比如,“小明”哈希之后得到1,“小红”哈希之后得到11,而数组容量是10,“小明”和“小红”最终得到的index都是1。这种现象称之为“哈希冲突”;
二、模运算比较低效,如果查询特别频繁,这个效率比较低下。
三、数组的容量不好确定。初始设置太大,造成浪费;容量太小,会因为冲突太多,造成查询低效。
解决一、“哈希冲突”的解决方法有很多,常用的,是将index相同的数据,以链表形式挂到相应数组桶上。
解决二、模运算则通常被位运算取代:
PUT:
index = Hash("小明");
index = index & (ARR_MAX_LEN-1); // 使用位运算替代模运算
arr[index] = "西瓜";
GET:
index = Hash("小明");
index = index & (ARR_MAX_LEN-1); // 使用位运算替代模运算
return arr[index];
解决三、数组容量动态调整,初始为16,当数据量达到阈值(一般为容量的两倍)时,自动扩容,并对所有数据重新哈希。
typedef struct hashmap_s {
unsigned long capability;
unsigned long used;
void **arr;
} hashmap_t;
hashmap_t hashmap;
bool hash_put (char *key, char *val) {
if (hashmap->capability > (hashmap->used)<<1) {
index = Hash(key);
index = index & (hashmap->capability-1);
hashmap->arr[index] = val;
return true;
}
//rehash
// malloc (hashmap->capability << 1) && 更新capability、used、arr
// 将原来数据全部重新哈希,并放入新arr指向的空间。
}
char *hash_get (char *key) {
index = Hash(key);
index = index & (hashmap->capability-1);
return hashmap->arr[index]; // 需要检查该空间是否曾被插入过数据
}
重哈希时,会导致本次查询速度很慢,redis中使用了分治策略,即,如果需要扩容,则生成第二张表:table2,在table 1中查询数据,查到则将该数据重hash并移动到table 2。经过无数次查询,总有一次,table 1中的数据全部迁移至table 2中,然后释放table1,table2替代table1。
同时,重哈希时,会导致内存使用增加(扩了一张更大的表),如果此时redis在执行某些占用大量内存的操作(比如备份),可能导致内存紧张。所以,在重哈希时,如果发现正在执行其他占用大量内存的操作,则尽可能延后重哈希。即,在(used > 4 * capability)时才执行重哈希。
为什么初始值是16呢?以及之后都是以2的幂次扩容呢?
因为2的幂次作为capability,那么(capability-1)转化为二进制会变成全1:
16-1=15
15的二进制:1111
index&1111B,可以保证每个桶都被使用到,这样冲突会最少。