(拿铁)- Redis-真实超高频八股速成/中频八股提升/低频八股扩展(持续更新中)
之前看面经分享帖的时候,学到了已经上岸大厂的前辈的做法。在准备暑期实习时,我也效仿着根据以往的真实面经整理八股。从牛客、小破站等各个平台搜集了上千篇真实面经,自己整理得到了面试题,根据题目在面试题中出现的频率以及我自己、交流群、好朋友面试被问到的频率进行了分类整理,得到⭐🌟💡三种级别的。在此,给大家分享一下我自己面试被问到的题目,以及我根据以往面经整理得到的题目。各位uu可在专栏关筑一波:
https://www.nowcoder.com/creation/manager/columnDetail/Mq7Xxv
Top 博主都订阅了,比如“Java 抽象带篮子”(7000+ 粉丝),在这里感谢篮子哥的支持!
所有内容经过科学分类与巧妙标注,针对性强,让你的学习事半功倍:
- ⭐ 必须掌握(必看):时间紧迫时的救命稻草,优先攻克核心要点。(可参考神哥的高频题,但我整理出来的比神哥还会多一些,另外还包括以下内容)
- 🌟 尽量掌握(有时间就看):适合两周以上备考时间的同学稳步提升,冲击大厂的uu们建议看!
- 💡 了解即可(知识拓展):时间充裕时作为补充,拓宽视野,被问到的概率小,但如果能答出来就是加分项
- 🔥 面试真题:根据真实面经整理出来的面试题,有些可能难度很高,可根据自身水平酌情参考。
按照推荐观看顺序 “🔥⭐> 🔥🌟 > > 🔥💡” 有条不紊地学习,让每一分每一秒都用在刀刃上,自此一路畅行。
全面覆盖面试核心知识
面试真题涵盖技术领域的核心考点,从高频热点到冷门难点一网打尽。以下是部分模块概览:
Java基础&集合 :
https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mq7Xxv&uuid=4f5b4cac4b9f4dee8b4b213851c154c5
JVM篇:
https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mq7Xxv&uuid=c87d9ad65eb840728ae63774893bccf5
Java并发编程&JUC:
https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mq7Xxv&uuid=28c748189f6b471f9f4218791778f41c
MySQL:
https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mq7Xxv&uuid=55b03d6d16604319a24395f393d615be
Redis:
https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mq7Xxv&uuid=77bd828f85984c22858c3724eef78723
计网:
https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mq7Xxv&uuid=65e9951c2e754d7086d26b9b46aa4a1a
后续还将持续更新 操作系统、设计模式、场景题、智力题等丰富内容
独特解析:知其然,更知其所以然
我整理的八股面经绝非简单的问答堆砌。
每一道题目都配有深度剖析的思考过程,在你看题之前,便清晰呈现出题意图,让你迅速抓住题目核心,加深对题目的理解与记忆,做到 “知己知彼,百战不殆”。
Java基础&集合举例
MySQL
Redis
JVM
Java并发(JUC)
计算机网络
助力你举一反三,深度梳理知识点之间的内在逻辑联系,真正实现知识的融会贯通,做到知其然更知其所以然。
后续还会分享如何包装项目、leetcode 刷题模版与刷题技巧、各种学习经验以及真实面经等,从多个角度助力牛u提升技术能力和面试水平。
还是那句话:
1、简历上相关技术点对应的面试题一定要准备,因为写在简历上了,面试官就默认你会,答不上来的话就很减分
2、抓大放小,优先重点高频八股,根据自身情况进行准备。
更新日志:
2025.4.15 更新Redis数据结构(Redis常用的五种数据结构要掌握,其中zset的底层编码之一跳表、hash的底层编码之一hashtable问的比较多)
2025.4.18 更新RDB持久化(主要就问bgsave,写时复制要能说出来)AOF持久化(AOF怎么做的、要了解,与RDB的对比,两者的优缺点要能说出来,另外AOF重写也问的蛮多)
2025.4.21 更新 事务(与MySQL事务的区别要了解,Redis的事务并非关系型数据库中的具有ACID特性的事务)& 应用-缓存场景(旁路缓存的读写策略、缓存一致性的解决方案,缓存三剑客的背景与解决方案-这几个都是超级超级重要的面试题)
2025.4.23 更新 应用-分布式锁(怎么实现的-加锁、解锁的要点一定要记得,可以的话结合项目答更好,另外redis实现分布式锁的缺点,比如业务时间太久导致锁过期-redisson看门狗,分布式情况下的问题-redlock要了解)
2025.4.27 更新 高可用(主要考察主从架构的原理、哨兵的作用、切片集群的架构是什么样的,一般不会考太深,但这几种架构要能说出来)
暂时更新完毕。。。
引入
🔥⭐Q: OK,那你能列举一些 Redis 常用的业务场景吗?
思考过程:
这个问题考察对 Redis 应用场景的了解。需要结合 Redis 支持的数据结构,列举一些常见的业务应用。
- 热点数据缓存。
- 限时业务实现(expire 命令)。
- 计数器实现(incrby 命令)。
- 排行榜实现(SortedSet)。
- 分布式锁实现(setnx 命令)。
- 队列机制实现(List 的 push 和 pop 命令)。
回答提问:
好的面试官,Redis 在很多业务场景中都有广泛的应用,我列举一些比较常用的:
- 热点数据缓存: 这是 Redis 最常见的应用场景。由于 Redis 速度快,非常适合存储访问频率很高的数据,比如商品信息、用户信息、配置信息等等。
- 限时类业务的实现: Redis 提供了 expire 命令可以设置 key 的过期时间,这非常适合实现一些限时业务,比如验证码的有效期、优惠券的使用期限、活动倒计时等等。
- 计数器的实现: Redis 的 incrby 等命令可以实现原子性的递增操作,在高并发场景下非常有用,比如可以用来做秒杀活动的计数、限制某个接口的访问频率等等。
- 排行榜的实现: Redis 的有序集合(SortedSet)可以根据分数进行排序,非常适合实现各种排行榜功能,比如热门商品排行榜、用户积分排行榜等等。
- 分布式锁的实现: 可以利用 Redis 的 setnx(set if not exists)命令来实现简单的分布式锁,保证在分布式环境下只有一个客户端能执行特定的操作。
🔥🌟Q: 看你的项目使用了Redis,你觉得使用 Redis 有哪些主要的优势呢?(可以从项目入手,尝试跟面试官聊项目中的使用场景(我们使用了Redis来做缓存.... / 用Redis实现了分布式锁.....),主动去展开话题,如果可以的话再详细说balabala....)
思考过程:
这个问题考察对 Redis 价值的理解。需要从性能、数据结构和功能丰富性等方面进行回答,与上一题略有重复,但侧重点可以放在“好处”上。
- 基于内存,读写速度快。
- 支持多种数据结构,满足不同业务需求。
- 功能丰富,如持久化、复制、集群等。
回答提问:
好的面试官,使用 Redis 主要有以下几个方面的优势:
首先,Redis 是基于内存的 key-value 数据库,这使得它的读写速度非常快,能够有效地提升应用程序的性能。
其次,Redis 支持多种数据结构,不仅仅是简单的键值对,还包括列表、集合、哈希表、有序集合等等,这些丰富的数据结构能够很好地满足各种不同的业务场景需求。
此外,Redis 还提供了很多丰富的功能,比如数据持久化,可以将内存中的数据保存到磁盘上;主从复制可以提高系统的可用性和读性能;集群模式可以实现更大的数据容量和更高的并发能力。
数据结构(Redis常用的五种数据结构要掌握,其中zset的底层编码之一跳表、hash的底层编码之一hashtable问的比较多)
🔥⭐Q: Redis 支持哪些常用的数据类型呢?/Redis常用的数据结构了解哪些呢?
思考过程:
这个问题考察对 Redis 数据类型及其特性的掌握。需要列出五种基本数据类型,并简述它们的特点和常见应用场景。如果了解更新的版本支持的数据类型也可以提及。
- 五种基本数据类型:String, Hash, List, Set, Zset。
- String:字符串,缓存、计数、分布式锁。
- Hash:键值对集合,缓存对象、购物车。
- List:链表,消息队列、最新列表。
- Set:无序集合,去重、标签、社交关系。
- Zset:有序集合,排行榜、带权重的队列。
- 提及后续版本新增的 BitMap, HyperLogLog, GEO, Stream。
回答提问:
好的面试官,Redis 支持多种数据类型,最常用的有以下五种:
- String(字符串): 这是最基本的数据类型,可以存储文本、数字或者二进制数据,单个 value 的最大容量是 512MB。常用于缓存对象、常规计数、实现分布式锁、存储 session 信息等。
- Hash(哈希): 类似于 Map,存储键值对的集合,适合存储对象,可以方便地对对象的属性进行修改,常用于缓存对象、实现购物车等。
- List(列表): 是一个有序的字符串列表,支持在列表的两端进行添加和删除操作,常用于实现消息队列、获取最新的列表数据等。
- Set(集合): 是一个无序且唯一的字符串集合,常用于做一些集合操作,比如求交集、并集、差集,可以应用于点赞、共同关注、抽奖活动等场景。
- Zset(有序集合): 在 Set 的基础上为每个元素关联一个分数,可以根据分数进行排序,常用于实现排行榜、带权重的消息队列等。
随着 Redis 版本的更新,后面还增加了一些新的数据类型,比如 BitMap 用于二值状态统计,HyperLogLog 用于海量数据基数统计,GEO 用于存储地理位置信息,以及 Stream 用于实现更强大的消息队列功能。在面试中,掌握前面五种基本数据类型就足够了。
🔥⭐Q: Redis 的跳表(Skiplist)是一种什么样的结构呢?
思考过程:
这个问题考察对 跳表这种数据结构的理解。需要描述跳表的核心思想、组成部分以及其性能特点。
- 跳表是一种有序数据结构,通过多级索引优化链表性能。
- 核心思想是空间换时间,通过稀疏索引跳过部分元素。
- 平均时间复杂度接近 O(logN),效率媲美平衡树。
- 主要组成部分:跳表头节点、跳表节点(包含数据元素、分值、层数组)。
- 层数组是核心,每层索引指向后继节点,层级越高跨越节点越多。
回答提问:
好的面试官,Redis 的跳表(Skiplist)是一种有序的数据结构,它通过在普通的有序链表之上构建多级索引的方式,来实现对链表的性能优化。
它的核心思想是以空间换时间。通过维护多层级的索引,这些索引相对稀疏地指向底层链表中的节点,使得在查找、插入、删除等操作时,可以跳过一些不必要的节点,从而达到接近 O(logN) 的平均时间复杂度,其效率可以与平衡树(如红黑树)相媲美。
Redis 的跳表主要由以下几个关键部分组成:
- 跳表头节点(header): 它负责维护跳表的全局信息,比如跳表的层级数(level),以及指向每一层索引链表的头节点的指针。
- 跳表节点(node): 这是跳表中真正存储数据的单元。每个跳表节点包含:数据元素(value/member): 实际存储的数据,在 ZSet 中就是 member。分值(score): 用于排序的依据,在 ZSet 中就是 score。层(level)数组: 这是跳表最核心的部分!每个节点都包含一个层数组,数组中的每个元素被称为层或索引层。每个层都包含一个指向后继节点的前向指针(forward)。不同层级的指针跨越的节点数量不同,层级越高,指针跨越的节点就越多。层数组的长度(即包含多少个层)在节点创建时会基于概率随机生成,这决定了该节点在跳表中所处的索引层级。
通过这种多层级的索引结构,跳表可以在大部分情况下实现高效的查找、插入和删除操作。
🔥⭐Q:Redis 的 Hash数据结构了解吗?
思考过程
这个问题考察对 Redis Hash 数据结构的基本概念和特点 的理解。
- 定义:键值对集合,值本身又是一个键值对的集合。
- 存储方式:将多个字段和值存储在同一个键中。
- 适用场景:管理关联数据,存储对象属性。
- 特点:适合小数据,使用哈希表实现,内存高效,支持快速字段操作。
回答提问
面试官你好,Redis 的 Hash 是一种 键值对集合,但与普通的键值对不同的是,Hash 中 每个键对应的值本身又是一个包含多个字段和值的键值对集合。你可以把它想象成一个存储对象的容器,其中外层的键是对象的标识,而内层的字段和值则代表了对象的属性。
Redis 的 Hash 特点如下:
- 适合存储小数据:Hash 特别适合存储具有多个相关属性的小对象。
- 使用哈希表实现:底层通常使用哈希表(或压缩列表)实现,能够在内存中高效地存储和操作。
- 支持快速的字段操作:Redis 提供了针对 Hash 中字段的快速增、删、改、查操作,例如 HGET、HSET、HDEL 等,这使得它非常适合存储和管理对象的属性。
🔥⭐Q:渐进式扩容了解过吗?/HashTable 是怎么扩容的?
思考过程
这个问题考察对 Redis HashTable 扩容机制 的理解,需要说明触发条件、扩容步骤以及渐进式 rehash 的策略。
- 触发条件:负载因子超过阈值(与是否正在进行持久化操作有关)。
- 扩容步骤: 分配新哈希表(大小为原表 used 的 2 倍的 2 的整数次幂)。设置 rehash 索引为 0。渐进式迁移数据:每次 CRUD 操作迁移一部分数据。完成 rehash:释放旧表,新表替换旧表,重置 rehash 索引。
- 渐进式 rehash 的意义:避免一次性扩容带来的性能卡顿。
- 扩容期间的 CRUD 操作:查询在两个表进行,写入直接写入新表,删除需要在两个表都删除。
回答提问
面试官你好,Redis 的 HashTable 扩容采用了 渐进式 rehash 的策略,这是为了避免一次性扩容带来的性能卡顿,将扩容的开销平摊到后续的操作中。
当 HashTable 的 负载因子(used / size) 超过一定的阈值时,就会触发扩容。具体来说,触发条件有两个:
- 当服务器当前没有执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 1 时。
- 当哈希表的负载因子大于等于 5 时,即使当前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,也会强制进行扩容。
扩容的过程如下:
- Redis 会为 HashTable 的 1 号哈希表分配空间,这个新哈希表的大小通常是 第一个大于等于 0 号哈希表当前已使用节点(used)的两倍的 2 的整数次幂。
- Redis 会将 rehashidx 索引计数器设置为 0,表示开始进行 rehash 操作。
- 在 rehash 进行期间,每次对 HashTable 执行添加、删除、查找或者更新操作时,除了完成正常的操作外,还会顺带将 rehashidx 位置上的所有键值对从 0 号哈希表迁移到 1 号哈希表,并将 rehashidx 的值加 1。
- 随着时间的推移和操作的不断进行,0 号哈希表中的所有键值对都会被迁移到 1 号哈希表中。
- 当所有的键值对都迁移完毕后,Redis 会释放 0 号哈希表的空间,然后将 1 号哈希表设置为新的 0 号哈希表,并创建一个新的空的 1 号哈希表,最后将 rehashidx 的值设置为 -1,表示 rehash 操作完成。
在渐进式 rehash 期间,对 HashTable 的 CRUD 操作会有些特殊:
- 查询操作:会先在 0 号哈希表中查找,如果没找到,会继续在 1 号哈希表中查找。
- 写入操作:新的键值对会直接写入到 1 号哈希表中,这样可以保证 0 号哈希表中的数据只会减少不会增加,最终会变成空表。
- 删除操作:需要在 0 号和 1 号哈希表中都进行查找和删除。
通过这种渐进式 rehash 的方式,Redis 可以平滑地完成哈希表的扩容操作,避免了因为一次性大量数据迁移而导致服务出现明显的卡顿。
🔥🌟 Q: SDS(Simple Dynamic String)在 Redis 中有什么作用或者优势呢?
思考过程:
这个问题考察对 SDS 数据结构的理解及其相较于 C 字符串的优势。需要从获取长度、二进制安全、防止缓冲区溢出、内存分配策略等方面进行说明。
- SDS 是对 C 字符串的封装。
- 优势一:获取字符串长度的时间复杂度为 O(1)。
- 优势二:二进制安全,可以存储任意格式的数据。
- 优势三:自动进行内存分配,防止缓冲区溢出。
- 优势四:减少内存分配次数,优化性能。
回答提问:
好的面试官,SDS(Simple Dynamic String)是 Redis 中用于表示字符串的基本数据结构,它是对 C 语言原生字符串的一种封装和扩展,解决了 C 字符串的一些问题,并为 Redis 提供了更好的性能和安全性。SDS 主要有以下几个方面的作用和优势:
- 快速获取字符串长度: C 语言的字符串需要通过遍历才能计算出长度(直到遇到空字符 '\0'),时间复杂度是 O(n)。而 SDS 在结构中维护了一个 len 字段,记录了字符串的实际长度,因此获取字符串长度的时间复杂度只需要 O(1)。
- 二进制安全: C 语言的字符串以空字符 '\0' 作为结束标识,这导致它不能存储包含空字符的二进制数据。而 SDS 使用 len 字段来记录字符串的长度,不依赖空字符来判断字符串结束,因此可以存储任意格式的二进制数据,这使得 Redis 可以存储图片、音频等多种类型的数据。
- 防止缓冲区溢出: C 语言的字符串在进行拼接等操作时,如果没有预留足够的内存空间,很容易发生缓冲区溢出。而 SDS 在进行字符串拼接等操作之前,会先检查剩余空间是否足够,如果不够会自动进行内存扩展,避免了缓冲区溢出的风险。
- 减少内存分配次数: SDS 通过预分配内存的方式来减少字符串增长时的内存重新分配次数。它有一个 alloc 字段记录了分配给字符串的总长度,通常会预留一部分冗余空间。当字符串需要增长时,如果预留空间足够,则直接使用;如果不够,才会进行内存重新分配。这种机制可以有效地减少内存分配和释放的次数,提高性能。
总的来说,SDS 通过引入长度记录、预分配空间等机制,弥补了 C 语言字符串的不足,为 Redis 提供了更高效、更安全、更灵活的字符串操作支持。
🔥🌟Q:你提到了压缩列表,那你了解ZIPLIST 是如何压缩数据的吗?
思考过程
这个问题考察对 ZIPLIST 数据结构内部实现和优势 的理解,需要从其内存布局和节点结构入手。
- 内存结构:紧凑的连续内存块。
- 整体布局:<zlbytes><zltail><zllen><entry><entry>...<entry><zlend>
- Entry 结构:<prevlen><encoding><entry-data>
- 压缩原理:通过紧凑的内存布局和对节点长度的编码优化来节省内存。
- 优点: 节省内存:紧凑的内存结构,减少内存碎片。方便一次性分配:整体占用一块连续内存。遍历时局部性更强:数据连续存储,CPU 缓存命中率高。
回答提问
面试官你好,ZIPLIST,顾名思义,是一种为了 节省内存 而设计的压缩列表。它的核心思想是采用 紧凑的内存结构,将所有的数据节点都存储在一块连续的内存空间中,这样可以有效地减少内存碎片。
从结构上来看,一个 ZIPLIST 大致可以分为三个部分:
- 结构头(Header):包含三个字段:<zlbytes>:记录整个 ZIPLIST 的总字节数。<zltail>:记录 ZIPLIST 中最后一个节点的偏移量,方便从后向前遍历。<zllen>:记录 ZIPLIST 中节点的个数。
- 数据部分(Entries):由 N 个数据节点(entry)组成,每个节点都存储着一个列表元素。每个 entry 的结构如下:<prevlen>:记录前一个节点的长度。这个字段的长度可以是 1 字节或 5 字节,取决于前一个节点的长度是否小于 254 字节。<encoding>:记录当前节点数据的编码方式和长度信息。Redis 会根据实际存储的数据类型和大小选择合适的编码方式,例如存储整数或字符串。<entry-data>:存储节点实际的数据内容。
- 结尾标识(End):用一个字节 <zlend>(固定值为 0xFF)标记 ZIPLIST 的结束。
ZIPLIST 的主要优点在于 节省内存。由于它将所有数据都紧凑地排列在一起,避免了像链表那样每个节点都需要额外的指针来指向下一个节点,从而提高了内存的利用率,并减少了内存碎片。此外,由于数据是连续存储的,方便一次性分配内存,并且在 遍历时具有更好的局部性,可以提高 CPU 缓存的命中率,从而提升性能。虽然 ZIPLIST 在查找特定位置的元素时不如数组那样可以直接通过索引访问(因为每个 entry 的长度不固定),但在元素数量较少的情况下,其性能仍然可以接受。
🔥🌟Q:什么是 ZIPLIST 的连锁更新问题?
思考过程
这个问题考察对 ZIPLIST 连锁更新现象及其原因 的理解,需要从 prevlen
字段的编码方式入手。
- 原因:每个节点使用 prevlen 字段记录前一个节点的长度,长度小于 254 字节用 1 字节,否则用 5 字节。
- 表现:更新或插入节点导致某个节点的长度变化,使得后继节点的 prevlen 字段所需的空间也发生变化。
- 连锁反应:如果这种变化持续向后传播,导致后续多个节点的 prevlen 都需要调整大小。
- 结果:每个节点的空间都可能需要重新分配,导致性能下降。
回答提问
面试官你好,ZIPLIST 的连锁更新问题是其设计的一个不足之处。它的发生原因与 ZIPLIST 中每个节点(entry)使用 prevlen
字段记录前一个节点的长度有关。prevlen
字段的长度不是固定的,如果前一个节点的长度小于 254 字节,那么 prevlen
字段只需要 1 个字节来存储;如果前一个节点的长度大于等于 254 字节,那么 prevlen
字段就需要 5 个字节来存储。
连锁更新的 表现 是这样的:假设在一个 ZIPLIST 中,有多个连续的节点的长度都非常接近 254 字节。这时,如果我们更新了其中一个节点(比如使其长度增加),导致它的长度从小于 254 字节变为大于等于 254 字节,那么它的后继节点中记录前一个节点长度的 prevlen
字段就需要从原来的 1 字节扩展到 5 字节。如果这个扩展导致了后继节点自身的长度也发生了变化(比如也超过了某个阈值),那么它的后继节点的 prevlen
字段可能也需要扩展,以此类推,这个变化可能会像多米诺骨牌一样,依次向后传播到多个节点。
结果 就是,在一次插入或更新操作中,可能会导致多个连续节点的内存都需要重新分配,这会带来 额外的内存分配和拷贝开销,从而导致 ZIPLIST 的访问性能下降,尤其是在 ZIPLIST 中包含大量连续的、长度接近阈值的节点时,这个问题会更加明显。
🔥🌟Q: Redis 的跳表是如何进行数据查找的呢?
思考过程:
这个问题考察对 跳表查找算法的理解。需要描述从顶层索引开始,逐层向下查找,通过比较节点值和目标值来缩小查找范围的过程。
- 查找过程类似于二分查找。
- 从顶层索引开始查找。
- 在每一层索引上,通过 forward 指针进行跳跃移动。
- 比较当前索引节点的后继节点值与目标值。
- 后继节点值小于目标值:继续向右跳跃。
- 后继节点值大于等于目标值:降低层级,进行更精细查找。
- 最终在最底层链表找到目标值。
- 核心思想是逐层跳跃,分而治之。
回答提问:
好的面试官,Redis 跳表的数据查找过程与二分查找的思想有些相似,但它是通过多层索引来实现的。具体的查找步骤如下:
- 起始于顶层索引: 查找总是从跳表头节点(header)的最高层索引开始。
- 在每一层索引上进行查找: 从当前层索引的当前节点出发,沿着当前层索引链表向右移动(通过 forward 指针)。
- 值的比较判断: 在移动过程中,将目标值(或者在 ZSet 中是 score 和 member)与当前索引节点的后继节点的值进行比较。比较结果决定下一步的移动方向:后继节点值小于目标值(或者后继节点为空): 说明目标值可能在更右侧的区域,需要继续沿着当前层索引链表向右移动,重复比较过程。后继节点值大于等于目标值: 说明目标值不可能在更右侧的区域,需要降低层级,进行更精细的查找。切换到下一层索引,并从当前索引节点在下一层的对应位置开始查找。
- 直至最底层,完成精确查找: 查找过程中不断进行层级切换和链表移动,逐层缩小查找范围,越来越逼近目标值所在的索引层。最终,会下降到最底层链表(level 1)。在最底层链表中,会逐个检查链表节点,以精确定位目标值。
这种逐层跳跃,分而治之的思想是跳表实现高效查找的关键。通过高层索引快速定位到大致范围,然后再在低层索引中进行精确定位,从而大大减少了需要遍历的节点数量。
🔥🌟Q: Redis 的 ZSet 数据类型在底层有几种编码方式呢?
思考过程:
这个问题考察对 ZSet 底层编码的了解。需要了解 ZSet 可以使用 ziplist 或 skiplist 加字典这两种编码方式,并说明它们的选择条件。
- ZSet 有两种底层编码:ziplist 和 skiplist + 字典。
- ziplist 的使用条件:元素数量小于 128 且所有元素成员长度小于 64 字节。
- 否则使用 skiplist + 字典。
- 选择目的是平衡内存占用和操作效率。
回答提问:
好的面试官,Redis 的 ZSet(有序集合)对象在底层有两种主要的编码方式:ziplist 和 skiplist 结合 字典(hashtable)。
当一个 ZSet 对象满足以下两个条件时,会使用 ziplist 编码:
- 有序集合中保存的元素数量少于 128 个;
- 有序集合中所有元素成员的长度都小于 64 字节。
如果不能同时满足这两个条件,ZSet 对象就会使用 skiplist 和 字典 结合的方式进行编码。
这两种编码方式的选择主要是为了在内存占用和操作效率之间进行平衡。ziplist 是一种非常紧凑的数据结构,可以有效地节省内存,但对于数据量较大或者成员长度较长的情况,其操作效率可能会下降。而 skiplist 则提供了较高的操作效率,字典则可以提供 O(1) 时间复杂度的成员查找。
🔥🌟Q:Redis 的 HashTable 在什么时候会进行扩容?
思考过程
这个问题考察对 Redis HashTable 扩容触发时机 的理解,需要明确负载因子的概念以及与持久化操作的关系。
- 扩容时机: 服务器没有执行 BGSAVE 或 BGREWRITEAOF,且负载因子 >= 1。负载因子 >= 5(强制扩容,即使在持久化)
- 负载因子:used / size。
- 持久化操作的影响:在进行 RDB 快照或 AOF 重写时,会更保守地进行扩容。
回答提问
面试官你好,Redis 的 HashTable 的扩容是由 哈希表的负载因子 来决定的。负载因子等于哈希表中已使用的键值对数量(used
)除以哈希表的大小(size
)。
扩容的时机:当以下两个条件中的任意一个被满足时,HashTable 会自动开始扩容:
- 当服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令(也就是没有进行 RDB 快照或者 AOF 重写操作),并且 哈希表的负载因子大于等于 1。之所以在没有执行持久化操作时采用相对较低的负载因子阈值,是因为扩容本身也需要一定的 CPU 资源,如果在持久化过程中进行扩容可能会对持久化性能产生影响。
- 当哈希表的负载因子大于等于 5。即使此时服务器正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,也会强制进行扩容。这是因为当负载因子达到 5 时,说明哈希表已经非常拥挤,哈希冲突的概率很高,性能会急剧下降,因此需要立即扩容以保证 HashTable 的基本性能。
🔥💡Q: 你了解 Redis 的 String 类型能够存储的最大容量是多少吗?
思考过程:
这个问题考察对 Redis String 类型限制的了解。需要了解具体的数值。
- Redis String 最大容量是 512MB。
- 可以在 Redis 官网或者源码中找到明确说明。
回答提问:
好的面试官,一个 Redis 字符串(String)类型的 value 最大可以存储 512MB 的数据。这个限制在 Redis 的官方文档中有明确说明,并且在 Redis 的源码中也有相关的常量定义。
🔥💡(问的很少,就看到几篇问过,当提高吧)Q:Redis比较新的版本设计实现了 LISTPACK,了解过吗?
思考过程
这个问题考察对 LISTPACK 出现的背景和目的 的理解,需要联系 ZIPLIST 的不足之处进行说明。
- ZIPLIST 的不足:查找效率下降、内存重新分配、连锁更新。
- QUICKLIST 的折衷:使用链表连接 ZIPLIST,减少内存重分配和拷贝,但增加了指针开销。
- LISTPACK 的目标:进一步减少内存开销,彻底避免连锁更新。
回答提问
面试官你好,Redis 设计实现 LISTPACK 主要是为了 解决 ZIPLIST 存在的一些不足之处,并进一步 优化内存使用效率。虽然 QUICKLIST 通过链表连接 ZIPLIST 的方式在一定程度上缓解了 ZIPLIST 的缺点,比如减少了插入时的内存重新分配和拷贝,但也引入了额外的指针开销。
LISTPACK 的设计目标是 在保持 ZIPLIST 紧凑内存布局的优点的前提下,进一步减少内存开销,并且彻底避免 ZIPLIST 的连锁更新问题。LISTPACK 沿用了 ZIPLIST 将每个元素紧挨着放置的策略,但在其内部结构上做了重要的改变,使得它在插入或删除元素时,不会像 ZIPLIST 那样容易触发连锁更新,从而提高了性能和稳定性。
🔥💡(暑期实习盒马问了,感觉问的还挺巧妙的,大家也可以思考一下,结合其他的数据结构)Q:为什么 Redis的很多数据结构都使用了几种不同的编码方式呢?
思考过程
这个问题考察对 Redis 选择多种编码方式的通用设计思想 的理解,需要从平衡内存占用和操作效率的角度进行总结。
- 核心目的:达成内存占用和操作效率之间的平衡。
- 小数据量场景:为降低内存消耗,倾向使用更为节省内存的编码形式。
- 大数据量场景:为保证高效运行,会选择性能更优的编码方式。
- 编码转换:依据数据规模大小以及数据类型特征,实现编码方式的动态切换。
回答提问
面试官你好,像我比较常用的 Redis 的 Set 和 Hash 这两种数据结构都使用了两种不同的编码方式,其核心目的都是为了 在内存占用和操作效率之间找到一个最佳的平衡点,并根据不同的数据规模和类型特点,选择最合适的底层实现。
比如说,在数据量比较小的情况下,Set 使用 INTSET(整数集合),Hash 使用 ZIPLIST(压缩列表),这两种编码方式都非常 节省内存。INTSET 通过有序数组和紧凑的存储来减少内存开销,而 ZIPLIST 则通过连续的内存块来存储数据,避免了额外的指针开销。
然而,当数据量逐渐增大,或者数据的类型不再满足某些条件(例如 Set 中包含非整数元素,Hash 中的键或值过长,或者说对String 字符串进行修改),这些紧凑型编码的性能可能会下降。例如,ZIPLIST 在元素较多时进行插入或查找操作的效率会降低,并且容易发生连锁更新。
这时,Redis 会自动将编码方式 升级 到 HASHTABLE(哈希表)。哈希表具有 O(1) 的平均查找和更新时间复杂度,能够提供更好的性能,尤其是在处理大量数据时。虽然哈希表通常会比 INTSET 或 ZIPLIST 占用更多的内存,但它能够保证在大数据量下的操作效率。
因此,Redis 通过提供多种编码方式,并根据实际的数据情况进行动态切换,可以在不同的场景下选择最合适的实现,从而在内存效率和操作性能之间取得最佳的平衡。这是一种非常灵活和高效的设计策略。
补充:大家可以结合其他数据结构一起说,我上面就是按Set和Hash、String说的,大家可以选几个自己比较熟的说。
字符串(String):
- 小数据量场景:对于较短字符串,Redis 采用embstr编码。该编码将 RedisObject 对象头和 SDS(Simple Dynamic String)在一块连续内存中存储,减少内存碎片以及内存分配开销,极大地节省内存。
- 大数据量场景:当字符串长度超出一定阈值(默认 44 字节),则切换为raw编码。此编码下,RedisObject 对象头与 SDS 分开存储,虽会增加一次内存分配,但对长字符串处理更为高效。
集合(Set):
- 小数据量场景:若集合内元素皆为整数且数量未超一定阈值(默认 512 个),Set 采用INTSET(整数集合)编码。INTSET 借助有序数组和紧凑存储降低内存开销,在小数据量时,查找与插入操作效率颇高。
- 大数据量场景:一旦集合中出现非整数元素,或者元素数量突破阈值,Redis 便会将编码升级为HASHTABLE(哈希表)。哈希表平均查找和更新时间复杂度为 O (1),在处理大量数据时性能卓越。
哈希(Hash):
- 小数据量场景:数据量较小时,Hash 运用ZIPLIST(压缩列表)编码。ZIPLIST 通过连续内存块存储数据,规避额外指针开销,内存利用率高。
- 大数据量场景:当哈希的键或值过长,或者元素数量超过一定阈值(默认键值对数量超 512 个),ZIPLIST 在元素增多时,插入或查找操作效率下降且易发生连锁更新。此时,Redis 自动切换编码为HASHTABLE,以确保大数据量下的操作效率。
列表(List):
- 小数据量场景:早期 Redis 在列表元素较少时用ZIPLIST编码,看重其内存节省特性。Redis 3.2 版本后引入QUICKLIST,它融合了ZIPLIST和双向链表优势。QUICKLIST由多个ZIPLIST构成双向链表,每个ZIPLIST作为一个节点。小数据量下,这种结构兼顾内存开销与操作效率。
- 大数据量场景:随着列表元素增加,QUICKLIST可通过调节ZIPLIST节点大小,平衡内存占用与操作效率。如在插入或删除元素时,QUICKLIST仅需对相应ZIPLIST节点操作,避免ZIPLIST可能出现的连锁更新问题。
有序集合(Sorted Set):
- 小数据量场景:当有序集合元素数量少,且每个元素的成员和分数长度较短时,采用ZIPLIST编码。在ZIPLIST中,元素依分数从小到大排列,节省内存。
- 大数据量场景:若元素数量超过一定阈值(默认 128 个),或者成员和分数长度超过一定限度,会采用SKIPLIST(跳表)和HASHTABLE结合的编码。跳表能在 O (logN) 时间复杂度内完成插入、删除和查找,哈希表可在 O (1) 时间复杂度内实现成员到分数的映射,这种组合在大数据量下操作性能高效。
RDB持久化(主要就问bgsave,写时复制要能说出来)
🔥⭐Q:Redis的RDB了解过吗?
思考过程
这个问题考察对 RDB 持久化机制核心概念的理解。RDB 的本质就是对 Redis 在某个时间点的数据进行快照。
- 核心概念:快照。
- 存储形式:二进制文件。
- 包含内容:某一时刻 Redis 数据库中的所有数据。
回答提问
好的,RDB 的本质可以理解为是 Redis 数据库在某个时间点上的数据的二进制快照。它就像给数据库拍了一张照片,记录了那个时刻所有的数据状态。
🔥⭐Q:你能简单描述一下 RDB 的执行流程是怎样的吗?
思考过程
这个问题考察对 RDB 持久化过程的步骤 的了解,可以选择简洁概括或详细描述的方式。这里选择推荐的简洁方式。
- 核心步骤: Fork 子进程。子进程写入临时 RDB 文件。替换旧的 RDB 文件。
回答提问
好的,RDB 的执行流程可以概括为以下几个核心步骤:
- Redis fork 一个子进程 出来,这个子进程专门负责进行 RDB 持久化。
- 子进程会 将当前 Redis 实例中的数据写入到一个临时的 RDB 文件 中。
- 当子进程完成数据写入后,它会 用这个新的临时 RDB 文件替换掉旧的 RDB 文件。
这样就完成了一次 RDB 持久化过程,并且由于是在子进程中进行,所以不会阻塞 Redis 主进程处理客户端的请求。
🔥💡Q:Redis 中 RDB 是通过哪些方式触发的呢?
思考过程
这个问题考察对 RDB 持久化触发机制 的了解,需要涵盖自动触发和手动触发两种情况,并区分前台阻塞和后台非阻塞方式。
- 手动触发: save 命令:阻塞 Redis 主进程执行持久化。
- bgsave 命令:fork 子进程在后台执行持久化。
- 其他触发场景:主从复制时的全量同步。
回答提问
好的,Redis 中触发 RDB 持久化主要有以下几种方式:
- 通过 Redis 的配置文件进行定时触发。我们可以在 redis.conf 文件中使用 save 指令来配置在满足一定的时间和数据变更条件时,Redis 会自动执行 RDB 持久化。这个过程通常是通过后台子进程(使用 bgsave 命令)来完成的。
- 手动触发,可以通过客户端发送命令。有两种命令可以触发 RDB 持久化: save 命令:这个命令会 阻塞 Redis 的主进程,直到 RDB 文件创建完成。在生产环境中,由于会影响 Redis 的正常服务,通常不建议使用这个命令。bgsave 命令:这个命令会 fork 一个子进程,由子进程在后台负责创建 RDB 文件,主进程可以继续处理客户端的请求。这是更常用的手动触发 RDB 的方式。
- 在主从复制场景中,当从节点首次进行全量同步时,主节点会执行一次 RDB 持久化,并将生成的 RDB 文件发送给从节点。
AOF持久化(AOF怎么做的、要了解,与RDB的对比,两者的优缺点要能说出来,另外AOF重写也问的蛮多)
🔥⭐Q:你能说一下 AOF 的写入流程是怎样的吗?
思考过程
这个问题考察对 AOF 持久化将写命令写入 AOF 文件的详细过程 的理解,需要区分用户态缓冲区、内核缓冲区和磁盘。
- 内存 -> AOF 缓冲区:Redis 执行写命令后,将命令追加到 server.aof_buf。
- AOF 缓冲区 -> 内核缓冲区:通过 write() 系统调用写入内核缓冲区(page cache)。
- 内核缓冲区 -> 磁盘:操作系统根据刷盘策略(如 fsync())将数据写入磁盘。
回答提问
好的,AOF 的写入流程大致是这样的:
当 Redis 执行完一个写操作命令后,它会将这个 写操作命令以日志的形式追加到 AOF 缓冲区(server.aof_buf
) 中,这个缓冲区位于用户空间的内存中。这样做的好处是可以快速响应客户端的写请求,避免因为磁盘 I/O 而阻塞 Redis 主线程。
然后,Redis 会通过 write()
系统调用,将 AOF 缓冲区中的数据写入到 AOF 文件的内核缓冲区(page cache) 中。此时数据并没有真正写入到硬盘,而是先拷贝到了操作系统的内核缓冲区,等待操作系统将数据刷入磁盘。使用 page cache 可以提高文件 I/O 的性能。
最后一步是 将内核缓冲区中的数据持久化到磁盘。这个刷盘的时机取决于 Redis 配置的 AOF 刷盘策略,可以通过 fsync()
等系统调用来强制操作系统将数据写入硬盘。
🔥⭐Q:你能简单描述一下 AOF 重写的流程吗?
思考过程
这个问题考察对 AOF 重写过程的关键步骤 的掌握,需要说明子进程的创建、数据写入和双缓冲机制。
- 触发:文件大小增长到一定程度或手动执行 BGREWRITEAOF。
- 子进程创建:主进程 fork 出子进程。
- 数据写入新文件:子进程读取当前数据库数据快照,以命令格式写入新的 AOF 文件。
- 双缓冲机制:主进程将新的写入命令同时写入 AOF 缓冲区和 AOF 重写缓冲区。
- 增量数据追加:子进程将 AOF 重写缓冲区的数据追加到新的 AOF 文件。
- 文件替换:新的 AOF 文件改名覆盖旧的 AOF 文件。
回答提问
好的,AOF 重写的流程可以概括为“一次拷贝,两处日志”:
当 AOF 文件的大小增长到一定的程度,或者我们手动执行 BGREWRITEAOF
命令时,会触发 AOF 重写。
- 在 AOF 重写触发的那一刻,Redis 的 主进程会 fork 出一个子进程。这个子进程会 读取当前 Redis 数据库中的数据快照(这部分数据是与主线程共享且只读的,未修改的部分),然后将这些数据以 Redis 命令的格式写入到一个 新的 AOF 文件 中。
- 为了保证在重写过程中不会丢失新的写入操作,Redis 使用了 双缓冲机制。在子进程进行重写的同时,主进程会继续处理客户端的请求。对于新接收到的写入命令,主进程会 同时将这些增量数据写入到原来的 AOF 缓冲区 和一个 AOF 重写缓冲区 中。原来的 AOF 缓冲区用于保证在重写过程中如果发生宕机,仍然可以使用旧的 AOF 文件进行恢复。AOF 重写缓冲区则用于记录在重写期间发生的所有新的写入操作。
- 当子进程完成将当前数据库快照写入到新的 AOF 文件后,主进程会将 AOF 重写缓冲区中的所有数据发送给子进程,子进程再将这部分增量数据 追加到新的 AOF 文件的末尾,这样就保证了新的 AOF 文件包含了重写开始之后的所有修改,与当前的数据库状态是一致的。
- 最后,当子进程完成所有数据的写入后,主进程会将新的 AOF 文件的名字进行修改,覆盖现有的旧的 AOF 文件。
需要注意的是,在重写过程中,如果主进程修改了已经存在的 key-value,可能会发生写时复制,但只会复制主进程修改的物理内存数据,没有修改的部分仍然与子进程共享。
🔥🌟Q:RDB 和 AOF 这两种持久化方式的区别说一下吧?
思考过程
这个问题考察对 RDB 和 AOF 核心差异 的理解,需要从文件类型、数据安全性、恢复速度和操作开销等方面进行对比。
- 本质区别:RDB 是快照,AOF 是日志。
- 文件类型:RDB 二进制,AOF 文本。
- 安全性:AOF 更高(取决于策略)。
- 恢复速度:RDB 更快。
- 操作开销:RDB 全量保存较重,AOF 追加写入较轻。
回答提问
好的,RDB 和 AOF 这两种持久化方式的 本质区别 在于:RDB 是通过保。
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
在准备暑期实习时,从等各个平台搜集了上千篇真实面经,自己整理得到了面试题,根据题目在面试题中出现的频率以及我自己、交流群、好朋友面试被问到的频率进行了分类整理,所有内容经过科学分类与巧妙标注,针对性强: 得到⭐🌟💡三种级别的,其中⭐为最高频的题目(类似神哥总结的高频八股),只是我自己整理出来的这部分更多一些,🌟为中高频题目(冲击大厂的uu们建议看)、💡为低频题,可以作为补充,时间充裕再看!