一对一聊天源码,你是否了解ERedis的扩容机制?
Redis的扩容时机
Redis会在如下两种情况触发扩容。
1、如果没有fork子进程在执行RDB或者AOF的持久化,一旦满足ht[0].used >= ht[0].size,此时触发扩容;
2、如果有fork子进程在执行RDB或者AOF的持久化时,则需要满足ht[0].used > 5 * ht[0].size,此时触发扩容。
下面将结合源码对Redis的扩容时机进行学习。当向dict添加或者更新数据时,对应的方法是位于dict.c文件中的dictReplace()方法,如下所示。
int dictReplace(dict *d, void *key, void *val) { dictEntry *entry, *existing, auxentry; // 如果添加成功,dictAddRaw()方法会把成功添加的dictEntry返回 // 返回的dictEntry只设置了键,值需要在这里调用dictSetVal()方法进行设置 entry = dictAddRaw(d,key,&existing); if (entry) { dictSetVal(d, entry, val); return 1; } // 执行到这里,表明在哈希表中已经存在一个dictEntry的键与当前待添加的键值对的键相等 // 此时应该做更新值的操作,且existing此时是指向这个已经存在的dictEntry auxentry = *existing; // 更新值,即为existing指向的dictEntry设置新值 dictSetVal(d, existing, val); // 释放旧值 dictFreeVal(d, &auxentry); return 0; }
dictReplace()方法会执行键值对的添加或更新,如果哈希表中不存在dictEntry的键与待添加键值对的键相等,此时会基于待添加键值对新创建一个dictEntry并以头插法插入哈希表中,此时返回1;如果哈希表中存在dictEntry的键与待添加键值对的键相等,此时就为已经存在的dictEntry设置新值并释放旧值,然后返回0。通常要触发扩容,触发时机一般在添加键值对的时候,所以继续分析dictAddRaw()方法,其源码实现如下所示。
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) { long index; dictEntry *entry; dictht *ht; // 判断是否在进行rehash,如果正在进行rehash,则触发渐进式rehash // dictIsRehashing()方法在dict.h文件中,如果dict的rehashidx不等于-1,则表明此时在进行rehash if (dictIsRehashing(d)) _dictRehashStep(d); // 获取待添加键值对在哈希表中的索引index // 如果哈希表已经存在dictEntry的键与待添加键值对的键相等,此时_dictKeyIndex()方法返回-1 if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1) return NULL; // 如果在进行rehash,待添加的键值对存放到ht[1],否则存放到ht[0] ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; // 为新dictEntry开辟内存,此时dictEntry的键和值尚未设置 entry = zmalloc(sizeof(*entry)); // 头插的方式插入哈希表index位置的哈希桶中 entry->next = ht->table[index]; ht->table[index] = entry; // 哈希表的当前大小加1 ht->used++; // 为新dictEntry设置键 dictSetKey(d, entry, key); return entry; }
dictAddRaw()方法会首先判断当前是否处于rehash阶段(判断当前是否正在扩容),如果正在rehash,则触发一次哈希桶的迁移操作(这一点后面再详细分析),然后通过_dictKeyIndex()方法获取待添加键值对在哈希表中的索引index,如果获取到的index为-1,表明存在dictEntry的键与待添加键值对的键相等,此时dictAddRaw()方法返回NULL以告诉方法调用方需要执行更新操作,如果index不为-1,则基于待添加键值对创建新的dictEntry并以头插的方式插入哈希表index位置的哈希桶中,然后更新哈希表的当前大小以及为新dictEntry设置键。扩容的触发在_dictKeyIndex()方法中,其源码实现如下所示。
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing) { unsigned long idx, table; dictEntry *he; if (existing) *existing = NULL; // 在_dictExpandIfNeeded()方法中判断是否需要扩容,如果需要,则执行扩容 if (_dictExpandIfNeeded(d) == DICT_ERR) return -1; for (table = 0; table <= 1; table++) { // 将待添加键值对的键的hash值与哈希表掩码相与以得到待添加键值对在哈希表中的索引 idx = hash & d->ht[table].sizemask; // 遍历哈希表中idx索引位置的哈希桶的链表 he = d->ht[table].table[idx]; while(he) { if (key==he->key || dictCompareKeys(d, key, he->key)) { // 链表中存在dictEntry的key与待添加键值对的key相等 // 此时让existing指向这个已经存在的dictEntry,并返回-1 // 表明存在dictEntry的key与待添加键值对的key相等且existing已经指向了这个存在的dictEntry if (existing) *existing = he; return -1; } // 继续遍历链表中的下一个节点 he = he->next; } if (!dictIsRehashing(d)) break; } // 执行到这里,表明哈希表中不存在dictEntry的key与待添加键值对的key相等,返回索引idx return idx; }
在_dictKeyIndex()方法的一开始,就会调用_dictExpandIfNeeded()方法来判断是否需要扩容,如果需要,则会执行扩容逻辑,假如_dictExpandIfNeeded()方法在扩容过程中出现错误,会返回状态码1,也就是DICT_ERR字段表示的值,此时_dictKeyIndex()方法直接返回-1,如果不需要扩容或者扩容成功,则将待添加键值对的键的hash值与哈希表掩码相与得到待添加键值对在哈希表中的索引,然后遍历索引位置的哈希桶的链表,看是否能够找到一个dictEntry的键与待添加键值对的键相等,如果能够找到一个这样的dictEntry,则返回-1并让existing指向这个dictEntry,否则返回之前计算得到的索引。可知判断扩容以及执行扩容的逻辑都在_dictExpandIfNeeded()方法中,其源码实现如下所示。
static int _dictExpandIfNeeded(dict *d) { // 如果已经在扩容,则返回状态码0 if (dictIsRehashing(d)) return DICT_OK; // 如果哈希表的容量为0,则初始化哈希表的容量为4 if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); // 如果哈希表的当前大小大于等于容量,并且dict_can_resize为1或者当前大小大于容量的五倍 // 此时判断需要扩容,调用dictExpand()方法执行扩容逻辑,且指定扩容后的容量至少为当前大小的2倍 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; }
通过_dictExpandIfNeeded()方法的源码可知,要触发扩容,首先需要满足的条件就是哈希表当前大小大于等于了哈希表的容量,然后再判断Redis当前是否允许扩容,如果允许扩容,则执行扩容逻辑,如果不允许扩容,那么再判断哈希表当前大小是否已经大于了哈希表容量的5倍,如果已经大于,则强制执行扩容逻辑。在_dictExpandIfNeeded()方法有两个重要的参数,分别是dict_can_resize和dict_force_resize_ratio,这两个参数定义在dict.c文件中,初始值如下所示。
static int dict_can_resize = 1; static unsigned int dict_force_resize_ratio = 5;
那么现在最后还需要结合源码分析一下,什么时候会更改dict_can_resize的值,在dict.c文件中有如下两个方法,会将dict_can_resize的值设置为1或者0,如下所示。
void dictEnableResize(void) { dict_can_resize = 1; } void dictDisableResize(void) { dict_can_resize = 0; }
这两个方法会被server.c文件中的updateDictResizePolicy()方法调用,如下所示。
void updateDictResizePolicy(void) { // 如果有正在执行RDB或AOF持久化的子进程,hasActiveChildProcess()方法返回true if (!hasActiveChildProcess()) // 没有正在执行RDB或AOF持久化的子进程时将dict_can_resize设置为1,表示允许扩容 dictEnableResize(); else // 有正在执行RDB或AOF持久化的子进程时将dict_can_resize设置为0,表示不允许扩容 dictDisableResize(); }
至此Redis的扩容时机的源码分析就到此为止,现在进行小节:当向Redis添加或者更新数据时,会判断一下存储数据的哈希表的当前大小是否大于等于哈希表容量,如果大于,再判断Redis是否允许扩容,而决定Redis是否允许扩容的依据就是当前是否存在子线程在执行RDB或者AOF的持久化,如果存在就不允许扩容,反之则允许扩容,假如Redis不允许扩容,那么还需要判断一下是否要强制扩容,判断依据就是存储数据的哈希表的当前大小是否已经大于哈希表容量的5倍,如果大于,则强制扩容。
下面给出流程图,对上述整个触发扩容的源码流程进行示意。
以上就是一对一聊天源码,你是否了解Redis的扩容机制?, 更多内容欢迎关注之后的文章