【Redis5源码学习】浅析redis命令之keys篇

baiyanredis

命令语法

命令含义:查找并返回全部符合给定模式 pattern 的 key
命令格式:算法

KEYS pattern

命令实战:数据库

127.0.0.1:6379> keys *
1) "kkk"
2) "key1"

返回值: 根据pattern匹配后的全部键的集合安全

源码分析

keys命令对应的处理函数是keysCommand():并发

void keysCommand(client *c) {
    dictIterator *di; 
    dictEntry *de;
    sds pattern = c->argv[1]->ptr; // 获取咱们输入的pattern
    int plen = sdslen(pattern), allkeys;
    unsigned long numkeys = 0;
    void *replylen = addDeferredMultiBulkLength(c); 

    di = dictGetSafeIterator(c->db->dict); // 初始化一个安全迭代器
    allkeys = (pattern[0] == '*' && pattern[1] == '\0'); // 判断是否返回所有keys的集合
    while((de = dictNext(di)) != NULL) { // 遍历整个键空间字典
        sds key = dictGetKey(de); // 获取key值
        robj *keyobj;

        // 若是是返回全体键的集合,或者当前键与咱们给定的pattern匹配,那么添加到返回列表
        if (allkeys || stringmatchlen(pattern,plen,key,sdslen(key),0)) {
            keyobj = createStringObject(key,sdslen(key));
            if (!keyIsExpired(c->db,keyobj)) { // 筛选出没有过时的键
                addReplyBulk(c,keyobj); // 添加到返回列表
                numkeys++; // 返回键的数量++
            }
            decrRefCount(keyobj); 
        }
    }
    dictReleaseIterator(di); // 释放安全迭代器
    setDeferredMultiBulkLength(c,replylen,numkeys); // 设置返回值的长度
}

因为咱们使用了keys *命令,须要返回全部键的集合。咱们首先观察这段代码,它会使用一个安全迭代器,来遍历整个键空间字典。在遍历的同时,筛选出那些匹配咱们pattern以及非过时的键,而后返回给客户端。因为其遍历的时间复杂度是和字典的大小成正比的,这样就会致使一个问题,当键很是多的时候,这个键空间字典可能会很是大,咱们一口气使用keys把字典从上到下遍历一遍,会消耗很是多的时间。因为redis是单进程的应用,长时间执行keys命令会阻塞redis进程,形成redis服务对外的不可用状态。因此,不少公司都会禁止开发者使用keys命令,这可能致使redis服务长时间不可用。参考案例:Redis的KEYS命令引发RDS数据库雪崩,RDS发生两次宕机,形成几百万的资金损失
那么可能你们会问了,我若是换上其余范围比较小的pattern去替换以前的*,不就能够避免一次性去遍历所有的键空间了吗?可是咱们看上面的源码,因为它是在遍历到每个key的时候,都会去判断当前key是否与传入的pattern所匹配,因此,并非咱们想象中的,只遍历咱们传入的pattern的键空间元素集合,而须要遍历完整的键空间集合,在遍历的同时筛选出符合条件的key值。其实遍历的初始范围并无缩小,其时间复杂度仍然为O(N),N为键空间字典的大小。函数

扩展

安全迭代器与非安全迭代器

在keys命令的遍历过程当中,涉及到了安全迭代器的概念。与之相对的,还有非安全迭代器。那么,迭代器是如何工做的,安全与非安全的区别有是什么呢?咱们首先来看迭代器的存储结构:源码分析

typedef struct dictIterator {
    dict *d; // 指向所要遍历的字典
    long index; // 哈希表中bucket的索引位置
    int table, safe; // table索引(参考dict结构只能为0或1),以及迭代器是否安全的标记
    dictEntry *entry, *nextEntry; // 存储当前entry和下一个entry
    long long fingerprint; // 指纹,只在非安全迭代的状况下作校验
} dictIterator;

为了让你们可以看明白index和table字段的做用,咱们又要贴上dict的结构了:

其中的table字段只能为0或1。0是正常状态下会使用的哈希表,1是rehash过程当中须要用到的过渡哈希表。而index就是每一个哈希表中01234567这个索引了。迭代器中的safe字段就是用来区分迭代器类型是安全仍是非安全的。所谓安全就是指在遍历的过程当中,对字典的操做不会影响遍历的结果;而非安全的迭代器可能会因为rehash等操做,致使其遍历结果会有所偏差,可是它的性能更好。性能

怎么作才会安全

在redis中,安全迭代器经过直接禁止rehash操做,来让迭代器变得安全。那么,为何禁止rehash操做就安全了呢?咱们都知道,rehash操做是渐进式的。每执行一个命令,才会作一个rehash。rehash操做会同时使用字典的两个table。咱们考虑这样一种状况:假设迭代器当前正在遍历第一个table,此时进度已经到了索引index为3的位置,而某一个元素尚未进行rehash,咱们已经遍历过了这个元素。那么rehash和遍历同时进行,假设rehash完毕,这个元素到了第二个table的index为33的位置上。而目前迭代器的进度仅仅到了第二个table的index为13的位置,尚未遍历到index为33的位置上。那么若是继续遍历,因为这个元素已经在第一个table中遍历过一次,那么如今会被不可避免地遍历第二次。也就是说,因为rehash致使同一个元素被遍历了两次,这就是为何rehash会影响迭代器的遍历结果。为了解决以上问题,redis经过在安全迭代器运行期间禁止rehash操做,来保证迭代器是安全的。那么究竟redis是如何判断当前是否有安全迭代器在运行,进而来禁止rehash操做的呢?咱们首先回顾一下dict的结构:spa

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2]; // 两个table的指针
    long rehashidx;  // rehash标志,若是是-1则没有在rehash
    unsigned long iterators; // 当前运行的安全迭代器的数量
} dict;

咱们看到,字典结构中的iterators字段用来描述安全迭代器的数量。若是有一个安全迭代器在运行,那么这个字段就会++。这样,在迭代的过程当中,字典会变得相对稳定,避免了一个元素被遍历屡次的问题。若是当前有一个安全迭代器在运行,iterator字段必然不会为0。当这个字段为0的时候,才能进行rehash操做:指针

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

其实,除了安全迭代器这种简单粗暴地禁止rehash操做以外,redis还提供了SCAN这种更高级的遍历方式。它经过一种更为复杂以及巧妙的算法,来保证了即便在rehash过程当中,也能保证遍历的结果不重不漏。这就保证了rehash操做以及遍历操做可以并发执行,同时也避免了keys在遍历当键空间很大的时候超高的时间复杂度会致使redis阻塞的问题,大大提升了效率。

安全迭代器必定安全吗

那么继续思考,仅仅不进行rehash操做就可以保证迭代器是安全的了吗?因为redis是单进程的应用,因此咱们在执行keys命令的时候,会阻塞其余全部命令的执行。因此,在迭代器进行遍历的时候,咱们外部是没法经过执行命令,来对键空间字典进行增删改操做的。可是redis内部的一些时间事件会有修改字典的可能性。好比:每隔一段时间扫描某个键是否已通过期,过时了则把它从键空间中删除。这一点,我认为即便是安全迭代器,也是没法避免可能在遍历期间对字典进行操做的的。好比在遍历期间,redis某个时间事件把尚未遍历到的元素删除了,那么后续迭代器再去继续遍历,就没法遍历到这个元素了。那么如何解决这个问题呢?除非redis内部根本不在遍历期间触发事件并执行处理函数,不然这些操做所致使遍历结果的细微偏差,redis是没法避免的。

迭代器遍历的过程

抛开上面这些细节,咱们接下来看一下具体的遍历逻辑。首先咱们须要初始化安全迭代器:

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

若是是安全迭代器,除了须要初始化以上字段以外,还须要将safe字段设置为1:

dictIterator *dictGetSafeIterator(dict *d) {
    dictIterator *i = dictGetIterator(d); // 调用上面的方法初始化其他字段

    i->safe = 1; // 初始化safe字段
    return i;
}

回到开始的keys命令,它调用的就是dictGetSafeIterator()函数来初始化一个安全迭代器。接下来,keys命令会循环调用dictNext()方法对全部键空间字典中的元素作遍历:

dictEntry *dictNext(dictIterator *iter)
{
    while (1) {
    
        // 进入这个if的两种状况:
        // 1. 这是迭代器第一次运行
        // 2. 当前索引链表中的节点已经迭代完
        if (iter->entry == NULL) {

            // 指向被遍历的哈希表,默认为第一个哈希表
            dictht *ht = &iter->d->ht[iter->table];

            // 仅仅第一次遍历时执行(index初始化值为-1)
            if (iter->index == -1 && iter->table == 0) {
            
                // 若是是安全迭代器(safe == 1),那么更新iterators计数器
                if (iter->safe)
                    iter->d->iterators++;
                // 若是是不安全迭代器,那么计算指纹
                else
                    iter->fingerprint = dictFingerprint(iter->d);
            }
            
            // 更新索引,继续遍历下一个bucket上的元素
            iter->index++;

            // 若是迭代器的当前索引大于当前被迭代的哈希表的大小
            // 那么说明这个哈希表已经迭代完毕
            if (iter->index >= (signed) ht->size) {
                // 若是正在进行rehash操做,说明第二个哈希表也正在使用中
                // 那么继续对第二个哈希表进行遍历
                if (dictIsRehashing(iter->d) && iter->table == 0) {
                    iter->table++;
                    iter->index = 0;
                    ht = &iter->d->ht[1];
                // 若是没有rehash,则不须要遍历第二个哈希表
                } else {
                    break;
                }
            }

            // 若是进行到这里,说明这个哈希表并未遍历完成
            // 更新节点指针,指向下个索引链表的表头节点(index已经++过了)
            iter->entry = ht->table[iter->index];
        } else {
            // 执行到这里,说明正在遍历某个bucket上的链表(为了解决冲突会在一个bucket后面挂接多个dictEntry,组成一个链表)
            iter->entry = iter->nextEntry;
        }

        // 若是当前节点不为空,那么记录下该节点的下个节点的指针(即next)
        // 由于安全迭代器在运行的时候,可能会将迭代器返回的当前节点删除,这样就找不到next指针了
        if (iter->entry) {
            iter->nextEntry = iter->entry->next;
            return iter->entry;
        }
    }

    // 遍历完成
    return NULL;
}

具体的遍历过程已以注释的形式给出了。代码中又有一个新的概念:fingerprint指纹,下面咱们讨论一下指纹的概念。

指纹的做用

在dictNext()遍历函数中,有这样一段代码:

if (iter->safe) { // 若是是安全迭代器(safe == 1),那么更新iterators计数器
     iter->d->iterators++;
} else { // 若是是不安全迭代器,那么计算指纹
     iter->fingerprint = dictFingerprint(iter->d);
}

咱们看到,当迭代器是非安全的状况下,它会验证一个指纹。顾名思义,非安全的意思就是在遍历的时候能够进行rehash操做,这样就会致使遍历结果可能出现重复等问题。为了正确地识别这种问题,redis采用了指纹机制,即在遍历以前采集一次指纹,在遍历完成以后再次采集指纹。若是两次指纹比对一致,就说明遍历结果没有由于rehash操做的影响而改变。那么具体如何去验证指纹呢?验证指纹的本质其实就是判断字典是否由于rehash操做发生了变化:

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];
        /* 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;
}

咱们看到,指纹验证就是基于字典的table、size、used等字段来进行的。若是这几个字段发生了改变,就表明rehash操做正在执行或已执行完毕。一旦有rehash操做在执行,那么有可能就会致使遍历结果受到影响。因此,非安全迭代器的指纹验证可以很好地发现rehash操做对遍历结果产生影响的可能性。

相关文章
相关标签/搜索