Redis之字典遍历

遍历和修改

Redis本身就是一个大字典,当我们使用keys命令查找这个大字典时,它会遍历整个redis,如果找到了符合匹配条件的key,还需要判断key指向的对象是否已经过期,如果过期了就需要进行删除操作。

void keysCommand(client *c) {
    dictIterator *di; // 迭代器
    dictEntry *de; // 迭代器当前的entry
    sds pattern = c->argv[1]->ptr; // keys的匹配模式参数
    int plen = sdslen(pattern), allkeys; // allkeys 的意思是是否需要获取所有的key,例如keys * 这种指令
    unsigned long numkeys = 0;
    void *replylen = addDeferredMultiBulkLength(c);

    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);
}

复制代码

重复遍历的问题

字典拓容时需要进行渐进式hash,此时存在新旧两个hashtable,需要先遍历old hashtable,然后遍历new hashtable,

如果遍历过程中进行了rehash,旧的hashtable迁移到新的hashtable中,遍历可能会产生重复,某些场景下需要避免遍历重复,redis使用安全迭代器解决了这个问题。

迭代器结构

redis提供两种迭代器,安全与非安全迭代器,安全指的是遍历过程中可以对字典进行查找和修改,这个过程中还会触发过期删除,同时,安全迭代器可以禁止发生rehash。

不安全迭代器则是,遍历过程中,字典是只读的,不可修改,只能调用 dictNext 对字典持续遍历,不得调用过期删除的函数,好处是不影响rehash,坏处则是可能会出现遍历重复。

安全迭代器在遍历开始时,会给字典加上标记,该标记意味着rehash不会发生。


typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx;
    unsigned long iterators; // 这个就是标记,表示在当前字典上安全迭代器的数量
} dict;

static void _dictRehashStep(dict *d) {
    if (d->interators == 0) dictRehash(d, 1)
}

复制代码

迭代过程

安全迭代器在遍历过程中允许删除元素,这意味着一维数组下的链表中的元素可能会被摘走,元素的next指针就会发生变动。

dictEntry *dictNext(dictIterator *iter)
{
    while (1) {
        if (iter->entry == NULL) {
            // 遍历一个新槽位下面的链表
            dictht *ht = &iter->d->ht[iter->table];
            if (iter->index == -1 && iter->table == 0) {
                // 如果是第一次遍历这个字典,就给字典打上安全标记,禁止字典进行rehash
                if (iter->safe)
                    iter->d->iterators++;
                else
                    // 记录迭代器指纹,类似md5值,如果遍历过程中字典发生变动,指纹将会改变
                    iter->fingerprint = dictFingerprint(iter->d);
            }
            iter->index++; // index由-1变成0,进入第一个槽位
            if (iter->index >= (long) ht->size) {
                // 最后一个槽位遍历完毕, 如果处于rehash中,继续遍历第二个hashtable
                if (dictIsRehashing(iter->d) && iter->table == 0) {
                    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) {
            /* We need to save the 'next' here, the iterator user
             * may delete the entry we are returning. */
            // 此处用于防止安全迭代将当前过期元素删除找不到下一个需要遍历的元素
            // 如果发生rehash,当前遍历的链表被打散,链表中的元素被挂到新数组下的两个槽位下,就会导致重复遍历
            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--;
        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;

    /* We hash N integers by summing every successive integer with the integer
     * hashing of the previous sum. Basically:
     *
     * Result = hash(hash(hash(int1)+int2)+int3) ...
     *
     * This way the same set of integers in a different order will (likely) hash
     * to a different number. */
    for (j = 0; j < 6; j++) {
        hash += integers[j];
        /* For the hashing step we use Tomas Wang's 64 bit integer hash. */
        hash = (~hash) + (hash << 21); // hash = (hash << 21) - hash - 1;
        hash = hash ^ (hash >> 24);
        hash = (hash + (hash << 3)) + (hash << 8); // hash * 265
        hash = hash ^ (hash >> 14);
        hash = (hash + (hash << 2)) + (hash << 4); // hash * 21
        hash = hash ^ (hash >> 28);
        hash = hash + (hash << 31);
    }
    return hash;
}

复制代码

字典拓容时的rehash,将链表迁移到新数组中的时候,某个具体槽位的链表只可能迁移到新数组的两个槽位中。

hash mod 2^n = k 
hash mod 2^(n+1) = k or k+2^n
复制代码

迭代器的选择

如果遍历过程中不允许发生重复,需要处理元素过期,也需要对字典进行修改,就选择安全迭代器,否则就选择非安全迭代器。

选择安全迭代器的情况:

  1. keys指令,需要遍历所有对象,不允许重复

  2. bgaofrewrite,需要遍历所有对象持久化,不允许重复

  3. bgsave也需要遍历所有对象,不允许重复

你可能感兴趣的:(redis,缓存,数据库)