1、遍历Redis
1)Redis对象树的主干是一个字典,如果对象很多,那么这个树会很大,当我们执行keys命令时,首先它会遍历整棵树,查找出满足条件的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); } |
2、迭代器结构
1)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; } |
2)迭代器安全是指在遍历过程中,能够对迭代器进行读和修改遍历过程中不会出现重复元素,为了保证不重复,就要禁止rehashStep操作,安全迭代器在开始遍历时,会给字典打上一个标记,有了这个标记,rehashStep就不会进行了。
3)不安全迭代器是指在遍历过程中,只能对迭代器进行读操作,只能调用dictNext对字典进行持续遍历,不能调用任何触发过期函数的操作,但是不安全迭代器会出现重复元素
4)禁止rehash方法
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); } |
3、迭代过程
1)遍历链表方法
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; } |
2)字典扩容时,进行rehash,将旧的数组链表迁移到新的数组下面,某个槽位下的链表只会迁移到新数组的俩个槽位中
hash mod 2^n = k hash mod 2^(n+1) = k or k+2^n |
4、迭代器的选择
1)遍历过程中不允许出现重复对象,就选择安全迭代器,否则选择不安全迭代器
2)bgaofrewrite指令需要遍历所有的对象转换哼操作指令进行持久化,那么绝对不允许出现重复
3)bgsave需要遍历所有对象进行持久化,同样不允许出现重复
4)遍历过程中如果需要处理过期元素,对字典进行修改,那就必须使用安全遍历器
5)其他情况下,可以选择不安全遍历器