因为不会Redis的scan命令,我被开除了
时间:2025-11-04 19:27:08 出处:IT科技类资讯阅读(143)
那个深夜,不会被开我登上了公司的命令服务器,在 Redis 命令行里敲入 keys* 后,不会被开线上开始报警,命令服务瞬间被卡死。不会被开
图片来自 Pexels 
我只能举起双手,命令焦急地等待几千万 key 被慢慢扫描,不会被开束手无策万念俱灰的命令时候,我收到了 Leader 的不会被开短信:你明天不用来上班了。
 
虽然上面是命令我的臆想,事实上很多公司的不会被开运维也会禁用这些命令,来防止开发出错。命令
但我在群里依然看到有同学在问“为什么 Redis 不能用 keys?不会被开我觉得挺好的呀”时,为了不让上面的命令情况发生,我决定写下这篇文章。不会被开
如何才能优雅地遍历 Redis?作为一种可以称为数据库的组件,这是多么理所应当的要求。
终于,Redis 在 2.8.0 版本新增了众望所归的 scan 操作,从此再也不用担心敲入了 keys*,然后等着定时炸弹的引爆。b2b信息网
学会使用 scan 并不困难,那么问题又来了,它是如何遍历的?当遍历过程中加入了新的 key,当遍历过程中发生了扩容,Redis 是如何解决的?
抱着深入学习的态度,以及为了能够将来在面试官面前谈笑风生,让我们一起来借此探索 Redis 的设计原理。
 
开门见山,首先让我们来总结一下 scan 的优缺点。
优点如下:
提供键空间的遍历操作,支持游标,复杂度 O(1),整体遍历一遍只需要 O(N)。 提供结果模式匹配。 支持一次返回的数据条数设置,但仅仅是个 hints,有时候返回更多。 弱状态,所有状态只需要客户端维护一个游标。缺点如下:
无法提供完整的快照遍历,也就是高防服务器中间如果有数据修改,可能有些涉及改动的数据遍历不到。 每次返回的数据条数不一定,极度依赖内部实现。 返回的数据可能有重复,应用层需要能够处理重入逻辑。所以 scan 是一个能够满足需求,但也不是完美无瑕的命令。下面来介绍一下原理,scan 到底是如何实现的?
scan,hscan 等命令主要都是借用了通用的 scan 操作函数:scanGenericCommand 。
scanGenericCommand 函数分为以下几步:
解析 count 和 match 参数,如果没有指定 count,默认返回 10 条数据。 开始迭代集合,如果是 key 保存为 ziplist 或者 intset,则一次性返回所有数据,没有游标(游标值直接返回 0)。由于 Redis 设计,只有数据量比较小的时候才会保存为 ziplist 或者 intset,所以此处不会影响性能。b2b供应网
游标在保存为 hash 的时候发挥作用,具体入口函数为 dictScan,下文详细描述。
根据 match 参数过滤返回值,并且如果这个键已经过期也会直接过滤掉(Redis 中键过期之后并不会立即删除)。当迭代一个哈希表时,存在三种情况:
从迭代开始到结束,哈希表没有进行 rehash。 从迭代开始到结束,哈希表进行了 rehash,但是每次迭代时,哈希表要么没开始 rehash,要么已经结束了 rehash。 从迭代开始到结束,某次或某几次迭代时哈希表正在进行 rehash。在这三种情况之下,sacn 是如何实现的?首先需要知道的前提是:Redis 中进行 rehash 扩容时会存在两个哈希表,ht[0] 与 ht[1],rehash 是渐进式的,即不会一次性完成。
新的键值对会存放到 ht[1] 中并且会逐步将 ht[0] 的数据转移到 ht[1]。全部 rehash 完毕后,ht[1] 赋值给 ht[0] 然后清空ht[1]。
 
模拟问题
①迭代过程中,没有进行 rehash
这个过程比较简单,一般来说只需要最简单粗暴的顺序迭代就可以了,这种情况下没什么好说的。
②迭代过程中,进行过 rehash
但是字典的大小是能够进行自动扩容的,我们不得不考虑以下两个问题:
第一,假如字典扩容了,变成 2 倍的长度,这种情况下,能够保证一定能遍历所有最初的 key,但是却会出现大量重复。
举个例子:比如当前的 key 数组大小是 4,后来变为 8 了。假如从下表 0,1,2,3 顺序扫描时,如果数组已经发生扩容。
那么前面的 0,1,2,3 slot 里面的数据会发生一部分迁移到对应的 4,5,6,7 slot 里面去,当扫描到 4,5,6,7 的 slot 时,无疑会出现值重复的情况。
需要知道的是,Redis 按如下方法计算一个当前 key 扩容后的 slot:hash(key)&(size-1)。
如图,当字典大小从 4 扩容到 8 时,原先在 0 slot 的数据会分散到 0(000) 与 4(100) 两个 slot,对应关系表如下:
 
第二, 如果字典缩小了,比如从 16 缩小到 8, 原先 scan 已经遍历了 0,1,2,3 ,如果数组已经缩小。
这样后来迭代停止在 7 号 slot,但是 8,9,10,11 这几个 slot 的数据会分别合并到 0,1,2,3 里面去,从而 scan 就没有扫描出这部分元素出来,无法保证可用性。
③迭代过程中,正在进行 rehash
上面考虑的情况是,在迭代过程的间隙中,rehash 已经完成。那么会不会出现迭代进行中,切换游标时,rehash 也正在进行?当然可能会发生。
如果返回游标 1 时正在进行 rehash,那么 ht[0](扩容之前的表)中的 slot1 中的部分数据可能已经 rehash 到 ht[1](扩容之后的表)中的 slot1 或者 slot4。
此时必须将 ht[0] 和 ht[1] 中的相应 slot 全部遍历,否则可能会有遗漏数据,但是这么做好像也非常麻烦。
解决方法
为了解决以上两个问题,Redis 使用了一种称为:reverse binary iteration 的算法。
源码如下:
unsigned long dictScan(dict *d, unsigned long v, dictScanFunction *fn, void *privdata){ if (!dictIsRehashing(d)) { t0 = (d->ht[0]); m0 = t0->sizemask; /* Emit entries at cursor */ while (de) { fn(privdata, de); de = de->next; } } else { m0 = t0->sizemask; m1 = t1->sizemask; de = t0->table[v & m0]; while (de) { fn(privdata, de); de = de->next; } do { de = t1->table[v & m1]; while (de) { fn(privdata, de); de = de->next; } v = (((v | m0) + 1) & ~m0) | (v & m0); } while (v & (m0 ^ m1)); } v |= ~m0; v = rev(v); v++; v = rev(v); return v; }一起来理解下核心源码,第一个 if,else 主要通过 dictIsRehashing 这个函数来判断是否正在 rehash。
sizemask 指的是字典空间长度,假如长度为 16,那么 sizemask 的二进制为 00001111。m0 代表当前字典的长度,v 代表游标所在的索引值。
接下来关注这个片段:
v |= ~m0; v = rev(v); v++; v = rev(v);这段代码初看好像有点摸不着头脑,怎么多次在多次 rev?我们来看下在字典长度从 4 rehash 到 8 时,scan 是如何迭代的。
当字典长度为 4 时,m0 等于 4,二进制表示为 00000011,那么 ~m0 为 11111100,v 初始值为 0,那么 v |=~m0为11111100。
接下来看图:
 
可以看到,第一次 dictScan 后,游标从 0 变成了 2,四次遍历分别为 0→2→1→3,四个值都遍历到了。
在字典长度为 8 时,遍历情况如下:

遍历顺序为:0→4→2→6→1→5→3→7。
是不是察觉了什么?遍历顺序是不是顺序是一致的?如果还没发现,不妨再来看看字典长度为 16 时的遍历情况,以及三次顺序的对比:
 
让我们设想这么一个情况,字典的大小本身为 4,开始迭代,当游标刚迭代完 slot0 时,返回的下一个游标是 slot2。
此时发现字典的大小已经从 4 rehash 到 8,那么不妨继续从 size 为 8 的 hashtable 中 slot2 处继续迭代。
有人会说,不是把 slot4 遗漏掉了吗?注意之前所说的扩容方式:hash(key)&(size-1),slot0 和 slot4 的内容是相同的,巧妙地避开了重复,当然,更不会遗漏。
如果你看到这里,你可能会发出和我一样的感慨:我 X,这算法太牛 X 了。
没错,上面的算法是由 Pieter Noordhuis 设计实现的,Redis 之父 Salvatore Sanfilippo 对该算法的评价是“Hard to explain but awesome。”
 
字典扩大的情况没问题,那么缩小的情况呢?可以仿照着自己思考一下具体步骤。答案是可能会出现重复迭代,但是不会出现遗漏,也能够保证可用性。
迭代过程中,进行过 rehash 这种情况下的迭代已经比较完美地解决了,那么迭代过程中,正在进行 rehash 的情况是如何解决的呢?
我们继续看源码,之前提到过 dictIsRehashing 这个函数用来判断是否正在进行 rehash。
那么主要就是关注这段源码:
m0 = t0->sizemask; m1 = t1->sizemask; de = t0->table[v & m0]; while (de) { fn(privdata, de); de = de->next; } do { de = t1->table[v & m1]; while (de) { fn(privdata, de); de = de->next; } v = (((v | m0) + 1) & ~m0) | (v & m0); } while (v & (m0 ^ m1));m0 代表 rehash 前的字典长度,假设为 4,即 00000011,m1 代表 rehash 后的字典长度,假设为 8,即 00000111。
首先当前游标 &m0 可以得到较小字典中需要迭代的 slot 的索引,然后开始循环迭代。
然后开始较大字典的迭代,首先我们关注一下循环条件:
v & (m0 ^ m1)m0,m1 二者经过异或操作后的值为 00000100,可以看到只留下了最高位的值。
游标 v 与之做 & 操作,将其作为判断条件,即判断游标 v 在最高位是否还有值。
当高位为 0 时,说明较大字典已经迭代完毕。(因为较大字典的大小是较小字典的两倍,较大字典大小的最高位一定是 1)
到此为止,我们已经将 scan 的核心源码通读一遍了,相信很多其间的迷惑也随之解开。
 
不仅在写代码的时候更自信了,假如日后被面试官问起相关问题,那绝对可以趁机表现一番,想想还有点小激动。

猜你喜欢
- 电脑端设置主页教程(简单教你如何在电脑上设置主页)
 - 安装Ubuntu之后默认英文,选简体中文,正常来说这样装好就应该能用的,可是这个时候又出现了错误,說:broken package database. 请用apt-get install -f解决问题之后再安装。1、如图,左上角红色按钮,单击输入software,选择Ubuntu Software Center2、如图,选择installed,右边搜索thunder,选择列表中的thunderbird,点右边的remove,输入密码之后稍等片刻3、此时再次进入language support,按部就班,应该大家都熟悉啦:)4、好了。
 - 10月13日消息,Ubuntu 15.10(Wily Werewolf)即将在10月22日正式发布,目前Ubuntu 15.10已经确认达成最终内核的冻结,也就是说,今后除了一些bug修复,将不会再有相关升级。具体说来,Ubuntu 15.10进入冻结阶段后,其软件栈和内核都不会再有升级,这能够让开发者更好地进行测试,为最终发布做好准备。据悉,Ubuntu 15.10所用Linux内核为4.2版。来自Canonical的Joseph Salisburty几天前就表示:“我们即在10月8日达成Wily Werewoft内核冻结,若还有针对15.10的补丁,请尽快提交。按照内核冻结的最终期限,所有补丁需要遵守我们的SRU策略,存在错过发布的可能。”
 - Ubuntu 14.04 LTS 已经出来了,我要如何(怎样)升级到Ubuntu 14.04 LTS版本呢?我们可以从镜像或者主要发型版本来升级到最新版本复制代码代码如下:$ uname -mrs复制代码代码如下:Linux 3.2.0-51-generic x86_64复制代码代码如下:$ sudo apt-get update复制代码代码如下:$ sudo do-release-upgratedo-release-upgrate 会运行升级工具。你只需要根据屏幕上的提示操作即可。复制代码代码如下:Checking for a new Ubuntu release复制代码代码如下:sudo do-release-upgrade -d提醒:关于从Ubuntu 13.10 从桌面 升级系统的操作首先,你需要移除所有第三方的二进制驱动,比如 NVIDIA 或者 AMD 显卡驱动。一旦移除后再重启桌面,按住 ALT+F2 并且在 命令框中输入 update-managerupdate manager 会打开并告诉你: New distribution release 14.04 LTS is available(新版的版本 14.04 LTS已经可以使用).只要点击 Upgrade(升级),然后跟着屏幕上的指示操作即可。注意所有的TLS 桌面版用户需要等到一个叫做 Ubuntu LTS v14.04.1 释放出来才行。假如不想等这个版本,可以在 update-manager中使用 -d 参数来升级。可以通过这种方式,将 Ubuntu 12.04 LTSs 升级到 Ubuntu 14.04 LTS 版本:复制代码代码如下:$ sudo reboot然后确认你是否升级到了最新版本;复制代码代码如下:$ lsb_release -a$ uname -mrs$ tail -f /var/log/app/log/file确认升级到最新版本后,再重新安装第三方的二进制驱动。
 - 台式电脑内置喇叭安装教程(一步步教你如何给台式电脑安装内置喇叭)
 - 2015年4月24 日,优麒麟开发团队很高兴地宣布今天发布 15.04 正式版本。此次发布的版本以本地化体验和稳定性为主,我们修复了之前版本积累的很多本地化/国际化问题,并着力将最初为中国用户开发的优客助手等软件国际化,以服务更多的用户。下载地址:http://www.ubuntukylin.com/downloads/相对于 14.10 版本,本次发布的版本采用最新的 3.19 系统内核,Unity 桌面升级为 7.3.2 版本,Ubuntu Kylin 软件中心升级为 1.3.1 版本,优客助手升级到 2.0.2 版本,优客农历升级到 1.0.2 版本,快盘升级到 2.0.0.4 版本,优客企鹅(小企鹅面板)升级到 2.1.0 版本,搜狗输入法升级到 1.2.0 版本,WPS 升级到 2014 版本。
 - deepin音乐播放器是一款外观较为漂亮的播放软件了。虽然比不上windows下的那些华丽的播放器。但从实用性出发已经够了。它可以播放本地的音乐,当然,也可以添加一些插件,获得播放在线音乐的功能。相信deepin linux 用户正在用的不亦乐乎,然而,用其他版本linux的可能就要麻烦一下,动下手啦。1、首先是要安装deepin音乐播放器,这点我已经发过,不在赘述。2、直接开始安装插件。打开终端,在终端输入命令:sudo apt-get install cython libwebkitgtk-dev python-dev git3、然后在开始安装pyjavascriptcore同样在终端下输入命令:git clone https://github.com/sumary/pyjavascriptcore.git4、然后输入:cd pyjavascriptcore5、在输入:sudo python setup.py install6、完成上述步骤之后,接下来正式开始安装百度音乐的插件。在终端中输入复制内容到剪贴板 git clone https://github.com/sumary/dmusic-plugin-baidumusic.git cd dmusic-plugin-baidumusic cp -r baidumusic ~/.local/share/deepin-music-player/plugins/ 7、然后打开deepin音乐,选项设置—插件—选择百度音乐——选择启用。然后设置完成。
 - ubutnu14.04默认登陆界面看久了有点审美疲劳,想换个自己喜欢的背景图片应该如何去设置呢?需要的朋友可以参考下1、添加ubuntu14.04下的ubuntu-tweak的ppa源:$sudo add-apt-repository ppa:tualatrix/ppa2、系统更新:$sudo apt-get update 3、安装ubuntu tweak工具:$sudo apt-get install ubuntu-tweak4、打卡dash菜单搜索ubuntu tweak 并打开5、进入导航界面的 调整 栏目 进入 登陆设置 6、点击上面的 解锁 输入密码 解锁后才可以配置图片7、解锁后 点击 下面的 设置为与当前桌面相同的背景 8、注销后登陆界面的背景就变成了自己设置的桌面的背景图片相关推荐:重新设置Ubuntu登录密码的方法教程
 - Flyme5.1.6.0a(打造独特个性化的手机主题,尽享视觉盛宴)