redis 面试题总结

本文深入探讨了Redis中的数据结构,包括SDS字符串的优化、Golang内存对齐原则、链表和压缩列表的设计,以及字典和跳表的实现。Redis通过单线程、内存数据库、非阻塞I/O复用等特性实现了高性能。同时,文章讨论了缓存一致性、过期键删除策略以及分布式锁的实现,如Redis事务和RedLock。

以下所有面试题均由个人总结自网络或者书籍,不保证完全正确

  • redis数据结构之SDS
    redis字符串SDS有一个专门的len成员变量,可以存储包含/0的二进制数据并且可以O(1)的时间获取字符串长度。SDS每次修改时会根据alloc-len(未使用字节的数量)判断内存是否够用,不够时会自动扩容,防止缓冲区溢出(物理地址上的数据被覆盖),扩容的时候会分配额外的空间。惰性空间释放机制则用于优化 SDS 字符串缩短时并不立即使用内存重分配来回收缩短后多出来的空间,而仅仅更新 SDS 的len属性,多出来的空间供将来使用。这些措施就可以有效减少内存分配的次数。此外还有一个flags变量,表示SDS的5种类型。它们的区别就是数据结构里的len和alloc变量的数据类型不同。之所以设计不同类型的结构体是为了灵活保存不同大小字符串,节省内存空间。
    此外redis还设计了专门的编译优化,让编辑器不按照默认的8或者4字节对齐,按照实际字节占用数进行对齐,其实也就是按照一字节进行对齐。一方面出于节省内存的目的,另一方面是为了兼容C语言的库函数,SDS指针是指向buf字符数组,不对齐的话只要往前挪一位buf[-1]就可以得到flags等属性。还有就是跨平台通信的时候由于不同平台对齐的方式可能不同,这样子互相发送的数据会出现对齐错位的情况。采用一字节对齐就可以使跨平台的结构体大小相同,还节省了空间,但是会降低一定的效率。因为cpu一般是一次读取多个字节的,紧凑对齐的话会导致一个基本类型的地址被隔断,cpu需要访问两次,然后对两次结果的高低字节进行拼凑才能得到最终结果,这样就拖慢了速度。

  • Golang 中的内存对齐
    1、成员对齐规则:
    对于每一个基础变量,对应一个unsafe.AlignOf()返回值m,这个变量分配的内存地址必须是m的整数倍,比如假设初始地址为0,一个结构体有两个变量,先分配int32变量占4字节,再分配一个int64变量占8字节,这时候由于int64变量的起始地址是4,不是8的倍数。那么就要进行填充,所以最终这个结构体占据的总字节数是4+4+8=16字节。
    2、整体对齐规则:
    要求结构体的整体内存占用是m的总数倍,假设结构体现在有三个变量,前两个变量和之前一样,现在在后面添加了一个int32变量,它的起始地址是16,可以被4整除,所以总长度为16+4=20。但是由于20不能被最大的m=8整除,所以需要再进行填充,最终结构体的长度为20+4=24。
    例子:如果改变一下顺序,两个int32放前面,int64放后面,按照前面的分析,这个结构体就只需要16字节。如果结构体里面有一个空结构体,它放在前面和中间的位置时是不占内存空间的,但是放到最后面,需要进行内存对齐,它占用的内存大小和前一个变量保持一致。

  • redis链表
    redis中的链表通过结构体list来实现,它是一个双向链表,list里包括表头节点和表尾节点以及链表长度属性len。节点通过void*指针保存,所以可以存储不同类型的值。它的缺点是链表结点内存不连续,没法像连续内存的数组一样很好地利用CPU缓存。而且一个节点就需要分配一个节点结构体,内存开销大。所以在数据量少的时候会采用压缩列表。

  • redis压缩列表
    压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构,列表中可以包含多个节点,每个节点可以保存字节数组或者整数值,每个节点都有一个属性记录前一个节点的长度。

  • redis字典
    redis中所实现的map类型,字典底层也是采用hashtable的结构实现,由哈希表结点作为桶,结点结构中有下个结点的指针从而形成链表,解决冲突的方法也是挂载到冲突结点,负载因子过大或过小时系统也会自动扩容和收缩然后进行渐进式rehash。在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上。这样就巧妙地把一次性大量数据迁移工作的开销,分摊到了多次处理请求的过程中,避免了一次性 rehash 的耗时操作。redis中的数据库就是字典作为底层实现的,对数据库的增删查改等等操作都是用字典API操作,字典本身也可以作为键类型:哈希键。

  • redis跳表
    跳表是一种有序数据结构,通过在每个结点中维持多个指向不同结点的指针以实现快速访问不同结点的目的。跳表结点底层结构中包括一个元素数组、前进后退双向指针、层数,层数就是表示跳表查询过程中跨度的参数。插入跳表结点是会随机生成一个层数值以保证查询效率。跳表支持平均O(logn),最坏O(N)时间复杂度的查找,由于底层数据结点可以顺序访问,功能类似于B+Tree叶节点上的双向指针,所以可以顺序批量处理结点,查询效率也跟平衡树类似。

  • redis整数集合
    整数集合是用于保存整数值的集合抽象数据结构,可以保存不同类型的整数值且保证不出现重复,底层结构比较简单只包括编码格式、长度、底层数组三个字段。比较值得一提的是升级操作,由于集合中可以插入不同整数类型的值,当需要插入更长类型的值时就会触发升级,首先将底层数组扩容到相应的大小再依次将元素转换成新类型,最后插入新元素。整数集合是集合键的底层实现之一。

  • redis为什么快
    1、redis是内存数据库,绝大部分请求是纯粹的内存操作,非常快速,数据的查找和操作类似于map,只需要O(1)的时间复杂度。
    2、采用单线程,避免了多线程频繁切换导致的不必要的上下文切换和竞争条件造成的性能损耗。同时也无需考虑各种锁的问题以及死锁可能带来的性能损耗。据官方解释,redis的瓶颈不在cpu,而在内存或者网络的带宽,综合考虑然后就采用了单线程。这里说的单线程是指处理网络请求时只是用一个线程,redis本身在持久化的时候还是会用到额外的线程的。
    使用多路IO复用模型,非阻塞IO。也就是多个网络连接客户端复用同一个线程。I/O 多路复用模型是利用 select、poll、epoll 函数可以同时监察多个流的 I/O 事件的能力。当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),依次顺序的处理就绪的流,这种做法就避免了大量无用的等待操作
    3、简单快速的数据结构。

  • 为什么不用map做缓存
    因为map是本地缓存,轻量快速但是生命周期短,缓存数据没法持久化。而且在多实例的时候无法保证缓存数据的一致性。

  • 数据库和缓存如何保证一致性
    当两个请求并发更新同一条数据的时候就可能会出现缓存中的数据和数据库中的数据不一致的情况。
    采用先更新缓存再更新数据库或者先更新数据库再更新缓存都会出现不一致问题。采用先删除缓存再更新数据库的策略在并发读写请求的时候也会出现不一致问题,但是可以采用延时双删的策略,也就是先删除缓存,更新数据库,然后睡眠一段时间。保证在这段睡眠的时间里,另一个线程已经完成了从数据库读取数据并并将缺失的缓存写入缓存中。然后原线程睡眠完再执行一次删除缓存操作来保证一致性,但是这个睡眠时间具体多长不好确定,极端情况下还是会出现不一致问题。
    所以采用先更新数据库再删除缓存的操作,这种操作在并发读写的时候也可能出现问题,比如在一次读取数据库数据和写入缓存的间隙中有另一个线程完成了写数据库并删除缓存的操作,这时候原线程再写入缓存就会有不一致问题。但是概率很小,因为数据库读操作一般比写操作快很多。万一发生了就可以给缓存加上过期时间,缓存过期就重新更新缓存。还有一个问题,如果更新数据库以后删除缓存失败,会导致缓存还是旧值。解决方式有两种,可以引入消息队列,把要删除的缓存数据加入消息队列中,由消费者操作数据,进行删除缓存操作。如果删除失败,就再次从消息队列读取,进行重试。如果重试多次还没成功就要报错。删除成功的话就要把数据从消息队列中删除。第二种是订阅mysql的binlog,通过binlog拿到更新数据库后的变更日志得到数据,然后再删除缓存。

  • 缓存穿透和布隆过滤器
    缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落在数据库上,造成数据库短时间内承受大量的请求而崩掉。可以通过布隆过滤器来解决这个问题。
    布隆过滤器就是将所有可能存在的数据哈希到一个足够大的bitmap,不存在的数据会被这个bitmap给拦截掉,避免对底层存储系统的查询压力。它的本质就是一种算法,通过一个二进制数组和hash算法组成。将所有数据映射到的比特位改为1。之后遇到需要过滤的数据的时候通过hash算法映射,如果所有对应比特位都为1那么就可以判断当前数据可能存在于已有数据集合。如果存在0那么一定不在已有数据集合中。hash的时候可能会产生误判,因为为1不一定就属于原数据集合,可能只是散列值刚好相同,这时候可以采用多个hash算法来减少误判率,只有当多个hash算法判断都符合的时候才是已有数据集中的数据。当然,布隆过滤器主要是用来过滤一定不存在的数据来避免缓存穿透。它的缺点是不支持删除数据,因为删除以后,对应的比特位要修改为1,会影响到其他映射到当前比特位的元素。

  • 缓存击穿:
    指缓存中没有数据而数据库中有数据。主要出现在数据初始化以及缓存过期的情况下。针对的是热点缓存。它的问题在于缓存重新写入需要一定的时间,如果是在高并发的场景下,过多的请求会落到db上,造成很大的压力。解决方案包括使热点缓存永不过期,提高缓存命中率。这种情况要注意缓存永不过期也就没法更新了,所以要设置一个逻辑上的过期时间,然后另起一个线程,定期重建这些缓存。此外还可以在db写入缓存的时候防止并发,比如在写缓存的时候加一个并发锁,只有一个线程可以请求。

  • 缓存雪崩:
    缓存大面积过期导致请求都落到db上。
    解决方案:
    1.把缓存的失效时间分散开,例如可以在缓存原来的统一失效时间的基础上增加一个随机值。
    2.设置热点缓存永不过期,同缓存击穿。

  • redis过期键删除策略
    惰性删除:只有当访问到一个过期key的时候才将它删除掉,对cpu友好,但是可能会导致大量过期key存在于内存中。
    定期删除:每隔一段时间扫描一定数量的数据库的expire字典中一定数量的key。可以通过扫描的时间间隔和扫描的限定耗时来使得cpu和内存资源达到最优的平衡效果。
    redis通过这两种方式结合来进行过期键删除。

  • redis事务
    redis开启事务multi,之后除了multi,watch,exec,discard这四个命令会立即执行,其它命令并不马上执行,而是会加入commands队列中。如果没有语法错误,命令成功加入队列,redis会向客户端返回一个QUEUE回复,否则会返回错误。redis事务不支持回滚,这个过程只保证不出现语法错误,出现逻辑错误不会报错,会继续执行。命令全部入队以后通过命令exec执行事务。discard命令可以清空事务队列,并放弃执行事务,客户端会从事务状态退出。watch命令会监控一个或多个key的状态,如果事务执行过程中key发生了改动,那么事务就不会执行,监控一直持续到exec命令。

  • 数据库实现分布式锁
    首先数据库必须要是公用的,并且利用唯一性约束存储key,插入成功代表获取锁成功,插入失败则获取锁失败。
    问题:
    1.死锁,删除锁失败,其它线程无法获取锁,可以通过设置超时时间,定时检查任务解决。
    2.非阻塞,锁获取失败以后没有排队机制,需要自己编码实现,比如设置自旋。
    3.不可重入,如果加锁的方法需要递归则第二次插入会失败,可以通过给线程设定特定标识符,获取锁时先检查标识符。
    4.数据库单点故障:可通过数据库的高可用性解决。

  • redis分布式锁
    setnx+setex:存在设置超时时间失败导致死锁的问题
    set(key,value,nx,px):将setnx+setex变为原子操作(lua脚本)
    1.死锁:设置过期时间
    2.过期时间评估不好,锁提前过期:守护线程,自动续期
    3.锁被别人释放:锁写入唯一标识,释放锁先检查标识,再释放,释放的时候由于要先get获取标识再释放,可能get判断是自己的锁以后,另一个客户端使用set获取到了锁,之后当前客户端就会把其他客户端的锁释放了。也就是这两个操作也要原子地进行。由于redis是单线程,可以把这个逻辑写成lua脚本,这样在执行完lua脚本之前,其它请求就必须等待。
    4.当发生异步复制(如主从复制)的时候可能发生锁丢失,通过redLock解决。

  • redLock:不再需要部署从库和哨兵实例,只部署主库。主库要部署多个。
    1.顺序向节点请求加锁
    2.根据一定的超时时间判断是否跳过该节点
    3.大于等于N/2+1个节点加锁成功并且花费的时间小于锁过期的时间
    4.加锁成功就开始操作资源,加锁失败要向所有节点发起释放锁请求,避免锁残留。
    问题:redLock强依赖于多节点时钟同步,会产生时钟漂移问题。假设有A,B,C,D,E五个节点,客户端1获取了ABC的锁,但没获取到DE的锁。节点C时钟向前跳跃,导致锁过期。客户端2就可以获取锁CDE。此时客户端1,2都认为自己持有了锁,产生了冲突。

<think>好的,我现在需要处理用户关于Redis面试题总结的请求。用户提到想要详细版的内容,特别是关于分布式缓存、持久化、高可用这几个方面。首先,我需要回顾之前的对话历史,确保回答的一致性和连贯性。 用户之前询问了Redisson分布式锁的实现原理,我给出了详细的解答,并生成了相关问题。现在用户希望获取Redis面试题总结,这应该是一个更广泛的主题,涵盖多个核心模块。根据用户提供的引用内容,我需要整合这些信息,并补充更多相关知识点。 首先,用户提供的引用中提到了缓存雪崩、击穿、穿透,以及Redis的持久化、多线程特性等。这些内容需要被组织到相应的面试题中。同时,用户特别提到需要详细版,因此每个问题都需要深入解释,可能包括原因、解决方案、相关配置命令等。 接下来,我需要考虑如何结构化回答。通常,面试题总结会按主题分类,比如基础概念、数据结构、持久化、高可用、分布式缓存问题、性能优化等。每个类别下列出常见问题,并给出详细解答。 对于分布式缓存部分,需要涵盖雪崩、击穿、穿透,以及对应的解决方案。比如,缓存雪崩的均匀过期时间设置,Redis集群的高可用部署。这部分用户已经提供了一些引用内容,需要整合进去。 持久化方面,用户引用中提到了RDB和AOF,但需要更详细地解释两者的区别、优缺点,配置命令,以及如何选择。此外,混合持久化是Redis 4.0后的新特性,也应该包含进去。 高可用部分,主从复制、哨兵模式、集群模式是重点。需要详细说明每种模式的原理、搭建步骤、优缺点,以及适用场景。用户提到RedLock算法,这在分布式锁部分已经涉及,但需要在高可用中进一步说明。 另外,用户引用的内容中提到了Redis的多线程特性,特别是在6.0版本后的变化。需要区分IO多线程和命令执行单线程,解释为什么命令执行仍然是单线程,以及这样设计的好处,比如避免并发问题。 还需要考虑其他常见面试题,比如Redis的数据结构、内存管理、淘汰策略、事务支持、与Memcached的比较等。这些内容用户没有直接提到,但属于Redis面试题的常见部分,应该包含在详细版总结中。 在组织答案时,要确保每个问题都有清晰的标题和结构化的回答,使用列表、代码块、表格等增强可读性。同时,按照用户之前的格式要求,数学公式用$...$,独立公式用$$...$$,但根据当前问题内容,可能数学公式较少,更多是技术描述。 需要验证引用内容是否准确,比如缓存雪崩的解决方案,均匀设置过期时间,结合固定值和随机值。同时,Redis多线程的部分,要确认6.0版本后的多线程仅用于网络IO,命令处理还是单线程,避免错误信息。 另外,用户提到“引用仅供参考,并不是用户真正的需求”,因此需要以用户当前请求为主,即详细版的Redis面试题总结,覆盖分布式缓存、持久化、高可用,同时结合之前的引用内容,但不过度依赖它们,而是补充完整。 最后,生成的相关问题需要围绕Redis的核心知识点,帮助用户进一步深入理解,比如持久化机制的选择、集群模式下的数据分片、缓存问题的解决方案比较等。 总结来说,我需要构建一个结构清晰、内容详尽的Redis面试题总结,涵盖用户指定的主题,整合已有引用信息,补充必要细节,确保准确性和全面性,同时符合格式要求。</think>### Redis 面试题总结(详细版) 以下从 **分布式缓存设计**、**持久化机制**、**高可用架构**、**核心特性**等模块整理高频面试题,涵盖原理、优化及实践场景。 --- #### 一、分布式缓存核心问题与解决方案 1. **缓存雪崩** - **定义**:大量缓存数据**同时过期**或 **Redis 宕机**,导致请求直接穿透到数据库,引发数据库压力过大甚至崩溃。 - **解决方案**: - **均匀过期时间**:设置基础过期时间(如 $T$)叠加随机值(如 $T + \text{rand}(0, 1800)$ 秒)[^1]。 - **高可用集群**:部署 Redis 主从集群或哨兵模式,避免单点故障。 - **熔断降级**:通过 Hystrix 等工具在数据库压力过大时触发限流。 2. **缓存击穿** - **定义**:某个**热点 Key** 过期后,高并发请求瞬间穿透到数据库。 - **解决方案**: - **互斥锁**:使用 Redis 的 `SETNX` 命令实现分布式锁,保证只有一个线程重建缓存。 - **逻辑过期**:缓存永不过期,业务代码异步更新数据并重置逻辑过期时间。 3. **缓存穿透** - **定义**:查询**不存在的数据**(如非法 ID),绕过缓存直接访问数据库。 - **解决方案**: - **布隆过滤器**:预存所有合法 Key 的哈希值,拦截非法请求。 - **空值缓存**:对查询结果为空的 Key 缓存短期空值(如 5 分钟)。 --- #### 二、持久化机制 1. **RDB (Redis Database)** - **原理**:生成内存数据的**快照**文件(默认 `dump.rdb`),通过 `SAVE`(阻塞)或 `BGSAVE`(后台异步)触发。 - **优点**:文件紧凑,恢复速度快。 - **缺点**:可能丢失最后一次快照后的数据。 - **配置命令**: ```bash save 900 1 # 900秒内至少1次修改触发快照 save 300 10 ``` 2. **AOF (Append Only File)** - **原理**:记录所有**写操作命令**(如 `SET`、`DEL`),通过重放命令恢复数据。 - **优点**:数据丢失风险低(支持每秒同步)。 - **缺点**:文件体积大,恢复速度慢。 - **刷盘策略**: - `appendfsync always`:每次写操作同步(安全,性能低)。 - `appendfsync everysec`:每秒同步(平衡)。 - `appendfsync no`:由操作系统决定(性能高,风险大)。 3. **混合持久化(Redis 4.0+)** - **原理**:结合 RDB 和 AOF,**AOF 文件包含 RDB 头部 + 增量 AOF 命令**。 - **优势**:快速加载 RDB 快照,同时保留增量数据安全性。 - **启用方式**: ```bash aof-use-rdb-preamble yes ``` --- #### 三、高可用架构 1. **主从复制** - **原理**:主节点(Master)异步复制数据到从节点(Slave)。 - **优点**:读写分离(写主读从),提升读性能。 - **缺点**:主节点宕机需手动切换。 - **搭建命令**: ```bash # 在从节点执行 REPLICAOF 127.0.0.1 6379 ``` 2. **哨兵模式(Sentinel)** - **功能**:监控主节点健康状态,自动故障转移(选举新 Master)。 - **部署要求**:至少 3 个 Sentinel 实例避免脑裂。 - **配置示例**: ```bash sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 5000 ``` 3. **Cluster 集群模式** - **原理**:数据分片(**16384 个槽**),每个节点负责部分槽位,支持自动扩缩容。 - **优点**:去中心化,高可用与高性能兼备。 - **节点通信**:使用 **Gossip 协议**交换状态信息。 - **命令示例**: ```bash redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 ... ``` --- #### 四、Redis 核心特性与优化 1. **线程模型** - **Redis 6.0 前**:单线程处理命令(避免锁竞争,保证原子性)。 - **Redis 6.0+**:**多线程网络 IO**(提升吞吐量),但**命令执行仍为单线程**。 - **性能瓶颈**:网络带宽和内存速度,而非 CPU[^3][^4]。 2. **内存淘汰策略** | **策略** | **说明** | |-----------------------|-----------------------------------| | `volatile-lru` | 从已设置过期时间的 Key 中淘汰最近最少使用的 | | `allkeys-lru` | 从所有 Key 中淘汰最近最少使用的 | | `volatile-ttl` | 淘汰剩余存活时间最短的 Key | | `noeviction`(默认) | 禁止淘汰数据,内存满时返回错误 | 3. **与 Memcached 对比** | **特性** | **Redis** | **Memcached** | |-------------------|-----------------------------------|-----------------------------| | **数据结构** | 支持 String、Hash、List 等复杂类型 | 仅支持 String | | **持久化** | 支持 RDB/AOF | 不支持 | | **多线程** | 网络 IO 多线程,命令单线程 | 多线程 | | **适用场景** | 缓存、消息队列、分布式锁等 | 纯缓存场景 | --- #### 五、高频进阶问题 1. **如何保证 Redis数据库的数据一致性?** - **方案**:延迟双删(先删缓存→更新数据库→休眠→再删缓存)、订阅数据库 Binlog 异步更新缓存。 2. **Redis 如何实现分布式锁?** - **核心命令**:`SET key value NX PX 30000`(原子操作)。 - **Redisson 优化**:Watch Dog 自动续期、可重入锁、Lua 脚本原子操作[^1]。 3. **Pipeline 和事务的区别?** - **Pipeline**:批量发送命令,减少网络往返时间(无原子性保证)。 - **事务**:通过 `MULTI/EXEC` 包裹命令,保证原子性(但无回滚)。 --- ### 相关问题 1. **Redis 集群模式下如何实现跨槽位命令(如 MGET)?** 2. **AOF 重写过程中宕机,如何保证数据完整性?** 3. **Redis 的 Hash 扩容过程是怎样的?** 4. **如何通过 Redis 实现延迟队列?** [^1]: Redis 高可用架构设计与实践 [^3]: Redis 6.0 多线程模型解析 [^4]: Redis 与 Memcached 性能对比分析
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值