因为不会Redis的scan命令,我被开除了
mhr18 2024-12-03 12:04 18 浏览 0 评论
那个深夜,我登上了公司的服务器,在 Redis 命令行里敲入 keys* 后,线上开始报警,服务瞬间被卡死。
图片来自 Pexels
我只能举起双手,焦急地等待几千万 key 被慢慢扫描,束手无策万念俱灰的时候,我收到了 Leader 的短信:你明天不用来上班了。
虽然上面是我的臆想,事实上很多公司的运维也会禁用这些命令,来防止开发出错。
但我在群里依然看到有同学在问“为什么 Redis 不能用 keys?我觉得挺好的呀”时,为了不让上面的情况发生,我决定写下这篇文章。
如何才能优雅地遍历 Redis?作为一种可以称为数据库的组件,这是多么理所应当的要求。
终于,Redis 在 2.8.0 版本新增了众望所归的 scan 操作,从此再也不用担心敲入了 keys*,然后等着定时炸弹的引爆。
学会使用 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,所以此处不会影响性能。
游标在保存为 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 的核心源码通读一遍了,相信很多其间的迷惑也随之解开。
不仅在写代码的时候更自信了,假如日后被面试官问起相关问题,那绝对可以趁机表现一番,想想还有点小激动。
- 上一篇:Redis学习总结
- 下一篇:redis scan命令
相关推荐
- 【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库
-
如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...
- Pure Storage推出统一数据管理云平台及新闪存阵列
-
PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...
- 对Java学习的10条建议(对java课程的建议)
-
不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...
- SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!
-
官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...
- JDK21有没有什么稳定、简单又强势的特性?
-
佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...
- 「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了
-
在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...
- Java面试题及答案最全总结(2025版)
-
大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...
- 数据库日常运维工作内容(数据库日常运维 工作内容)
-
#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...
- 分布式之系统底层原理(上)(底层分布式技术)
-
作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...
- oracle 死锁了怎么办?kill 进程 直接上干货
-
1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...
- SpringBoot 各种分页查询方式详解(全网最全)
-
一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...
- 《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略
-
《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...
- LoadRunner(loadrunner录制不到脚本)
-
一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...
- Redis数据类型介绍(redis 数据类型)
-
介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...
- RMAN备份监控及优化总结(rman备份原理)
-
今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)