Redis的字典(dict)rehash过程源码解析

原文 http://blog.csdn.net/yuanrxdu/article/details/24779693

分类: redis 分布式系统   439人阅读  评论(0)  收藏  举报
redis 代码分析 c nosql

Redis的内存存储结构是个大的字典存储,也就是我们通常说的哈希表。Redis小到可以存储几万记录的CACHE,大到可以存储几千万甚至上亿的记录(看内存而定),这充分说明Redis作为缓冲的强大。Redis的核心数据结构就是字典(dict),dict在数据量不断增大的过程中,会遇到HASH(key)碰撞的问题,如果DICT不够大,碰撞的概率增大,这样单个hash 桶存储的元素会越来愈多,查询效率就会变慢。如果数据量从几千万变成几万,不断减小的过程,DICT内存却会造成不必要的浪费。Redis的dict在设计的过程中充分考虑了dict自动扩大和收缩,实现了一个称之为rehash的过程。使dict出发rehash的条件有两个:

1)总的元素个数 除 DICT桶的个数得到每个桶平均存储的元素个数(pre_num),如果 pre_num > dict_force_resize_ratio,就会触发dict 扩大操作。dict_force_resize_ratio = 5。

2)在总元素 * 10 < 桶的个数,也就是,填充率必须<10%,DICT便会进行收缩,让total / bk_num 接近 1:1。


dict rehash扩大流程:

Redis的字典(dict)rehash过程源码解析_第1张图片

源代码函数调用和解析:

dictAddRaw->_dictKeyIndex->_dictExpandIfNeeded->dictExpand,这个函数调用关系是需要扩大dict的调用关系,
_dictKeyIndex函数代码:

[cpp]  view plain copy
  1. static int _dictKeyIndex(dict *d, const void *key)  
  2. {  
  3.     unsigned int h, idx, table;  
  4.     dictEntry *he;  
  5.   
  6.     // 如果有需要,对字典进行扩展  
  7.     if (_dictExpandIfNeeded(d) == DICT_ERR)  
  8.         return -1;  
  9.   
  10.     // 计算 key 的哈希值  
  11.     h = dictHashKey(d, key);  
  12.   
  13.     // 在两个哈希表中进行查找给定 key  
  14.     for (table = 0; table <= 1; table++) {  
  15.   
  16.         // 根据哈希值和哈希表的 sizemask   
  17.         // 计算出 key 可能出现在 table 数组中的哪个索引  
  18.         idx = h & d->ht[table].sizemask;  
  19.   
  20.         // 在节点链表里查找给定 key  
  21.         // 因为链表的元素数量通常为 1 或者是一个很小的比率  
  22.         // 所以可以将这个操作看作 O(1) 来处理  
  23.         he = d->ht[table].table[idx];  
  24.         while(he) {  
  25.             // key 已经存在  
  26.             if (dictCompareKeys(d, key, he->key))  
  27.                 return -1;  
  28.   
  29.             he = he->next;  
  30.         }  
  31.   
  32.         // 第一次进行运行到这里时,说明已经查找完 d->ht[0] 了  
  33.         // 这时如果哈希表不在 rehash 当中,就没有必要查找 d->ht[1]  
  34.         if (!dictIsRehashing(d)) break;  
  35.     }  
  36.   
  37.     return idx;  
  38. }  
_dictExpandIfNeeded函数代码解析:

[cpp]  view plain copy
  1. static int _dictExpandIfNeeded(dict *d)  
  2. {  
  3.     // 已经在渐进式 rehash 当中,直接返回  
  4.     if (dictIsRehashing(d)) return DICT_OK;  
  5.   
  6.     // 如果哈希表为空,那么将它扩展为初始大小  
  7.     // O(N)  
  8.     if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);  
  9.   
  10.     // 如果哈希表的已用节点数 >= 哈希表的大小,  
  11.     // 并且以下条件任一个为真:  
  12.     //   1) dict_can_resize 为真  
  13.     //   2) 已用节点数除以哈希表大小之比大于   
  14.     //      dict_force_resize_ratio  
  15.     // 那么调用 dictExpand 对哈希表进行扩展  
  16.     // 扩展的体积至少为已使用节点数的两倍  
  17.     // O(N)  
  18.     if (d->ht[0].used >= d->ht[0].size &&  
  19.         (dict_can_resize ||  
  20.          d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))  
  21.     {  
  22.         return dictExpand(d, d->ht[0].used*2);  
  23.     }  
  24.   
  25.     return DICT_OK;  
  26. }  

dict rehash缩小流程:

Redis的字典(dict)rehash过程源码解析_第2张图片

源代码函数调用和解析:

serverCron->tryResizeHashTables->dictResize->dictExpand

serverCron函数是个心跳函数,调用tryResizeHashTables段为:

[cpp]  view plain copy
  1. int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {  
  2.     ....  
  3.     if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) {  
  4.         // 将哈希表的比率维持在 1:1 附近  
  5.         tryResizeHashTables();  
  6.         if (server.activerehashing) incrementallyRehash(); //进行rehash动作  
  7.     }  
  8.     ....  
  9. }  
tryResizeHashTables函数代码分析:

[cpp]  view plain copy
  1. void tryResizeHashTables(void) {  
  2.     int j;  
  3.   
  4.     for (j = 0; j < server.dbnum; j++) {  
  5.   
  6.         // 缩小键空间字典  
  7.         if (htNeedsResize(server.db[j].dict))  
  8.             dictResize(server.db[j].dict);  
  9.   
  10.         // 缩小过期时间字典  
  11.         if (htNeedsResize(server.db[j].expires))  
  12.             dictResize(server.db[j].expires);  
  13.     }  
  14. }  


htNeedsResize函数是判断是否可以需要进行dict缩小的条件判断,填充率必须>10%,否则会进行缩小,具体代码如下:

[cpp]  view plain copy
  1. int htNeedsResize(dict *dict) {  
  2.     long long size, used;  
  3.   
  4.     // 哈希表大小  
  5.     size = dictSlots(dict);  
  6.   
  7.     // 哈希表已用节点数量  
  8.     used = dictSize(dict);  
  9.   
  10.     // 当哈希表的大小大于 DICT_HT_INITIAL_SIZE   
  11.     // 并且字典的填充率低于 REDIS_HT_MINFILL 时  
  12.     // 返回 1  
  13.     return (size && used && size > DICT_HT_INITIAL_SIZE &&  
  14.             (used*100/size < REDIS_HT_MINFILL));  
  15. }  

dictResize函数代码:

[cpp]  view plain copy
  1. int dictResize(dict *d)  
  2. {  
  3.     int minimal;  
  4.   
  5.     // 不能在 dict_can_resize 为假  
  6.     // 或者字典正在 rehash 时调用  
  7.     if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;  
  8.   
  9.     minimal = d->ht[0].used;  
  10.   
  11.     if (minimal < DICT_HT_INITIAL_SIZE)  
  12.         minimal = DICT_HT_INITIAL_SIZE;  
  13.   
  14.     return dictExpand(d, minimal);  
  15. }  


以上两个过程最终调用了dictExpand函数,这个函数主要是产生一个新的HASH表(dictht),并让将dict.rehashidx= 0。表示开始进行rehash动作。具体的rehash动作是将ht[0]的数据按照hash隐射的规则重新隐射到 ht[1]上.具体代码如下:

[cpp]  view plain copy
  1. int dictExpand(dict *d, unsigned long size)  
  2. {  
  3.     dictht n; /* 被转移数据的新hash table */  
  4.       
  5.     // 计算哈希表的真实大小  
  6.     unsigned long realsize = _dictNextPower(size);  
  7.     if (dictIsRehashing(d) || d->ht[0].used > size || d->ht[0].size == realsize)  
  8.         return DICT_ERR;  
  9.   
  10.     // 创建并初始化新哈希表  
  11.     n.size = realsize;  
  12.     n.sizemask = realsize-1;  
  13.     n.table = zcalloc(realsize*sizeof(dictEntry*));  
  14.     n.used = 0;  
  15.   
  16.     // 如果 ht[0] 为空,那么这就是一次创建新哈希表行为  
  17.     // 将新哈希表设置为 ht[0] ,然后返回  
  18.     if (d->ht[0].table == NULL) {  
  19.         d->ht[0] = n;  
  20.         return DICT_OK;  
  21.     }  
  22.   
  23.     /* Prepare a second hash table for incremental rehashing */  
  24.     // 如果 ht[0] 不为空,那么这就是一次扩展字典的行为  
  25.     // 将新哈希表设置为 ht[1] ,并打开 rehash 标识  
  26.     d->ht[1] = n;  
  27.     d->rehashidx = 0;  
  28.   
  29.     return DICT_OK;  
  30. }  

字典dict的rehashidx被设置成0后,就表示开始rehash动作,在心跳函数执行的过程,会检查到这个标志,如果需要rehash,就行进行渐进式rehash动作。函数调用的过程为:

serverCron->incrementallyRehash->dictRehashMilliseconds->dictRehash

incrementallyRehash函数代码:

[cpp]  view plain copy
  1. /* 
  2.  * 在 Redis Cron 中调用,对数据库中第一个遇到的、可以进行 rehash 的哈希表 
  3.  * 进行 1 毫秒的渐进式 rehash 
  4.  */  
  5. void incrementallyRehash(void) {  
  6.     int j;  
  7.   
  8.     for (j = 0; j < server.dbnum; j++) {  
  9.         /* Keys dictionary */  
  10.         if (dictIsRehashing(server.db[j].dict)) {  
  11.             dictRehashMilliseconds(server.db[j].dict,1);  
  12.             break/* 已经耗尽了指定的CPU毫秒数 */  
  13.         }  
  14.     ...  
  15. }  


dictRehashMilliseconds函数是按照指定的CPU运算的毫秒数,执行rehash动作,每次一个100个为单位执行。代码如下:

[cpp]  view plain copy
  1. /* 
  2.  * 在给定毫秒数内,以 100 步为单位,对字典进行 rehash 。 
  3.  */  
  4. int dictRehashMilliseconds(dict *d, int ms) {  
  5.     long long start = timeInMilliseconds();  
  6.     int rehashes = 0;  
  7.   
  8.     while(dictRehash(d,100)) {/*每次100步数据*/  
  9.         rehashes += 100;  
  10.         if (timeInMilliseconds()-start > ms) break/*耗时完毕,暂停rehash*/  
  11.     }  
  12.     return rehashes;  
  13. }  
[cpp]  view plain copy
  1. /* 
  2.  * 执行 N 步渐进式 rehash 。 
  3.  * 
  4.  * 如果执行之后哈希表还有元素需要 rehash ,那么返回 1 。 
  5.  * 如果哈希表里面所有元素已经迁移完毕,那么返回 0 。 
  6.  * 
  7.  * 每步 rehash 都会移动哈希表数组内某个索引上的整个链表节点, 
  8.  * 所以从 ht[0] 迁移到 ht[1] 的 key 可能不止一个。 
  9.  */  
  10. int dictRehash(dict *d, int n) {  
  11.     if (!dictIsRehashing(d)) return 0;  
  12.   
  13.     while(n--) {  
  14.         dictEntry *de, *nextde;  
  15.         // 如果 ht[0] 已经为空,那么迁移完毕  
  16.         // 用 ht[1] 代替原来的 ht[0]  
  17.         if (d->ht[0].used == 0) {  
  18.   
  19.             // 释放 ht[0] 的哈希表数组  
  20.             zfree(d->ht[0].table);  
  21.   
  22.             // 将 ht[0] 指向 ht[1]  
  23.             d->ht[0] = d->ht[1];  
  24.   
  25.             // 清空 ht[1] 的指针  
  26.             _dictReset(&d->ht[1]);  
  27.             // 关闭 rehash 标识  
  28.             d->rehashidx = -1;  
  29.             // 通知调用者, rehash 完毕  
  30.             return 0;  
  31.         }  
  32.         assert(d->ht[0].size > (unsigned)d->rehashidx);  
  33.         // 移动到数组中首个不为 NULL 链表的索引上  
  34.         while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;  
  35.         // 指向链表头  
  36.         de = d->ht[0].table[d->rehashidx];  
  37.         // 将链表内的所有元素从 ht[0] 迁移到 ht[1]  
  38.         // 因为桶内的元素通常只有一个,或者不多于某个特定比率  
  39.         // 所以可以将这个操作看作 O(1)  
  40.         while(de) {  
  41.             unsigned int h;  
  42.   
  43.             nextde = de->next;  
  44.   
  45.             /* Get the index in the new hash table */  
  46.             // 计算元素在 ht[1] 的哈希值  
  47.             h = dictHashKey(d, de->key) & d->ht[1].sizemask;  
  48.   
  49.             // 添加节点到 ht[1] ,调整指针  
  50.             de->next = d->ht[1].table[h];  
  51.             d->ht[1].table[h] = de;  
  52.   
  53.             // 更新计数器  
  54.             d->ht[0].used--;  
  55.             d->ht[1].used++;  
  56.   
  57.             de = nextde;  
  58.         }  
  59.   
  60.         // 设置指针为 NULL ,方便下次 rehash 时跳过  
  61.         d->ht[0].table[d->rehashidx] = NULL;  
  62.   
  63.         // 前进至下一索引  
  64.         d->rehashidx++;  
  65.     }  
  66.   
  67.     // 通知调用者,还有元素等待 rehash  
  68.     return 1;  
  69. }  

总结,Redis的rehash动作是一个内存管理和数据管理的一个核心操作,由于Redis主要使用单线程做数据管理和消息效应,它的rehash数据迁移过程采用的是渐进式的数据迁移模式,这样做是为了防止rehash过程太长堵塞数据处理线程。并没有采用memcached的多线程迁移模式。关于memcached的rehash过程,以后再做介绍。从redis的rehash过程设计的很巧,也很优雅。在这里值得注意的是,redis在find数据的时候,是同时查找正在迁移的ht[0]和被迁移的ht[1]。防止迁移过程数据命不中的问题。

你可能感兴趣的:(redis,分布式系统)