[转帖]Redis中遍历大数据量的key:keys与scan命令

redis,遍历,数据量,key,keys,scan,命令 · 浏览次数 : 0

小编点评

**代码解析** **dictRehash函数** * **参数** * d:指向Redis的dict结构的指针 * n:表示要迁移的元素的数量 * **返回值** * 0:表示成功 * 1:表示还有元素需要迁移 * **函数逻辑** * 判断是否在进行rehash * 如果是,则继续进行 * 分n步进行渐进式rehash * 判断是否还有剩余元素 * 迁移元素 * 更新两张表中元素的数量 * 返回成功标志 **其他** * **注释** * 指示如何正确访问Redis中的海量数据 * 服务才不会挂掉! * 解放空间,重置rehash索引 * 生成内容时需要带简单的排版

正文

https://www.cnblogs.com/-wenli/p/13045432.html

 

keys命令

keys * 、keys id:* 分别是查询全部的key以及查询前缀为id:的key。

缺点:

1、没有 offset、limit 参数,一次返回所有满足条件的 key。

2.keys算法是遍历算法,复杂度是O(n),也就是数据越多,时间复杂度越高。

3.数据量达到几百万,keys这个指令就会导致 Redis 服务卡顿,因为 Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才可以继续。

 

scan命令

那我们如何去遍历大数据量呢?我们可以采用redis的另一个命令scan。我们看一下scan的特点

  • 复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程

  • 提供 count 参数,不是结果数量,是redis单次遍历字典槽位数量(约等于)

  • 同 keys 一样,它也提供模式匹配功能;

  • 服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;

  • 返回的结果可能会有重复,需要客户端去重复,这点非常重要;

  • 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零

scan命令格式

SCAN cursor [MATCH pattern] [COUNT count]

命令解释:scan 游标 MATCH <返回和给定模式相匹配的元素> count 每次迭代所返回的元素数量

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

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

redis > scan 0 match user_token* count 5 
 1) "6"
 2) 1) "user_token:1000"
 2) "user_token:1001"
 3) "user_token:1010"
 4) "user_token:2300"
 5) "user_token:1389"

从0开始遍历,返回了游标6,又返回了数据,继续scan遍历,就要从6开始

redis > scan 6 match user_token* count 5 
 1) "10"
 2) 1) "user_token:3100"
 2) "user_token:1201"
 3) "user_token:1410"
 4) "user_token:5300"
 5) "user_token:3389"

Redis的结构

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

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

SCAN的遍历顺序

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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函数中对游标进行了如下处理

1
2
3
v = rev(v);
v++;
v = rev(v);
 

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

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

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

原来挂接在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的机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 字典 */
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操作的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/* 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命令在需要同时扫描新表和旧表,将结果返回客户端。


refer:

如何正确访问Redis中的海量数据?服务才不会挂掉!

Redis中scan命令的深入讲解

与[转帖]Redis中遍历大数据量的key:keys与scan命令相似的内容:

[转帖]Redis中遍历大数据量的key:keys与scan命令

https://www.cnblogs.com/-wenli/p/13045432.html keys命令 keys * 、keys id:* 分别是查询全部的key以及查询前缀为id:的key。 缺点: 1、没有 offset、limit 参数,一次返回所有满足条件的 key。 2.keys算法是

[转帖]Redis Scan 原理解析与踩坑

https://www.cnblogs.com/jelly12345/p/16424080.html 1. 概述由于 Redis 是单线程在处理用户的命令,而 Keys 命令会一次性遍历所有 Key,于是在 命令执行过程中,无法执行其他命令。这就导致如果 Redis 中的 key 比较多,那么 Ke

[转帖]Redis中Key的过期策略和淘汰机制

Key的过期策略 Redis的Key有3种过期删除策略,具体如下: 1. 定时删除 原理:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作优点:能够很及时的删除过期的Key,能够最大限度的节约内存缺点:对CPU时间不友好,如果过期的Key比

[转帖]redis中的bigkey问题

https://cdn.modb.pro/db/459810 什么是bigkey bigkey就是redis key/value体系中的大value问题。我们知道redis的底层数据存储结构中,有多种数据结构的实现。 String: 简单动态字符串 List: 双向链表、压缩列表 Hash: 哈希表

[转帖]redis中keys与scan命令的区别有哪些

https://www.yisu.com/zixun/445908.html 这篇文章将为大家详细讲解有关redis中keys与scan命令的区别有哪些,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。 redis keys和scan的区别 redis的keys命令,

[转帖]Redis中的Lua脚本

最近琢磨分布式锁时接触到的知识点,简单记一下。 文章目录 1. Redis中的Lua2. 利用Lua操作Redis3. Lua脚本的原子性4. 关于 EVALSHA5. 常用`SCRIPT` 命令6. 脚本本地化 1. Redis中的Lua Redis支持Lua,代码直接发送完整脚本即可。基本语法(

[转帖]Redis key 乱码问题(springboot)

保存到redis中的key 前半段会出现乱码问题 原来配置: @Configuration@EnableCachingpublic class RedisCacheConfig { @Bean public CacheManager cacheManager(RedisTemplate

[转帖]【Redis】Redis中使用Lua脚本

Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。 Lua具体语法参考:https://www.runoob.com/lua/lua-tutorial.html 脚本的原子性 Redis使用单个Lua解释器去运

[转帖]Redis持久化-RDB和AOF

持久化的功能: Redis是内存数据库, 数据都是存储在内存中, 为了避免进程退出导致数据的永久丢失, 需要定期将Redis中的数据以某种形式(数据或命令) 从内存保存到硬盘。 当下次Redis重启时, 利用持久化文件实现数据恢复。 除此之外, 为了进行灾难备份, 可以将持久化文件拷贝到一个远程位置

[转帖]Redis系列(十七)、Redis中的内存淘汰策略和过期删除策略

我们知道Redis是分布式内存数据库,基于内存运行,可是有没有想过比较好的服务器内存也不过几百G,能存多少数据呢,当内存占用满了之后该怎么办呢?Redis的内存是否可以设置限制? 过期的key是怎么从内存中删除的?不要怕,本篇我们一起来看一下Redis的内存淘汰策略是如何释放内存的,以及过期的key