当前位置: 首页 > news >正文

Java Redis “底层结构” 面试清单(含超通俗生活案例与深度理解)

一、Redis底层数据结构有哪些?为什么要基于这些结构构建对象系统?

核心考点

准确掌握Redis的6种底层基础结构,理解“底层结构≠直接使用”的设计逻辑——底层结构是“基础组件”,对象系统是“定制化产品”,适配不同业务场景的key-value存储需求。

通俗理解+例子

我们可以把Redis的底层结构和对象系统,类比成“家具工厂的原材料与成品家具”:

• 底层结构就是“原材料”:动态字符串(SDS)像木板、链表像金属连接件、字典像合页、跳跃表像滑轨、整数集合像小五金件、压缩列表像布艺面料——这些原材料单独拿出来没用,比如一块木板不能直接当衣柜用,一根金属连接件也不能直接当椅子用,必须经过组合加工才能变成可用的家具。

• 对象系统就是“成品家具”:Redis不会把木板(SDS)直接给用户用,而是用木板+金属连接件+合页,做成衣柜(对应string类型,存用户昵称、商品标题)、书桌(对应hash类型,存用户信息:姓名、年龄、地址)、抽屉柜(对应zset类型,存商品销量排名)。比如你要存“用户A的昵称是小明”,用的是string对象,而这个string对象的底层,就是用SDS(木板)做的“柜体”;你要存“用户A的收货地址列表”,用的是list对象,底层是快速列表(木板+金属连接件的组合,后续会详细说明)。

• 为什么要这么设计?就像家具工厂不会只卖原材料——用户需要的是能直接用的衣柜、书桌,而不是自己拼原材料。Redis的对象系统就是帮用户“拼好成品”,比如同样用SDS,既能做存短文本的string对象,也能做hash对象里的“字段名”和“字段值”,不用用户自己去组合底层结构,还能根据数据量自动调整(比如hash对象小的时候用压缩列表,数据量大了就切换成字典)。

关键提醒

6种底层结构是Redis所有key-value类型的“基石”,具体对应关系可简单记忆:string底层是SDS;list底层是快速列表(3.2+版本);hash底层是压缩列表(小数据场景)或字典(大数据场景);zset底层是压缩列表(小数据场景)或跳跃表+字典(大数据场景);set底层是整数集合(纯整数小数据场景)或字典(非整数/大数据场景)。

二、Redis的SDS和C语言字符串比,优势在哪?

核心考点

对比C语言字符串的4个核心缺陷,理解SDS针对这些缺陷的优化思路——本质是“用少量额外空间,换效率、安全和功能扩展”,这也是Redis能高效处理字符串的关键。

通俗理解+例子

要搞懂SDS的优势,先得知道C语言字符串的“坑”,我们用“学生记笔记”这个日常场景来类比:

1. 获取长度:从“翻遍笔记本”到“看封面标签”

• C语言字符串像“没写总页数的笔记本”:比如你记了一学期的课堂笔记,想知道总共写了多少页,只能从第一页开始翻,一页页数到最后一页(遍历整个字符串,时间复杂度O(n))。如果笔记有1000页,你得翻1000次才能知道页数,不仅费时间,还容易数错。

• SDS像“写了总页数的笔记本”:笔记本封面直接贴了标签“共328页”,不管你想知道页数还是想翻到最后一页,看一眼标签就够了(直接读取len属性,时间复杂度O(1))。哪怕笔记有10000页,也不用翻一页纸,效率直接拉满,还不会出错。

2. 缓冲区溢出:从“水杯漏水”到“自动扩容保温杯”

• C语言字符串像“没标最大容量的敞口水杯”:你不知道这个杯子能装多少水,只知道现在装了半杯。如果朋友帮你倒开水,没注意容量,倒多了就会洒出来(缓冲区溢出)——比如你定义了一个能存5个字符的C字符串“abcde”(实际占6个字节,最后一个是结束符\0),想把它改成“abcdefg”,C语言不会检查容量是否足够,直接往后面写,就会覆盖后面的内存数据,导致程序崩溃,就像水洒出来弄湿了桌面。

• SDS像“会自动变大的保温杯”:杯子上标了“当前装300ml,最大能装500ml”,当你倒开水到450ml时,杯子会自动膨胀,把最大容量改成1000ml(这就是SDS的空间预分配机制),永远不会洒出来。比如你要给SDS追加内容,SDS会先检查当前剩余空间(alloc - len)够不够,如果不够,先自动扩容到足够的大小,再追加内容,完全杜绝溢出问题,就像保温杯总能装下你倒的水。

3. 内存分配次数:从“每次换衣服都买新的”到“一次多买两件+旧的先留着”

• C语言字符串改长度,像“每次穿衣服都要重新买一件”:比如你有一件M码的衣服(对应C字符串长度5),长胖了要穿L码(长度8),得重新买一件L码的(内存重新分配);后来瘦了又要穿M码(长度5),又得重新买一件M码的(再次内存分配)——每次改长度都要“买新衣服”,频繁跑服装店(操作系统内存管理),不仅麻烦,还浪费钱(效率低)。

• SDS用“空间预分配+惰性空间释放”,像“聪明的购物方式”:

◦ 空间预分配:你买衣服时,本来穿M码,直接买L码和XL码两件(分配比实际需要更多的空间),下次长胖了直接穿L码,不用再买(下次追加内容时,若剩余空间够,不用重新分配);

◦ 惰性空间释放:你瘦了穿回M码,L码的衣服先不扔(SDS缩短时,不立即释放多出来的空间),下次再长胖还能穿(下次追加内容时,直接用预留的空间)。
这样一来,你不用频繁跑服装店,内存分配次数大大减少,效率自然更高。

4. 二进制安全:从“只能装水的杯子”到“万能收纳箱”

• C语言字符串像“只能装纯净水的杯子”:如果装的是果汁(带果肉),果肉会堵住杯口(C语言会把\0当成字符串结束符,比如二进制数据里的\0会被误认为“字符串结束”,导致数据截断)。比如你想存一张用户头像的二进制数据(里面有很多\0),C语言会只存到第一个\0就停止,后面的头像数据全丢了,相当于果汁只装了半杯,果肉全没了,头像显示不完整。

• SDS像“带密封盖的万能收纳箱”:不管你装水、装果汁、装小石子(对应任意二进制数据,比如图片、音频、视频),都能完整装下,取出来的时候和放进去的一模一样,不会丢任何东西。SDS是按“字节”来存储和读取的,不管里面有没有\0,都按len属性记录的长度来处理,完全实现二进制安全,就像收纳箱能完好保存所有物品。

关键提醒

SDS不仅是Redis字符串类型(string)的底层实现,还是其他所有复合类型(hash、list、zset、set)中“字符串数据”的存储基础——比如hash的“字段名”和“字段值”、list里的每个元素、zset的“成员”,本质都是SDS,可见SDS在Redis中的核心地位。

三、Redis字典是怎么实现的?什么是渐进式Rehash?

核心考点

理解字典“数组+链表”的底层结构(解决哈希冲突的关键),掌握Rehash的触发条件,重点搞懂“渐进式Rehash”如何避免服务阻塞——这是Redis保证高可用性的重要设计之一。

通俗理解+例子

1. 字典的底层实现:小区的“智能快递柜”

我们可以把Redis的字典类比成小区门口的“智能快递柜”,这个场景很贴近生活,容易理解:

• 数组就是快递柜的“格子”:每个格子有一个编号(对应哈希表的索引),比如1号格、2号格、3号格……每个格子最多放一个快递盒(对应哈希表节点),快递盒里装的是“快递单(key)+ 包裹(value)”——key就是用户要查找的“取件码对应的标识”,value就是要存储的数据。

• 链表就是“格子里叠放的快递”:当两个快递的“取件码哈希后”都对应同一个格子(这就是哈希冲突),比如快递A的取件码哈希后是3,快递B的取件码哈希后也是3,这时候不能把两个快递挤在一个格子里,就把它们叠成一摞(形成链表),放在3号格里。取快递时,先找到对应的格子,再顺着叠放的顺序找自己的快递(遍历链表),就像你去快递柜取件,先找到格子,再从叠放的快递里找自己的那一个。

• 哈希计算的过程:就像快递员计算“取件码对应哪个格子”——比如快递柜有16个格子,取件码是“20240520”,用取件码除以16取余数(这就是简单的哈希算法),得到余数3,就把快递放在3号格。Redis里就是用key的哈希值,对哈希表的大小取模,得到对应的数组索引,确定key要存在哪个“格子”里。

2. Rehash:快递柜“扩容/缩容”

当小区里的快递越来越多,16个格子的快递柜全满了(对应哈希表负载因子超过1,负载因子=哈希表已用节点数/哈希表大小),再放快递就没地方了——这时候物业要换一个更大的快递柜(比如32个格子,对应哈希表扩容),这个“换柜子”的过程就是Rehash:

• 第一步:新建一个更大的快递柜(新哈希表ht[1]),比如原来16格,新的32格;如果快递很少,比如32格的柜子只放了3个快递(负载因子低于0.1),就换一个小柜子(16格,对应哈希表缩容)。

• 第二步:把旧柜子(旧哈希表ht[0])里的所有快递,全部搬到新柜子里——搬的时候要重新算每个快递的格子(用新哈希表大小取模),比如原来3号格的快递,可能会搬到6号格,因为新柜子的大小变了,取模结果也会变。

• 第三步:所有快递搬完后,删掉旧柜子,让新柜子(ht[1])变成“默认快递柜”,原来的ht[1]变成新的ht[0],等待下一次Rehash。

3. 渐进式Rehash:“不闭馆的快递柜搬迁”

如果直接一次性把旧柜子的所有快递搬到新柜子,会出现什么问题?比如旧柜子里有10万个快递,搬完需要10分钟——这10分钟里,快递柜不能用(Redis线程阻塞),业主取不了快递,快递员也放不了快递,线上服务直接瘫痪,就像快递柜贴了“暂停使用”的通知,大家只能等着。

Redis的“渐进式Rehash”就是为了解决这个问题,它像“不闭馆的搬迁”,完全不影响用户使用:

• 搬迁不一次性完成,而是“见缝插针”:每次有业主取快递(Redis执行查询操作)、快递员放快递(Redis执行插入操作)时,顺便把旧柜子里的1-2个快递搬到新柜子里。比如业主小明取3号格的快递时,物业顺便把3号格里的另一个快递搬到新柜子的6号格;快递员放新快递时,也会顺手搬一个旧快递,这样搬迁就在“空闲时间”悄悄进行。

• 新旧柜子同时可用:搬迁期间,新快递会直接放进新柜子(ht[1]),取快递时先查新柜子,新柜子没有再查旧柜子(ht[0])——业主完全感觉不到搬迁,正常取放快递,就像快递柜一直正常运行,没人知道后台在换柜子。

• 搬迁结束:当旧柜子里的快递全搬完了,物业就删掉旧柜子,搬迁正式结束,整个过程没有影响过业主的使用。

这样一来,搬迁过程不会阻塞快递柜的使用(Redis服务),既完成了扩容/缩容,又保证了线上服务的可用性,这就是渐进式Rehash的巧妙之处。

关键提醒

字典是Redis的“核心数据结构”:除了hash类型的底层实现,Redis的全局key-value存储(所有的key和对应的value)、带过期时间的key存储(过期字典)、集群模式下的节点映射,用的都是字典结构,可见字典在Redis中的重要性。

四、跳跃表是怎么实现的?Redis为什么用跳跃表而不用红黑树?

核心考点

掌握跳跃表“分层加速查找”的核心原理,理解Redis选择跳跃表的两个关键原因——性能适配高并发、实现难度低,这也是面试中常被追问的点。

通俗理解+例子

1. 跳跃表的实现:城市的“分层地铁线路”

我们先想一个问题:如果有一个有序链表,里面存了1000个数字(1→2→3→…→1000),要找“500”这个数字,得从1开始一个个往后数,要数500次(时间复杂度O(n)),特别慢,就像坐公交一站站停,到目的地要很久。

跳跃表的出现,就是为了“少停几站”——它像城市里的“分层地铁线路”,普通链表是“只停每一站的普通线”,跳跃表多了“快线”和“直达线”,通过“跳着走”减少查找次数,就像坐地铁选快线,比普通线快很多。

• 先看跳跃表的结构组成(对应地铁线路):

◦ 节点:就是地铁的“站点”,每个站点存3个核心信息——“分值(站号,比如1、5、10)”、“成员(站名,比如火车站、市中心、汽车站)”、“多层前进指针(对应不同线路的轨道)”。每个节点就像一个地铁站,有自己的编号和名称,还有不同线路的轨道连接其他站点。

◦ 层:就是地铁的“线路”,比如1层是普通线、2层是快线、3层是直达线(Redis里层的数量是1-32之间的随机数,像不是所有站点都有快线和直达线,比如郊区的小站可能只有普通线)。

◦ 前进指针:就是“轨道”,比如1层的前进指针连接相邻的站点(1→2→3→…),2层的前进指针连接间隔几个站的站点(1→5→10→…),3层的前进指针连接间隔更远的站点(1→10→20→…)。轨道的作用是让地铁能从一个站到另一个站,前进指针的作用是让跳跃表能从一个节点找到下一个节点。

◦ 跨度:就是“两个站点之间的站数”,比如1层1→2的跨度是1,2层1→5的跨度是4,3层1→10的跨度是9——跨度用来算“站点的排名”,比如从1站出发,走3层到10站,跨度累计9,说明10站是第10名(1+9=10),就像你从火车站坐直达线到汽车站,中间隔了9站,所以汽车站是第10个站。

• 再看查找过程(找“10”站):

1. 从“起点站”(表头)的最高层(3层)开始,沿着3层的轨道走,发现下一站是10站(正好是目标),直接到10站,就像坐直达线一步到位。

2. 如果找的是“8”站:从3层出发,下一站是10站(比8大,不能往前走),就降到2层;2层的下一站是5站(比8小,可以往前走),走到5站;再从5站的2层出发,下一站是10站(比8大,不能往前走),降到1层;1层从5站走到6→7→8站,找到目标。整个过程只走了5步(3层1步→2层1步→1层3步),比普通链表的8步快多了,时间复杂度降到了O(logn),就像坐快线转普通线,比全程坐普通线快很多。

• 节点的“层数”怎么定?Redis用“幂次定律”随机生成——比如50%的节点是1层,25%的节点是2层,12.5%的节点是3层,以此类推,最高32层。就像地铁里,一半的站点只有普通线,四分之一的站点有快线,八分之一的站点有直达线,这样既能保证查找速度,又不会浪费太多“轨道”(内存),避免出现“很多轨道但没几趟车”的浪费情况。

2. Redis为什么不用红黑树?——“立交桥”和“分层地铁”的选择

红黑树也是一种有序数据结构,查找、插入、删除的时间复杂度也是O(logn),和跳跃表差不多,但Redis最终选了跳跃表,核心原因有两个:

• 第一:高并发下的性能更稳定。红黑树像“复杂的立交桥”,插入或删除节点时,可能需要“调整整个桥的车道”(比如左旋、右旋、变色,维护红黑树的平衡),这个过程会涉及多个节点,在高并发场景下,容易出现“服务卡顿”,就像立交桥维修时要封多条车道,导致堵车。而跳跃表像“分层地铁”,插入或删除一个站点(节点)时,只需要修改该站点周围的“轨道”(前进指针),比如在5站和10站之间加一个8站,只需要改5站2层的指针指向8站,8站2层的指针指向10站,不会影响其他站点,性能更稳定,就像地铁加一站只需要接两段轨道,不影响其他线路。

• 第二:实现难度低,维护成本小。红黑树的逻辑非常复杂,比如要判断节点的颜色、父节点的颜色、叔叔节点的颜色,还要处理各种边界情况(比如根节点、叶子节点),代码量很大,容易出bug,就像设计立交桥需要考虑很多复杂的交通流向,稍微错一点就会堵车。而跳跃表的逻辑很直观——就是“分层链表+随机层数”,查找、插入、删除的逻辑都很简单,代码量少,后续维护起来也方便,就像设计地铁线路,只要确定站点和线路,后续调整也容易。Redis的开发者更倾向于“简单且高效”的实现,所以选了跳跃表。

关键提醒

跳跃表在Redis里主要用于两个场景:一是有序集合(zset)的底层实现(当zset的元素多或元素大时,用跳跃表+字典的组合,字典存“成员→分值”的映射,方便快速通过成员找分值;跳跃表存“分值→成员”的有序结构,方便排序和范围查找);二是集群模式下的“节点槽位映射”(用跳跃表管理节点的槽位范围,快速查找某个槽位属于哪个节点)。

五、压缩列表是什么?它的设计目的是什么?

核心考点

理解压缩列表“连续内存+紧凑编码”的结构特点,明确其“节约内存”的核心设计目的,以及适用场景(小数据量、小元素),这是Redis优化内存占用的重要手段。

通俗理解+例子

1. 压缩列表:“一体成型的小零件收纳盒”

我们先想一个问题:如果要存10个小螺丝(对应Redis里的小整数或短字符串),用普通的链表(每个节点有prev、next指针)会怎么样?每个节点的指针就要占16字节(64位系统),10个节点就是160字节,而螺丝本身可能只占10字节——指针占的空间比数据还多,太浪费了,就像用很大的盒子装很小的螺丝,盒子占的地方比螺丝还大。

压缩列表就是为了解决“小数据浪费内存”的问题,它像“一体成型的小零件收纳盒”:

• 普通链表像“用绳子串起来的10个小纸盒”:每个纸盒装一个螺丝,纸盒之间用绳子(指针)连接,绳子占的地方比纸盒还大,还容易有缝隙(内存碎片),就像10个小纸盒散在桌子上,用绳子串起来后,绳子和缝隙占了很多空间。

• 压缩列表像“一个长方形的铁盒,里面分成10个小格子”:铁盒是“连续的内存块”,没有缝隙,每个小格子紧挨着,格子里装螺丝(元素),不用绳子连接——既省空间,又能快速找到每个格子(因为内存连续,能通过计算偏移量定位),就像收纳盒里的格子紧挨着,没有浪费空间,还能一眼找到要的螺丝。

2. 压缩列表的组成:收纳盒的“标签和结构”

一个压缩列表就像一个标注清晰的收纳盒,从左到右分为5个部分,每个部分都有明确的作用,就像收纳盒上的各种标签和结构:

• zlbyttes:收纳盒的“总长度标签”——比如铁盒的总长度是50毫米,记录这个值是为了快速知道“整个收纳盒占多少空间”,销毁压缩列表时能直接释放对应大小的内存,不用一点点算,就像收纳盒的包装上写着“尺寸50×10×5mm”,一眼知道它占多大地方。

• zltail:收纳盒的“尾端偏移标签”——比如最后一个格子(尾节点)距离铁盒开口(压缩列表起始地址)的距离是45毫米,想找最后一个格子时,不用从第一个格子一个个往后数,直接用“起始地址+45毫米”就能定位到,时间复杂度O(1),就像收纳盒上标了“最后一格在距离开口45mm处”,不用翻遍所有格子。

• zllen:收纳盒的“格子数量标签”——比如铁盒里有10个小格子(10个节点),直接记录这个数,不用数格子。但要注意:如果格子数量超过65535,这个标签会记为65535,实际数量需要遍历所有格子才能知道(不过压缩列表本来就用于小数据,很少会超过这个数),就像收纳盒标签写着“10格”,不用自己数。

• entryX:收纳盒的“小格子”——每个格子存一个具体的元素,比如一个小螺丝(整数123)、一个小垫片(短字符串“abc”)。每个格子的编码很紧凑,会根据元素的类型(整数/字符串)和大小,用最短的字节数存储,比如存整数123,只用1字节,而不是4字节,就像小格子会根据零件大小调整,不会浪费空间。

• zlend:收纳盒的“底部标记”——比如铁盒的底部贴了一张“end”的贴纸,告诉使用者“到这里就结束了,后面没有格子了”,对应的二进制是“0xff”(255),就像收纳盒的底部有个“结束”标识,不会找过界。

3. 设计目的:“能省一点是一点”——Redis的内存优化思路

Redis是内存数据库,内存成本很高,所以对“小数据”的存储特别“抠”——能少用1字节就少用1字节。压缩列表的设计目的就是“极致节约内存”,主要适用于两种场景:

• 场景1:小数据量的集合类型。比如hash类型存用户的“收货地址”(字段是“addr1”,值是“北京市朝阳区”,短字符串),当字段数少(默认少于512个,可通过hash-max-ziplist-entries配置)、字段和值都小时(默认少于64字节,可通过hash-max-ziplist-value配置),底层用压缩列表,而不是字典(字典的数组和链表会浪费内存),就像用小收纳盒装少量小零件,不用大箱子。

• 场景2:小元素的有序集合。比如zset类型存“用户的签到天数排名”(成员是用户名,短字符串;分值是签到天数,小整数),当元素数少(默认少于128个,可通过zset-max-ziplist-entries配置)、成员和分值都小时(默认少于64字节,可通过zset-max-ziplist-value配置),底层用压缩列表,而不是跳跃表(跳跃表的多层指针会浪费内存)。

但压缩列表也有缺点:当元素变大或变多时,插入、删除元素会很麻烦——因为内存是连续的,插入一个元素需要“挪动后面所有元素的位置”,像在收纳盒的中间加一个格子,要把后面的格子都往后推,时间复杂度O(n)。所以当数据量超过阈值时,Redis会自动把压缩列表转换成其他结构,比如hash转成字典,zset转成跳跃表+字典,就像小收纳盒装不下了,换成大箱子。

关键提醒

压缩列表是Redis“小数据优化”的典型结构,但在3.2+版本后,list类型已经不用压缩列表了(改用快速列表),但hash、zset、set(纯整数小数据场景)仍然会用压缩列表作为底层结构,直到数据量超过配置的阈值,这是Redis根据数据大小动态调整结构的体现。

六、快速列表(quicklist)是什么?它解决了什么问题?

核心考点

理解快速列表“链表+压缩列表”的混合结构,掌握它对早期list类型(链表+压缩列表)的优化点——兼顾“内存紧凑”和“操作高效”,这是Redis优化list类型性能的关键改进。

通俗理解+例子

1. 先回顾:早期Redis的list类型“痛点”

在Redis 3.2版本之前,list类型的底层实现是“二选一”,就像有两种收纳方式,但各有缺点:

• 当元素少且小时(比如少于512个元素,每个元素少于64字节),用压缩列表(像“一体收纳盒”)——优点是省内存,缺点是元素多了之后,插入删除慢(要挪动元素),就像收纳盒里的零件多了,中间加一个零件要把后面的都往后推,很麻烦。

• 当元素多或大时,用双向链表(像“绳子串多个纸盒”)——优点是插入删除快(改指针就行),缺点是浪费内存:每个节点的prev和next指针各占8字节(64位系统),1000个节点就是16000字节,还会产生内存碎片(每个节点的内存是单独分配的,像散落在桌子上的纸盒,中间有空隙),就像用绳子串1000个纸盒,绳子和空隙占的地方比纸盒里的东西还多。

比如你用list存“用户的聊天记录”:

• 刚开始只有10条记录(短字符串),用压缩列表,占100字节,很省空间,就像用小收纳盒装10条记录。

• 后来记录多到1000条,自动转成双向链表,光指针就占16000字节,比数据本身还大,太浪费,就像用绳子串1000个纸盒,绳子占了很多空间。

• 而且链表的节点内存是分散的,像1000个纸盒散在桌子上,操作系统管理这些内存时效率低(内存碎片多),就像桌子上东西乱,整理起来麻烦。

2. 快速列表:“串起来的小收纳盒”——兼顾省空间和高效率

快速列表的设计思路很简单:“把压缩列表切成小块,用链表串起来”——就像把一个大收纳盒,分成10个小收纳盒,每个小收纳盒里装100条聊天记录,再用绳子把10个小收纳盒串起来。这样既保留了压缩列表的“省空间”,又有链表的“高效率”,完美结合了两者的优点。

具体来说,快速列表的结构有两个核心部分:

• 快速列表节点(quicklistNode):就是“小收纳盒”,每个节点里存的是一个压缩列表(ziplist),比如每个压缩列表里装50条聊天记录(节点大小可配置,默认是8192字节)。每个快速列表节点有prev和next指针,用来和其他节点串成链表——但因为每个节点里装了50条记录,所以1000条记录只需要20个节点,指针只占20×16=320字节,比原来的16000字节节省了很多,就像10个小收纳盒串起来,绳子占的地方比1000个纸盒串起来少很多。

• 快速列表(quicklist):就是“串起来的小收纳盒”,里面存的是快速列表节点的表头和表尾指针,以及节点总数、元素总数等信息——比如记录“总共有20个小收纳盒,1000条聊天记录”,方便快速获取整体信息,就像记录收纳盒的总数和里面的零件数,不用一个个数。

3. 解决了早期list的3个核心问题

快速列表通过“链表+压缩列表”的混合结构,完美解决了早期list的痛点,就像用“串起来的小收纳盒”解决了“大收纳盒难调整”和“散纸盒浪费空间”的问题:

• 问题1:内存浪费严重→解决。早期链表1000个节点占16000字节指针,快速列表20个节点只占320字节指针,还因为每个节点是压缩列表,内部元素紧凑存储,比链表的分散节点省更多内存——相当于把1000个散纸盒,装进20个小收纳盒再串起来,桌子上的空隙少了,内存碎片也少了,空间利用率大大提高。

• 问题2:元素多了之后插入删除慢→解决。早期压缩列表1000个元素,中间插入一条要挪动999个元素;快速列表里,每个小收纳盒只装50个元素,中间插入一条最多挪动49个元素,效率提升20倍。如果要在两个小收纳盒之间插入元素,直接加一个新的小收纳盒串进去,不用动其他盒子,和链表一样快,就像在两个小收纳盒之间加一个新的,不用动里面的零件。

• 问题3:内存管理效率低→解决。早期链表的节点内存分散,操作系统要管理1000个独立的内存块;快速列表只需要管理20个内存块(每个节点一个压缩列表),内存块数量减少50倍,操作系统的内存管理效率大大提升,就像桌子上只有20个收纳盒,比1000个纸盒好整理多了。

4. 快速列表的“细节优化”——更智能的小收纳盒

为了更省内存,快速列表还做了两个细节优化,让“小收纳盒”更智能:

• 节点大小自适应:每个小收纳盒(quicklistNode)里的压缩列表,大小不是固定的——如果元素是短字符串(比如聊天记录里的“你好”),一个压缩列表能装100条;如果元素是长一点的字符串(比如“今天去吃了火锅,味道很好”),一个压缩列表可能只装20条,避免单个节点太大,影响插入删除效率,就像根据零件大小调整收纳盒格子的大小。

• 空节点自动删除:如果一个小收纳盒里的元素全被删光了(比如聊天记录全删了),快速列表会自动把这个空节点删掉,不会留着空盒子占内存——就像把空的收纳盒扔掉,只留装了东西的盒子,不浪费空间。

关键提醒

Redis 3.2版本后,快速列表完全替代了“链表+压缩列表”的组合,成为list类型的唯一底层实现。不管list里的元素多还是少、大还是小,都用快速列表存储——你在Redis里用LPUSH、LPOP、LRANGE等命令操作list时,底层都是在操作快速列表,这是Redis对list类型的重要优化,让list的内存占用和操作效率都有了很大提升。

七、1亿个key中,如何高效找出10万个固定前缀的key?

核心考点

明确keys指令的“阻塞风险”,掌握scan指令的“无阻塞+渐进式遍历”特性,理解高并发场景下的正确选择逻辑——这是线上Redis运维的常见问题,也是面试高频考点。

通俗理解+例子

先假设一个场景:你的Redis里存了1亿个key,都是用户相关的,比如“user:10001”“user:10002”……“order:20001”“order:20002”……现在要找出所有以“user:”为前缀的key,大概有10万个,该怎么找?这时候选对方法很关键,选错了会影响线上服务。

1. 错误选择:用keys指令——“闭馆找书”,阻塞服务

keys指令的工作方式,像“图书馆闭馆找书”,虽然能找到书,但会影响正常使用:

• 图书馆里有1亿本书(对应1亿个key),你要找所有书名带“user:”的书(对应10万个前缀key)。用keys指令,相当于让图书馆闭馆,所有读者不能进(Redis线程阻塞),工作人员全员上阵,从第一本书开始,一本本翻书名(遍历所有key),把带“user:”的书挑出来——这个过程要多久?如果1亿本书,每本翻1毫秒,就要10万秒(约28小时),期间图书馆完全不能用(线上服务瘫痪),读者只能等着,体验极差。

• keys指令的核心问题:一是“全量遍历”,不管key有没有符合前缀,所有key都要过一遍,效率极低,就像找10本特定的书,却要翻遍整个图书馆;二是“阻塞线程”,Redis是单线程模型,keys指令会占用线程直到执行完,期间其他命令(GET、SET、LPUSH等)全被卡住,线上业务直接不可用,就像图书馆闭馆期间,没人能借书还书。

所以,线上环境绝对不能用keys指令,哪怕是找100个key,只要总key数多,就会阻塞服务,造成严重影响。

2. 正确选择:用scan指令——“营业中找书”,不阻塞服务

scan指令的工作方式,像“图书馆正常营业时找书”,既不影响读者,又能慢慢找完:

• 图书馆不闭馆,读者正常借书还书(Redis正常处理其他命令)。工作人员每次只找一个“区域”的书(比如每次遍历100个key),找完这个区域,把找到的带“user:”的书记下来,然后休息一下(返回结果给客户端);下次读者借书时,再找下一个区域的书——分多次找,直到把所有区域都找完,就像工作人员利用空闲时间找书,不影响读者。

• 具体来说,scan指令有3个核心特点,这些特点让它适合线上使用:

1. 渐进式遍历:不一次性遍历所有key,而是每次遍历“一部分key”(数量可通过COUNT参数控制,默认10),分多次完成全量遍历。比如1亿个key,每次遍历1000个,分1000次遍历,每次耗时10毫秒,总耗时10秒,期间不影响读者(线上服务),就像分1000次找书,每次找1000本,不会占用太多时间。

2. 无阻塞:scan指令是“非阻塞”的,每次遍历一部分key后,就把线程还给Redis,让Redis处理其他命令;下次再继续遍历,不会占用线程不放——相当于工作人员找完一个区域的书,就去帮读者借书,下次有空再找下一个区域,不会影响读者借书。

3. 可能重复:因为scan是基于“游标(cursor)”遍历的(每次返回下一个游标的位置),在遍历过程中,如果有key被插入或删除,可能会导致某个key被重复遍历到——比如工作人员刚找完A区域的书,又有一本“user:30000”的书被放到A区域,下次遍历A区域时,会再找到这本书。不过没关系,客户端只需要把拿到的key做一次去重(比如用Set集合存key,自动去重),就能得到唯一的10万个key,就像找到重复的书,去掉一本就行,不影响结果。

3. scan指令的使用细节——“怎么找更高效”

用scan指令找前缀key时,还要注意两个细节,能让找书的效率更高:

• 配合MATCH参数:指定前缀模式,比如执行“scan 0 MATCH user:* COUNT 1000”——意思是“从游标0开始,找前缀是user:的key,每次遍历1000个key”。这样scan只会挑出带前缀的key,不用把所有key都返回给客户端,减少网络传输量,就像工作人员只找带“user:”的书,不用把所有书都记下来。

• 合理设置COUNT参数:COUNT参数不是“返回的key数量”,而是“每次遍历的key数量”。如果设置COUNT=10,每次可能只返回1-2个带前缀的key,要分10万次才能找完;设置COUNT=1000,每次可能返回100个带前缀的key,分1000次就能找完——根据总key数和前缀key的比例,合理设置COUNT(比如1000-10000),能大幅提高效率,就像每次找1000本比每次找10本快很多。

4. 两者对比:为什么scan是线上首选?

从遍历方式来看,keys指令是全量遍历,不管key是否符合前缀要求,都会逐个检查所有key;而scan指令是渐进式遍历,每次只处理一部分key,分多次完成整个遍历过程。从是否阻塞服务来看,keys指令会完全阻塞Redis线程,期间所有其他命令都无法执行,线上业务会瘫痪;scan指令则是非阻塞的,每次处理完一部分key就释放线程,Redis能正常处理其他业务命令,不影响线上服务。从效率来看,处理1亿个key中的10万个前缀key时,keys指令可能需要几十小时,效率极低;scan指令只需要十几秒,效率适中。从结果重复问题来看,keys指令不会有重复的key,因为是一次性全量遍历;scan指令可能会有重复,但客户端简单去重就能解决。从线上可用性来看,keys指令完全不可用,会导致服务阻塞;scan指令完全可用,不影响业务,所以scan指令是线上环境的唯一正确选择。

关键提醒

除了scan,Redis还有针对特定类型的遍历指令,比如hscan(遍历hash的field)、sscan(遍历set的成员)、zscan(遍历zset的成员)——它们的原理和scan一样,都是渐进式无阻塞遍历,适合在高并发场景下使用。比如要遍历一个大hash的所有field,用hscan比hkeys(类似keys,全量遍历阻塞)更安全,不会影响Redis的正常服务。

 

http://www.dtcms.com/a/482670.html

相关文章:

  • Windows10停服!7-Zip被爆组合漏洞|附安全指南
  • 从 0 到 1 搭建完整 Python 语言 Web UI自动化测试学习系列 17--测试框架Pytest基础 1--介绍使用
  • 太原市微网站建设上海网站建设服务电话
  • QT6(鼠标键盘事件)
  • Mac应用快速启动器Alfred 5 Powerpack for Mac
  • 【Linux】——基础指令(下)
  • 做网站的域名怎么申请南宁网站建设策划外包
  • 云南企业建站网站项目怎么做
  • vue钩子函数调用问题
  • 【SpringCloud】Sentinel
  • 建设手机网站做网站有名的公司有哪些
  • JavaWeb流式传输速查宝典
  • 【hive】一种高效增量表的实现
  • AWS同一账号下创建自定义VPC并配置不同区域的对等链接
  • 企业营销网站建设公司淘宝客 网站备案
  • 软件工程的知识领域
  • Unity进阶--C#相关
  • 网页模板网站cms网站建设免费视频教程
  • 板块运动和地震分类
  • 用OpenCV实现智能图像处理从基础操作到实战应用全解析
  • 大庆门户网站wordpress大学主题3.5
  • C++ - vector
  • 做百度竞价网站搜索不到百度北京总部电话
  • Process Monitor 学习笔记(5.5):保存并打开追踪记录(PML/CSV)与协作分享全攻略
  • 论MyBatis和JPA权威性
  • SAP MM采购订单创建接口分享
  • 基于单片机的简易智能衣架控制系统设计
  • rrk3588 与 NPU 主机下的异构通信:基于 PCIe 的设计与实现
  • 2025年--Lc185--63.不同路径II(动态规划,矩阵)--Java版
  • 跨境电商网站排行榜wordpress数据量大网站访问