Redis中scan命令的作用是什么

56次阅读
没有评论

共计 4128 个字符,预计需要花费 11 分钟才能阅读完成。

自动写代码机器人,免费开通

这期内容当中丸趣 TV 小编将会给大家带来有关 Redis 中 scan 命令的作用是什么,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。

SCAN 命令

SCAN 命令的有 SCAN,SSCAN,HSCAN,ZSCAN。

SCAN 的话就是遍历所有的 keys

其他的 SCAN 命令的话是 SCAN 选中的集合。

SCAN 命令是增量的循环,每次调用只会返回一小部分的元素。所以不会有 KEYS 命令的坑。

SCAN 命令返回的是一个游标,从 0 开始遍历,到 0 结束遍历。

今天我们主要从底层的结构和源码的角度来讨论 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 = rev(v);

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

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

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

Redis 中 scan 命令的作用是什么

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

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

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,并且更新两张表中元素的数量。

每次迁移完一个 bucket,需要将旧表中的 bucket 指向 NULL。

最后判断一下是否全部迁移完成,如果是,则收回空间,重置 rehash 索引,否则告诉调用方,仍有数据未迁移。

由于 Redis 使用的是渐进式 rehash 机制,因此,scan 命令在需要同时扫描新表和旧表,将结果返回客户端。

上述就是丸趣 TV 小编为大家分享的 Redis 中 scan 命令的作用是什么了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注丸趣 TV 行业资讯频道。

向 AI 问一下细节

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-12-04发表,共计4128字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)