前言

熟悉Redis的人都知道,它是单线程的。因此在使用一些时间复杂度为O(N)的命令时要非常谨慎。可能一不小心就会阻塞进程,导致Redis出现卡顿。

有时,我们需要针对符合条件的一部分命令进行操作,比如删除以test_开头的key。那么怎么获取到这些key呢?在Redis2.8版本之前,我们可以使用keys命令按照正则匹配得到我们需要的key。但是这个命令有两个缺点:

没有limit,我们只能一次性获取所有符合条件的key,如果结果有上百万条,那么等待你的就是“无穷无尽”的字符串输出。

keys命令是遍历算法,时间复杂度是O(N)。如我们刚才所说,这个命令非常容易导致Redis服务卡顿。因此,我们要尽量避免在生产环境使用该命令。

在满足需求和存在造成Redis卡顿之间究竟要如何选择呢?面对这个两难的抉择,Redis在2.8版本给我们提供了解决办法——scan命令。

相比于keys命令,scan命令有两个比较明显的优势:

scan命令的时间复杂度虽然也是O(N),但它是分次进行的,不会阻塞线程。

scan命令提供了limit参数,可以控制每次返回结果的最大条数。

这两个优势就帮助我们解决了上面的难题,不过scan命令也并不是完美的,它返回的结果有可能重复,因此需要客户端去重。至于为什么会重复,相信你看完本文之后就会有答案了。

关于scan命令的基本用法,可以参看Redis命令详解:Keys一文中关于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++;

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的机制。

/* 字典 */

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命令在需要同时扫描新表和旧表,将结果返回客户端。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

java redis scan6_Redis中scan命令的深入讲解相关推荐

  1. java redis sentinel_Java中的Redis 哨兵高可用性

    让我们探索Redis Sentinel,看看如何在Java上运行它,一起来看看,最近get了很多新知识,分享给大家参考学习.需要详细的java架构思维导图路线也可以评论获取! 什么是Redis哨兵? ...

  2. Java 并发编程中的死锁 ( Kotlin 语言讲解)

    什么是死锁? 在操作系统中的并发处理场景中, 进程对资源的持有与请求过程中,会产生死锁. Say, Process A has resource R1 , Process B has resource ...

  3. redis的scan命令的源码分析,实现原理

    简言 1. 线上环境keys命令不可用,会导致redis卡死.scan命令因为可以分批遍历,比较实用 2. scan命令包括多个 遍历整个数据库的scan命令,处理函数 scanCommand(),最 ...

  4. HBase Scan命令详解

    hbase中scan命令是我们经常使用到的,而filter的作用尤其强大.这里简要的介绍下scan下filter命令的使用. 插入scan命令需要的数据 这里模拟了部分微博评论的数据,然后使用代码插入 ...

  5. java redis 命令_命令界面:使用Java中的动态API处理Redis

    java redis 命令 Redis是一个数据存储,支持190多个文档化命令和450多个命令排列. 社区积极支持Redis开发: 每个主要的Redis版本都附带新命令. 今年,Redis向第三方供应 ...

  6. java redis优化_Redis性能优化:使用scan命令替换keys

    由于每个Redis实例是使用单线程处理所有请求的,故keys命令和其他命令都是在同一个队列排队等待执行的,如果keys命令执行时间长(数量多),则会阻塞其他命令的执行,导致性能问题. scan命令是在 ...

  7. redis 命令 释放连接_redis scan命令导致redis连接耗尽,线程上锁的解决

    使用redis scan方法无法获取connection,导致线程锁死. 0.关键字 redis springboot redistemplate scan try-with-resource 1.异 ...

  8. 深入理解Redis的scan命令

    熟悉Redis的人都知道,它是单线程的.因此在使用一些时间复杂度为O(N)的命令时要非常谨慎.可能一不小心就会阻塞进程,导致Redis出现卡顿. 有时,我们需要针对符合条件的一部分命令进行操作,比如删 ...

  9. Redis 数据类型与使用命令大全以及Java使用

    基础数据类型 具体详情可直接访问Redis官网,这里只做命令总结 String 二进制安全的字符串 Lists: 按插入顺序排序的字符串元素的集合.他们基本上就是链表(linked lists). S ...

  10. Redis Scan命令

    原地址:https://www.cnblogs.com/tekkaman/p/4887293.html [Redis Scan命令] SCAN cursor [MATCH pattern] [COUN ...

最新文章

  1. RDKit | 基于RDKit绘制化学反应
  2. linux显示没有网卡
  3. 【计算理论】计算复杂性 ( 多项式等价引入 | 多项式时间规约 )
  4. python与java的比较_Python和Java两者有什么区别?
  5. 铺地毯pascal程序
  6. ORA-01109:数据库无法启动问题
  7. C#中判断空字符串的3种方法性能分析
  8. 爱泼斯坦事件发酵,MIT师生发起抗议逼迫校长Rafael Reif辞职
  9. Numpy实现酒鬼漫步问题【以及randint()、where()、cumsum()、argmax()的用法详解】
  10. element 登录_Python selenium自动化测试框架入门实战--登录测试案例
  11. xhtml 1.0与html4.0区别大全
  12. 七月算法机器学习 6 特征工程
  13. 2020年微信视频号数据分析生态趋势调查报告
  14. MAC地址批量生成器
  15. 重力坝计算c语言程序,混凝土重力坝计算程序
  16. 学习笔记 | 深度学习相关研究与展望 Review of deep learning
  17. 【idea打包jar包+运行jar包】亲测详解
  18. 038-拯救大兵瑞恩之 TiDB 如何在 TiKV 损坏的情况下恢复
  19. 常用工具软件的交叉编译
  20. 3 MyBatis动态SQL

热门文章

  1. 5面阿里,终获offer(Java后端)
  2. 鹏业安装算量软件通风扣减功能
  3. 对应阻尼下的开环增益matlab,自动控制原理实验指导书MATLAB版解析.doc
  4. PX4自主设置飞行模式
  5. 自己怎么做网站,个人做网站的步骤
  6. html 文字竖着排引号,竖排文字 引号如何使用?
  7. font标签的size属性
  8. html没有写font标签却出现font标签解决方案
  9. html打包的app软件报毒解析
  10. 三星服务器锁微信,三星手机微信支付设置指纹锁步骤