スキャンとコマンドキーを模索Redisの記事

序文

Redisのは、大量のデータの場合には、古典的な疑問を持って、情報キーの出会い一定のルール見上げるようなものは、二つの方法が、ここにありますか

A、キーコマンド

シンプル、粗起因Redisのこの機能は、シングルスレッドに大きな費用を達成するために、キーコマンドが道をブロックに実行される、キーがトラバースの複雑さを達成するための方法であり、O(n)と、複数のRedisライブラリキーは、見つけることです長いブロックする時間を作ります。

キーは*、キーは*すべてのキーのクエリとクエリプレフィックスcodeholeキーですcodehole。あまりにも暴力的、パフォーマンスの低下は、全体のRedisを検索しています。

短所:

1、オフセットなし、制限パラメータが存在しない、キーの例では、文字列フルスクリーンブラシの端に表示されていない満たしワット数百人の条件がある場合、あなたが不快知って、放電時のキーのすべての条件を満たして。

2、アルゴリズムはトラバーサルアルゴリズムの複雑さはO(n)は、キーの例以上の千万がある場合は、このディレクティブはRedisのは読んで、Redisのサービスカトンにつながるとすべての他の命令を書きますであるキーでも遅延されます彼らは現在の命令までのキーの上に続けることができます前に、エラーアウトの時間は、Redisのシングルスレッドのプログラムであるため、すべての命令を実行し、他の命令を実行する必要があります。

二、スキャンコマンド

強力なオプション、ほとんどのケースでは、コマンドキーを置き換えることができ、非ブロックキーの値を達成するための方法を探します

スキャンコマンドの機能:

1、複雑さはO(N)であり、それは段階的カーソルを介して行われ、スレッドがブロックされないが、

図2に示すように、結果の最大数は毎時間、ヒントのみ制限を返さ限界パラメータ制御を提供するために、返された結果は、多かれ少なかれであってもよいです。

図3は、キーのように、それはまた、パターンマッチングを提供します。

4は、サーバーカーソルは、カーソルの状態のみがクライアントにカーソルバックに整数をスキャンすることで、状態を保存する必要はありません。

図5は、結果が繰り返しに、クライアントのニーズを重複することが返され、これは非常に重要であり、
通常の状況下では、スキャン、問題のない使用は、あなたが焼き直している場合は、再読み込みになります
例を、4樽があり、0,1を読みます焼き直しが発生した場合には5〜0〜4、1を再読み込み引き起こされるであろう、
次のように0426の後にトラバース02に横切らなければならない場合Redisの高いビット増分は、横断
が、体積減少がキーを繰り返すようが生じる場合があります

00  0
10  2
01  1
11  3
 
000 0
100 4
010 2
110 6
001 1
101 7
011 3
111 8

図6に示すように、トラバースデータ変更時に、データが変更に横切ることができない場合は不明です。

図7に示すように、単一の結果を返す、トラバースの端部を意味するものではない空であるが、カーソル返される値に依存はゼロであります

三、キー、スキャンコマンドの特定の使用法

たとえば、クエリがここにkey111何のキーを開始しますか?

あなたは、コマンドキーを使用する場合は、キーにkey1111 *を実行し、すべてを一度にチェックしてください。
ここに画像を挿入説明

同様に、スキャンコマンドであれば、とscan 0 match key1111* count 20

ここに画像を挿入説明
スキャン構文は次のとおりですSCAN cursor [MATCH pattern] [COUNT count]The default COUNT value is 10

SCANコマンドは、カーソルベースのイテレータです。コマンドはすべての時間の前に反復プロセスを継続するためには、呼び出しのカーソルカーソルのパラメータを返すために呼び出しとしてこれを使用する必要が呼び出されることをこれが意味。

ここで使用されるようにscan 0 match key1111* count 20、この調査を完了するために、コマンド、やや意外に、結果にクエリ、ビューの主点からスキャンコマンドを使用して起動しませんでした。

スキャンキーを横断するとき、0は最初の時間を表しkey1111試合の開始を表し、key1111は*モデルによると、20カウント、20資格キーの出力の代表ではなく、スロットの数は、サーバを介して単一のパスを辞書限定(ほぼ等しいです)。

だから、また、データスロット何と呼ばれていますか?このスロットは、Redisのクラスタスロットではないでしょうか?答えはノーです。実際には、図は、答えを与えてきました。

それは言うならば、「辞書スロット」の数は、スロットのクラスターですが、また、スロットクラスタの数は16384である知っている、そして16,384スロットを通過した後に、明らかにトラバースするときことがわかり上記のすべてのキー情報を通過できなければなりませんカーソルがまだ結果を経て完成されていない場合2万スロットの数は、辞書ので、辞書は、クラスタ内のスロット溝の概念ことを意味するものではありません。

经过测试,在scan的时候,究竟遍历多大的COUNT值能完全match到符合条件的key,跟具体对象的key的个数有关,
如果以超过key个数的count来scan,必定会一次性就查找到所有符合条件的key,比如在key个数为10W个的情况下,一次遍历20w个字典槽,肯定能完全遍历出来结果。

ここに画像を挿入説明

四、探究scan底层源码

Redis的结构

Redis使用了Hash表作为底层实现,原因不外乎高效且实现简单。说到Hash表,很多Java程序员第一反应就是HashMap。没错,Redis底层key的存储结构就是类似于HashMap那样数组+链表的结构。其中第一维的数组大小为2n(n>=0)。每次扩容数组长度扩大一倍。

scan命令就是对这个一维数组进行遍历。每次返回的游标值也都是这个数组的索引。limit参数表示遍历多少个数组的元素,将这些元素下挂接的符合条件的结果都返回。因为每个元素下挂接的链表大小不同,所以每次返回的结果数量也就不同。

SCAN的遍历顺序

关于scan命令的遍历顺序,我们可以用一个小栗子来具体看一下。

127.0.0.1:6379> keys *
1) "db_number"
2) "key1"
3) "myKey"
127.0.0.1:6379> scan 0 MATCH * COUNT 1
1) "2"
2) 1) "db_number"
127.0.0.1:6379> scan 2 MATCH * COUNT 1
1) "1"
2) 1) "myKey"
127.0.0.1:6379> scan 1 MATCH * COUNT 1
1) "3"
2) 1) "key1"
127.0.0.1:6379> scan 3 MATCH * COUNT 1
1) "0"
2) (empty list or set)

我们的Redis中有3个key,我们每次只遍历一个一维数组中的元素。如上所示,SCAN命令的遍历顺序是

0->2->1->3

这个顺序看起来有些奇怪。我们把它转换成二进制就好理解一些了。

00->10->01->11

我们发现每次这个序列是高位加1的。普通二进制的加法,是从右往左相加、进位。而这个序列是从左往右相加、进位的。这一点我们在redis的源码中也得到印证。

在dict.c文件的dictScan函数中对游标进行了如下处理

v = rev(v);
v++;
v = rev(v);

意思是,将游标倒置,加一后,再倒置,也就是我们所说的“高位加1”的操作。

这里大家可能会有疑问了,为什么要使用这样的顺序进行遍历,而不是用正常的0、1、2……这样的顺序呢,这是因为需要考虑遍历时发生字典扩容与缩容的情况(不得不佩服开发者考虑问题的全面性)。

我们来看一下在SCAN遍历过程中,发生扩容时,遍历会如何进行。加入我们原始的数组有4个元素,也就是索引有两位,这时需要把它扩充成3位,并进行rehash。

IMG

rehash

原来挂接在xx下的所有元素被分配到0xx和1xx下。在上图中,当我们即将遍历10时,dict进行了rehash,这时,scan命令会从010开始遍历,而000和100(原00下挂接的元素)不会再被重复遍历。

再来看看缩容的情况。假设dict从3位缩容到2位,当即将遍历110时,dict发生了缩容,这时scan会遍历10。这时010下挂接的元素会被重复遍历,但010之前的元素都不会被重复遍历了。所以,缩容时还是可能会有些重复元素出现的。

总结:scan遍历顺序采用高位进位加法来遍历,进位的方向是从高位到低位,原因是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。

Redis的rehash

rehash是一个比较复杂的过程,为了不阻塞Redis的进程,它采用了一种渐进式的rehash的机制。

/* 字典 */
typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */
} dict;

在Redis的字典结构中,有两个hash表,一个新表,一个旧表。在rehash的过程中,redis将旧表中的元素逐步迁移到新表中,接下来我们看一下dict的rehash操作的源码。

/* Performs N steps of incremental rehashing. Returns 1 if there are still
 * keys to move from the old to the new hash table, otherwise 0 is returned.
 *
 * Note that a rehashing step consists in moving a bucket (that may have more
 * than one key as we use chaining) from the old to the new hash table, however
 * since part of the hash table may be composed of empty spaces, it is not
 * guaranteed that this function will rehash even a single bucket, since it
 * will visit at max N*10 empty buckets in total, otherwise the amount of
 * work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while(de) {
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}

通过注释我们就能了解到,rehash的过程是以bucket为基本单位进行迁移的。所谓的bucket其实就是我们前面所提到的一维数组的元素。每次迁移一个列表。下面来解释一下这段代码。

  • 首先判断一下是否在进行rehash,如果是,则继续进行;否则直接返回。
  • 接着就是分n步开始进行渐进式rehash。同时还判断是否还有剩余元素,以保证安全性。
  • 在进行rehash之前,首先判断要迁移的bucket是否越界。
  • 然后跳过空的bucket,这里有一个empty_visits变量,表示最大可访问的空bucket的数量,这一变量主要是为了保证不过多的阻塞Redis。
  • 接下来就是元素的迁移,将当前bucket的全部元素进行rehash,并且更新两张表中元素的数量。
  • 各移行バケットの後、あなたは古いテーブルバケットNULLを指すようにする必要があります。
  • 最後に、全体の移行が完了しているかを判断し、もしそうであれば、再利用スペース、リセット焼き直しインデックス、または、まだない移行されたデータを呼び出し側に伝えます。

Redisのは、進歩的な焼き直しメカニズム、古いテーブルと新しいテーブルをスキャンするので、スキャンコマンドの必要性を使用しているので、結果がクライアントに返されます。

概要:Redisの拡張:古いものと新しい配列を保持しながら、新しいの新しいセットに古いデータを移動し、Redisのは、進歩的な焼き直し可能

参考記事:

https://www.jianshu.com/p/be15dc89a3e8

https://blog.csdn.net/zanpengfei/article/details/83691841

http://jinguoxing.github.io/redis/2018/09/04/redis-scan/

公開された107元の記事 ウォン称賛14 ビュー40000 +

おすすめ

転載: blog.csdn.net/belongtocode/article/details/103969400