Redis 字典的遍历过程逻辑比较复杂,互联网上对这一块的分析讲解很是少。我也花了很多时间对源码的细节进行了整理,将我我的对字典遍历逻辑的理解呈现给各位读者。也许读者们对字典的遍历过程有比我更好的理解,还请不吝指教。数组
咱们知道 Redis 对象树的主干是一个字典,若是对象不少,这个主干字典也会很大。当咱们使用 keys 命令搜寻指定模式的 key 时,它会遍历整个主干字典。值得注意的是,在遍历的过程当中,若是知足模式匹配条件的 key 被找到了,还须要判断 key 指向的对象是否已通过期。若是过时了就须要从主干字典中将该 key 删除。安全
void keysCommand(client *c) {
dictIterator *di; // 迭代器
dictEntry *de; // 迭代器当前的entry
sds pattern = c->argv[1]->ptr; // keys的匹配模式参数
int plen = sdslen(pattern);
int allkeys; // 是否要获取全部key,用于keys *这样的指令
unsigned long numkeys = 0;
void *replylen = addDeferredMultiBulkLength(c);
// why safe?
di = dictGetSafeIterator(c->db->dict);
allkeys = (pattern[0] == '*' && pattern[1] == '\0');
while((de = dictNext(di)) != NULL) {
sds key = dictGetKey(de);
robj *keyobj;
if (allkeys || stringmatchlen(pattern,plen,key,sdslen(key),0)) {
keyobj = createStringObject(key,sdslen(key));
// 判断是否过时,过时了要删除元素
if (expireIfNeeded(c->db,keyobj) == 0) {
addReplyBulk(c,keyobj);
numkeys++;
}
decrRefCount(keyobj);
}
}
dictReleaseIterator(di);
setDeferredMultiBulkLength(c,replylen,numkeys);
}
复制代码
那么,你是否想到了其中的困难之处,在遍历字典的时候还须要修改字典,会不会出现指针安全问题?bash
字典在扩容的时候要进行渐进式迁移,会存在新旧两个 hashtable。遍历须要对这两个 hashtable 依次进行,先遍历完旧的 hashtable,再继续遍历新的 hashtable。若是在遍历的过程当中进行了 rehashStep,将已经遍历过的旧的 hashtable 的元素迁移到了新的 hashtable中,那么遍历会不会出现元素的重复?这也是遍历须要考虑的疑难之处,下面咱们来看看 Redis 是如何解决这个问题的。服务器
Redis 为字典的遍历提供了 2 种迭代器,一种是安全迭代器,另外一种是不安全迭代器。微信
typedef struct dictIterator {
dict *d; // 目标字典对象
long index; // 当前遍历的槽位置,初始化为-1
int table; // ht[0] or ht[1]
int safe; // 这个属性很是关键,它表示迭代器是否安全
dictEntry *entry; // 迭代器当前指向的对象
dictEntry *nextEntry; // 迭代器下一个指向的对象
long long fingerprint; // 迭代器指纹,放置迭代过程当中字典被修改
} dictIterator;
// 获取非安全迭代器,只读迭代器,容许rehashStep
dictIterator *dictGetIterator(dict *d) {
dictIterator *iter = zmalloc(sizeof(*iter));
iter->d = d;
iter->table = 0;
iter->index = -1;
iter->safe = 0;
iter->entry = NULL;
iter->nextEntry = NULL;
return iter;
}
// 获取安全迭代器,容许触发过时处理,禁止rehashStep
dictIterator *dictGetSafeIterator(dict *d) {
dictIterator *i = dictGetIterator(d);
i->safe = 1;
return i;
}
复制代码
迭代器的「安全」指的是在遍历过程当中能够对字典进行查找和修改,不用感到担忧,由于查找和修改会触发过时判断,会删除内部元素。「安全」的另外一层意思是迭代过程当中不会出现元素重复,为了保证不重复,就会禁止 rehashStep。函数
而「不安全」的迭代器是指遍历过程当中字典是只读的,你不能够修改,你只能调用 dictNext 对字典进行持续遍历,不得调用任何可能触发过时判断的函数。不过好处是不影响 rehash,代价就是遍历的元素可能会出现重复。ui
安全迭代器在刚开始遍历时,会给字典打上一个标记,有了这个标记,rehashStep 就不会执行,遍历时元素就不会出现重复。spa
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx;
// 这个就是标记,它表示当前加在字典上的安全迭代器的数量
unsigned long iterators;
} dict;
// 若是存在安全的迭代器,就禁止rehash
static void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d,1);
}
复制代码
安全的迭代器在遍历过程当中容许删除元素,意味着字典第一维数组下面挂接的链表中的元素可能会被摘走,元素的 next 指针就会发生变更,这是否会影响迭代过程呢?下面咱们仔细研究一下迭代函数的代码逻辑。指针
dictEntry *dictNext(dictIterator *iter)
{
while (1) {
if (iter->entry == NULL) {
// 遍历一个新槽位下面的链表,数组的index往前移动了
dictht *ht = &iter->d->ht[iter->table];
if (iter->index == -1 && iter->table == 0) {
// 第一次遍历,刚刚进入遍历过程
// 也就是ht[0]数组的第一个元素下面的链表
if (iter->safe) {
// 给字典打安全标记,禁止字典进行rehash
iter->d->iterators++;
} else {
// 记录迭代器指纹,就比如字典的md5值
// 若是遍历过程当中字典有任何变更,指纹就会改变
iter->fingerprint = dictFingerprint(iter->d);
}
}
iter->index++; // index=0,正式进入第一个槽位
if (iter->index >= (long) ht->size) {
// 最后一个槽位都遍历完了
if (dictIsRehashing(iter->d) && iter->table == 0) {
// 若是处于rehash中,那就继续遍历第二个 hashtable
iter->table++;
iter->index = 0;
ht = &iter->d->ht[1];
} else {
// 结束遍历
break;
}
}
// 将当前遍历的元素记录到迭代器中
iter->entry = ht->table[iter->index];
} else {
// 直接将下一个元素记录为本次迭代的元素
iter->entry = iter->nextEntry;
}
if (iter->entry) {
// 将下一个元素也记录到迭代器中,这点很是关键
// 防止安全迭代过程当中当前元素被过时删除后,找不到下一个须要遍历的元素
// 试想若是后面发生了rehash,当前遍历的链表被打散了,会发生什么
// 这里要使劲发挥本身的想象力来理解
// 旧的链表将一分为二,打散后从新挂接到新数组的两个槽位下
// 结果就是会致使当前链表上的元素会重复遍历
// 若是rehash的链表是index前面的链表,那么这部分链表也会被重复遍历
iter->nextEntry = iter->entry->next;
return iter->entry;
}
}
return NULL;
}
// 遍历完成后要释放迭代器,安全迭代器须要去掉字典的禁止rehash的标记
// 非安全迭代器还须要检查指纹,若是有变更,服务器就会奔溃(failfast)
void dictReleaseIterator(dictIterator *iter)
{
if (!(iter->index == -1 && iter->table == 0)) {
if (iter->safe)
iter->d->iterators--; // 去掉禁止rehash的标记
else
assert(iter->fingerprint == dictFingerprint(iter->d));
}
zfree(iter);
}
// 计算字典的指纹,就是将字典的关键字段进行按位糅合到一块儿
// 这样只要有任意的结构变更,指纹都会发生变化
// 若是只是某个元素的value被修改了,指纹不会发生变更
long long dictFingerprint(dict *d) {
long long integers[6], hash = 0;
int j;
integers[0] = (long) d->ht[0].table;
integers[1] = d->ht[0].size;
integers[2] = d->ht[0].used;
integers[3] = (long) d->ht[1].table;
integers[4] = d->ht[1].size;
integers[5] = d->ht[1].used;
for (j = 0; j < 6; j++) {
hash += integers[j];
hash = (~hash) + (hash << 21);
hash = hash ^ (hash >> 24);
hash = (hash + (hash << 3)) + (hash << 8);
hash = hash ^ (hash >> 14);
hash = (hash + (hash << 2)) + (hash << 4);
hash = hash ^ (hash >> 28);
hash = hash + (hash << 31);
}
return hash;
}
复制代码
值得注意的是在字典扩容时进行rehash,将旧数组中的链表迁移到新的数组中。某个具体槽位下的链表只可能会迁移到新数组的两个槽位中。code
hash mod 2^n = k
hash mod 2^(n+1) = k or k+2^n
复制代码
除了keys指令使用了安全迭代器,由于结果不容许重复。那还有其它的地方使用了安全迭代器么,什么状况下遍历适合使用非安全迭代器呢?
简单一点说,那就是若是遍历过程当中不容许出现重复,那就使用SafeIterator,好比下面的两种状况
若是遍历过程当中须要处理元素过时,须要对字典进行修改,那也必须使用SafeIterator,由于非安全的迭代器是只读的。
其它状况下,也就是容许遍历过程当中出现个别元素重复,不须要对字典进行结构性修改的状况下一概使用非安全迭代器。
请继续思考rehash对非安全遍历过程的影响,会重复哪些元素,重复的元素会很是多么仍是只是少许重复?
微信扫一扫关注公众号「码洞」,一步两步带你「码出个将来」。