《Redis源码学习笔记》数据结构-字典

转载自:http://diaocow.iteye.com/blog/1935091


要看懂redis代码,其中重要的一步就是要看懂它里面所使用的数据结构,而在这不算少的数据结构中,最重要的就是字典,它几乎就是redis实现各种功能的骨架,所以理解好字典至关重要! 

redis作为一个nosql数据库,所有的key-value都是存储在一个字典中,而字典则是用哈希表实现的;关于哈希表原理,随便上网查一下都能找到一大堆资料,因此这里我也不想做过多赘述,直接开门见山,看下在redis中哈希表是什么样的: 


上图所示结构对应代码如下: 

C代码   收藏代码
  1. // 字典  
  2. typedef struct dict {  
  3.     // 哈希表(2个)  
  4.     dictht ht[2];   
  5.     // rehash索引,若rehashidx == -1,则表示未开始进行rehash,  
  6.     // 否则rehashidx的值表示rehash正进行到ht[0]这个hash表上哪个索引节点  
  7.     int rehashidx;    
  8.     // 安全迭代器数量  
  9.     int iterators;   
  10. } dict;  
  11.   
  12. // 哈希表  
  13. typedef struct dictht {  
  14.     // 哈希表 (指向一个dictEntry*数组,俗称“桶”)  
  15.     dictEntry **table;        
  16.     // 哈希表大小  
  17.     unsigned long size;       
  18.     // 位掩码,通过hash_value & sizemask得出节点在哈希表索引  
  19.     unsigned long sizemask;   
  20.     // 哈希表中节点数量  
  21.     unsigned long used;       
  22. } dictht;  
  23.   
  24. // 哈希节点  
  25. typedef struct dictEntry {  
  26.     // 键  
  27.     void *key;  
  28.     // 值  
  29.     union {  
  30.         void *val;  
  31.         uint64_t u64;  
  32.         int64_t s64;  
  33.     } v;  
  34.     // 指向下一个节点  
  35.     struct dictEntry *next;   
  36. } dictEntry;  


关于redis中的字典,最特别的无疑是dict中维护着两个哈希表(ht[0],ht[1]),为什么要有两个呢?在解释这个之前我们先看下哈希表的rehash; 

rehash目的:  
当我们不断的往哈希表(ht[0])插入新的键值对,如果两个键的hash值相同,那么它们将以链表的形式放入到同一个“桶”中,如下图key1和key4: 



这样带来的问题就是,随着我们往哈希表里插入越来越多的键值对,哈希表性能会急剧下降(查找操作都退化成链表查找);所以,我们就需要扩大原来的哈希表,使得哈希表大小和哈希表中的节点数的比例能够维持在1:1(dictht.size:dictht.used),这时候哈希表才能达到最佳查询性能O(1) 

rehash过程:  
创建一个新的哈希表,大小是当前的两倍(准确说还必须是2的幂次),然后把全部键值对重新散列到新的哈希表中,最后再用它替换原来的哈希表; 

rehash问题:  
我们考虑下面一种情况:客户端A插入一个键值对,这时候发现dictht.used与dictht.size的比例大于1(查询性能开始下降),于是执行rehash操作,假设目前哈希表中有10万个键值对,那么redis就会一直埋头苦干,直到完成对这个10万个键值对的rehash操作,并且在这个过程中,其他客户端请求都会被阻塞(因为redis是单线程);很显然我们是无法忍受这种情况的发生,那redis是如何解决这个问题呢? 

渐进式rehash:  
“渐进式”意味着rehash过程不是一次做完而是每次做一点,这样就可以避免由于rehash过程太久导致其他客户端请求被阻塞,具体过程如下: 

1. 在ht[1]上分配一个更大的哈希表; 
2. “分多次”把ht[0]上的键值对重新散列到ht[1]上; 
3. 当处理完所有键值对时,让ht[0]指向新的哈希表; 



现在还有一个问题,我们说了 “分多次把ht[0]上的键值对重新散列到ht[1]上” ,那么这个分多次究竟是多少次?并且每次处理多少键值对才最合适? 

redis准备rehash时,会把dict.rehashidx置为0(标示rehash开始),然后当执行任意一个哈希表操作(添加,删除,查找等),就会执行一次_dictRehashStep函数; 

_dictRehashStep函数每次rehash把ht[0]上的第一个不为空索引上的全部键值对迁移到ht[1]上,并且用dict.rehashidx的值标示当前rehash正进行到了哪个索引; 

也就是说按照上图,第一次迁移key1,key4键值对(这时候dict.rehashidx的值为0),第二次迁移key2键值对(这时候dict.rehashidx的值为1),第三次迁移key3键值对(这时候dict.rehashidx的值为2),至此rehash完毕(dict.rehashidx被复位成-1),相关伪代码: 
Python代码   收藏代码
  1. # 任意dict操作(添加,删除,查找)  
  2. def anyDictOperation(dict):  
  3.     # 如果正在进行rehash  
  4.     if dict.rehashidx != -1:   
  5.         _dictRehashStep(dict)  
  6.     # 执行字典操作  
  7.     dictOperation(dict)  
  8.           
  9. def _dictRehashStep(dict):  
  10.     # 如果当前安全迭代器数量不为0,暂停此次rehash  
  11.     if dict.iterators > 0 : return  
  12.   
  13.     idx = dict.rehashidx  
  14.     # 获取第一个不为空的索引  
  15.     while len(dict.ht[0].table[idx]) <= 0:  
  16.         idx++  
  17.   
  18.     # 迁移该索引节点上的所有键值对到ht[1]上   
  19.     for key, val in dict.ht[0].table[idx].getKeyValuePairs:  
  20.         redisServer.ht[0].table.used--  
  21.         redisServer.ht[1].table.used++  
  22.         redisServer.ht[1].table.hash(key, val)    
  23.       
  24.     # 当所有键值对迁移完毕,用新的哈希表替换老的,并且重置ht[1]  
  25.     if dict.ht[0].table.used == 0:  
  26.         dict.ht[0] = dict.ht[1]  
  27.         dict.ht[1].reset()  
  28.         dict.rehashindx = -1 # 复位  

另外,redis在服务器执行例行任务时(serverCron),也会定期去做一部分rehash操作,伪代码: 
Python代码   收藏代码
  1. def serverCron():  
  2.     # 循环redis所有数据库  
  3.     for num in redisServer.dbnums:  
  4.         if redisServer.db[num].dict.rehashidx != -1 :  
  5.             # 第二个参数用来限制rehash执行多久,单位毫秒  
  6.             dictRehashMilliseconds(redisServer.db[num].dict, 1)  
  7.     # 其他例行任务...  
  8.   
  9. def dictRehashMilliseconds(dict, timeout_ms):  
  10.     start_time =  now_time()  
  11.     while now_time() - start_time < timeout_ms:  
  12.         _dictRehashStep(dict)  

至此,我们已经分析完:为什么要在dict中维护两个哈希表(ht[0],ht[1]); 
关于redis字典的更多细节,请参看:dict.h和dict.c代码以及redis.c/serverCron函数 

总结:  
1.了解redis中字典是如何设计的以及这样设计的原因; 
2.了解哈希表的rehash过程以及rehash时机;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值