面试题:缓存篇(Redis)

目录

一、Redis 数据类型

1.String 

2.List 

3.Hash 

二、keys 命令问题  (redis有一亿个 key,使用 keys 命令是否会影响线上服务)

三、Redis 过期 key 的删除策略

四、Redis 持久化  

1.AOF 持久化

2.AOF 重写

3.RDB 持久化

4.混合持久化

五、缓存问题  

1.缓存击穿 

2.缓存雪崩 

3.缓存穿透 

六、缓存原子性 

1.Redis 事务局限性 

2.用乐观锁保证原子性

3.用 lua 脚本保证原子性 

七、LRU Cache 实现

1.LRU Cache 淘汰规则 

2.LRU Cache 链表实现


一、Redis 数据类型

概述

数据类型实际描述的是 value 的类型,key 都是 string,常见数据类型(value)有

1. string(embstr、raw、int)
2. list(quicklist,由多个 ziplist 双向链表组成)
3. hash(ziplist、hashtable)
4. set(intset、hashtable)
5. sorted set(ziplist、skiplist)
6. bitmap
7. hyperloglog

        每一种类型都用 redisObject 结构体来表示,每种类型根据情况不同,有不同的编码 encoding(即底层数据结构)

1.String 

  1. 如果字符串保存的是整数值,则底层编码为 int,实际使用 long 来存储

  2. 如果字符串保存的是非整数值(浮点数字或其它字符)又分两种情况

    1. 长度 <= 39 字节,使用 embstr 编码来保存,即将 redisObject 和 sdshdr 结构体保存在一起,分配内存只需一次

    2. 长度 > 39 字节,使用 raw 编码来保存,即 redisObject 结构体分配一次内存,sdshdr 结构体分配一次内存,用指针相连

  3. sdshdr 称为简单动态字符串,实现上有点类似于 java 中的 StringBuilder,有如下特性

    1. 单独存储字符长度,相比 char* 获取长度效率高(char* 是 C 语言原生字符串表示)

    2. 支持动态扩容,方便字符串拼接操作

    3. 预留空间,减少内存分配、释放次数(< 1M 时容量是字符串实际长度 2 倍,>= 1M 时容量是原有容量 + 1M)

    4. 二进制安全,例如传统 char* 以 \0 作为结束字符,这样就不能保存视频、图片等二进制数据,而 sds 以长度来进行读取

2.List 

  1. 3.2 开始,Redis 采用 quicklist 作为其编码方式,它是一个双向链表,节点元素是 ziplist

    1. 由于是链表,内存上不连续

    2. 操作头尾效率高,时间复杂度 O(1)

    3. 链表中 ziplist 的大小和元素个数都可以设置,其中大小默认 8kb

  2. ziplist 用一块连续的内存存储数据,设计目标是让数据存储更紧凑,减少碎片开销,节约内存,它的结构如下

    1. zlbytes – 记录整个 ziplist 占用字节数

    2. zltail-offset – 记录尾节点偏移量

    3. zllength – 记录节点数量

    4. entry – 节点,1 ~ N 个,每个 entry 记录了前一 entry 长度,本 entry 的编码、长度、实际数据,为了节省内存,根据实际数据长度不同,用于记录长度的字节数也不同,例如前一 entry 长度是 253 时,需要用 1 个字节,但超过了 253,需要用 5 个字节

    5. zlend – 结束标记

  3. ziplist 适合存储少量元素,否则查询效率不高,并且长度可变的设计会带来连锁更新问题

3.Hash 

  1. 在数据量较小时,采用 ziplist 作为其编码,当键或值长度过大(64)或个数过多(512)时,转为 hashtable 编码

  2. hashtable 编码

    • hash 函数,Redis 5.0 采用了 SipHash 算法

    • 采用拉链法解决 key 冲突

    • rehash 时机

      ① 当元素数 < 1 * 桶个数时,不扩容

      ② 当元素数 > 5 * 桶个数时,一定扩容

      ③ 当 1 * 桶个数 <= 元素数 <= 5 * 桶个数时,如果此时没有进行 AOF 或 RDB 操作时

      ④ 当元素数 < 桶个数 / 10 时,缩容

    • rehash 要点

      ① 每个字典有两个哈希表,桶个数为 $2^n$,平时使用 ht[0],ht[1] 开始为 null,扩容时新数组大小为元素个数 * 2

      渐进式 rehash,即不是一次将所有桶都迁移过去,每次对这张表 CRUD 仅迁移一个桶

      active rehash,server 的主循环中,每 100 ms 里留出 1s 进行主动迁移

      ④ rehash 过程中,新增操作 ht[1] ,其它操作先操作 ht[0],若没有,再操作 ht[1]

      ⑤ redis 所有 CRUD 都是单线程,因此 rehash 一定是线程安全的  

4.Sorted Set 

  1. 在数据量较小时,采用 ziplist 作为其编码,按 score 有序,当键或值长度过大(64)或个数过多(128)时,转为 skiplist + hashtable 编码,同时采用的理由是

    • 只用 hashtable,CRUD 是 O(1),但要执行有序操作,需要排序,带来额外时间空间复杂度

    • 只用 skiplist,虽然范围操作优点保留,但时间复杂度上升

    • 虽然同时采用了两种结构,但由于采用了指针,元素并不会占用双份内存

  2. skiplist 要点:多层链表、排序规则、 backward、level(span,forward)

  • score 存储分数、member 存储数据、按 score 排序,如果 score 相同再按 member 排序

  • backward 存储上一个节点指针

  • 每个节点中会存储层级信息(level),同一个节点可能会有多层,每个 level 有属性:

    • foward 同层中下一个节点指针

    • span 跨度,用于计算排名,不是所有跳表都实现了跨度,Redis 实现特有

  1. 多层链表可以加速查询,规则为,从顶层开始

    1. 大于同层右边的,继续在同层向右找

    2. 相等找到了

    3. 小于同层右边的或右边为 NULL,下一层,重复 1、2 步骤

  • 以查找【崔八】为例

    1. 从顶层(4)层向右找到【王五】节点,22 > 7 继续向右找,但右侧是 NULL,下一层

    2. 在【王五】节点的第 3 层向右找到【孙二】节点,22 < 37,下一层

    3. 在【王五】节点的第 2 层向右找到【赵六】节点,22 > 19,继续向右找到【孙二】节点,22 < 37,下一层

    4. 在【赵六】节点的第 1 层向右找到【崔八】节点,22 = 22,返回

注意

  • 数据量较小时,不能体现跳表的性能提升,跳表查询的时间复杂度是 $log_2(N)$,与二叉树性能相当

二、keys 命令问题  (redis有一亿个 key,使用 keys 命令是否会影响线上服务

解答

  • keys 命令时间复杂度是 $O(n)$,n 即总的 key 数量,n 如果很大,性能非常低

  • redis 执行命令是单线程执行,一个命令执行太慢会阻塞其它命令,阻塞时间长甚至会让 redis 发生故障切换

改进方案

  • 可以使用 scan 命令替换 keys 命令,语法 scan 起始游标 match 匹配规则 count 提示数目,返回值代表下次的起点

    1. 虽然 scan 命令的时间复杂度仍是 $O(n)$,但它是通过游标分步执行,不会导致长时间阻塞

    2. 可以用 count 参数提示返回 key 的个数

    3. 弱状态,客户端仅需维护游标

    4. scan 能保证在 rehash 也正常工作

    5. 缺点是可能会重复遍历 key(缩容时)、应用应自己处理重复 key

三、Redis 过期 key 的删除策略

记录 key 过期时间

  • 每个库中都包含了 expires 过期字典

    • hashtable结构,键为指针,指向真正 key,值为 long 类型的时间戳,毫秒精度

  • 当设置某个 key 有过期时间时,就会向过期字典中添加此 key 的指针和时间戳

过期 key 删除策略

  • 惰性删除

    • 在执行读写数据库的命令时,执行命令前会检查 key 是否过期,如果已过期,则删除 key

  • 定期删除

    • redis 有一个定时任务处理器 serverCron,负责周期性任务处理,默认 100 ms 执行一次(hz 参数控制)包括:① 处理过期 key、② hash 表 rehash、③ 更新统计结果、④ 持久化、⑤ 清理过期客户端

    • 对于处理过期 key 会:依次遍历库,在规定时间内运行如下操作

      ① 从每个库的 expires 过期字典中随机选择 20 个 key 检查,如果过期则删除

      ② 如果删除达到 5 个,重复 ① 步骤,没有达到,遍历至下一个库

      ③ 规定时间没有做完,等待下一轮 serverCron 运行  

四、Redis 持久化  

1.AOF 持久化

  • AOF - 将每条写命令追加至 aof 文件,当重启时会执行 aof 文件中每条命令来重建内存数据

  • AOF 日志是写后日志,即先执行命令,再记录日志

    • Redis 为了性能,向 aof 记录日志时没有对命令进行语法检查,如果要先记录日志,那么日志里就会记录语法错误的命令

  • 记录 AOF 日志时,有三种同步策略

    • Always 同步写,日志写入磁盘再返回,可以做到基本不丢数据,性能不高

      • 为什么说基本不丢呢,因为 aof 是在 serverCron 事件循环中执行 aof 写入的,并且这次写入的是上一次循环暂存在 aof 缓冲中的数据,因此最多还是可能丢失一个循环的数据

    • Everysec 每秒写,日志写入 AOF 文件的内存缓冲区,每隔一秒将内存缓冲区数据刷入磁盘,最多丢一秒的数据

    • No 操作系统写,日志写入AOF 文件的内存缓冲区,由操作系统决定何时将数据刷入磁盘

2.AOF 重写

  • AOF 文件太大引起的问题

    1. 文件大小受操作系统限制

    2. 文件太大,写入效率变低

    3. 文件太大,恢复时非常慢

  • 重写就是对同一个 key 的多次操作进行瘦身

    1. 例如一个 key 我改了 100 遍,aof 里记录了100 条修改日志,但实际上只有最后一次有效

    2. 重写无需操作现有 aof 日志,只需要根据当前内存数据的状态,生成相应的命令,记入一个新的日志文件即可

    3. 重写过程是由另一个后台子进程完成的,不会阻塞主进程

  • AOF 重写过程

    1. 创建子进程时会根据主进程生成内存快照,只需要对子进程的内存进行遍历,把每个 key 对应的命令写入新的日志文件(即重写日志)

    2. 此时如果有新的命令执行,修改的是主进程内存,不会影响子进程内存,并且新命令会记录到 重写缓冲区

    3. 等子进程所有的 key 处理完毕,再将 重写缓冲区 记录的增量指令写入重写日志

    4. 在此期间旧的 AOF 日志仍然在工作,待到重写完毕,用重写日志替换掉旧的 AOF 日志

3.RDB 持久化

  • RDB - 是把整个内存数据以二进制方式写入磁盘

    • 对应数据文件为 dump.rdb

    • 好处是恢复速度快

  • 相关命令有两个

    • save - 在主进程执行,会阻塞其它命令

    • bgsave - 创建子进程执行,避免阻塞,是默认方式

      • 子进程不会阻塞主进程,但创建子进程的期间,仍会阻塞,内存越大,阻塞时间越长

      • bgsave 也是利用了快照机制,执行 RDB 持久化期间如果有新数据写入,新的数据修改发生在主进程,子进程向 RDB 文件中写入还是旧的数据,这样新的修改不会影响到 RDB 操作

      • 但这些新数据不会补充至 RDB 文件

  • 缺点: 可以通过调整 redis.conf 中的 save 参数来控制 rdb 的执行周期,但这个周期不好把握

    • 频繁执行的话,会影响性能

    • 偶尔执行的话,如果宕机又容易丢失较多数据

4.混合持久化

  • 从 4.0 开始,Redis 支持混合持久化,即使用 RDB 作为全量备份,两次 RDB 之间使用 AOF 作为增量备份

    • 配置项 aof-use-rdb-preamble 用来控制是否启用混合持久化,默认值 no

    • 持久化时将数据都存入 AOF 日志,日志前半部分为二进制的 RDB 格式,后半部分是 AOF 命令日志

    • 下一次 RDB 时,会覆盖之前的日志文件

  • 优缺点

    • 结合了 RDB 与 AOF 的优点,恢复速度快,增量用 AOF 表示,数据更完整(取决于同步策略)、也无需 AOF 重写

    • 与旧版本的 redis 文件格式不兼容

五、缓存问题  

1.缓存击穿 

  • 缓存击穿是指:某一热点 key 在缓存和数据库中都存在,它过期时,这时由于并发用户特别多,同时读缓存没读到,又同时去数据库去读,压垮数据库

  • 解决方法

    1. 热点数据不过期

    2. 对【查询缓存没有,查询数据库,结果放入缓存】这三步进行加锁,这时只有一个客户端能获得锁,其它客户端会被阻塞,等锁释放开,缓存已有了数据,其它客户端就不必访问数据库了。但会影响吞吐量(有损方案)

2.缓存雪崩 

  • 情况1:由于大量 key 设置了相同的过期时间(数据在缓存和数据库都存在),一旦到达过期时间点,这些 key 集体失效,造成访问这些 key 的请求全部进入数据库。

  • 解决方法:

    1. 错开过期时间:在过期时间上加上随机值(比如 1~5 分钟)

    2. 服务降级:暂停非核心数据查询缓存,返回预定义信息(错误页面,空值等)

  • 情况2:Redis 实例宕机,大量请求进入数据库

  • 解决方法:

    1. 事前预防:搭建高可用集群

    2. 多级缓存:缺点是实现复杂度高

    3. 熔断:通过监控一旦雪崩出现,暂停缓存访问待实例恢复,返回预定义信息(有损方案)

    4. 限流:通过监控一旦发现数据库访问量超过阈值,限制访问数据库的请求数(有损方案)

3.缓存穿透 

  • 缓存穿透是指:如果一个 key 在缓存和数据库都不存在,那么访问这个 key 每次都会进入数据库

    • 很可能被恶意请求利用

    • 缓存雪崩与缓存击穿都是数据库中有,但缓存暂时缺失

    • 缓存雪崩与缓存击穿都能自然恢复,但缓存穿透则不能

  • 解决方法

    1. 如果数据库没有,也将此不存在的 key 关联 null 值放入缓存,缺点是这样的 key 没有任何业务作用,白占空间

    2. 布隆过滤器

    ① 过滤器可以用来判定 key 不存在,发现这些不存在的 key,把它们过滤掉就好

    ② 需要将所有的 key 都预先加载至布隆过滤器

    ③ 布隆过滤器不能删除,因此查询删除的数据一定会发生穿透

旁路缓存

  • 旁路缓存(Cache Aside),是一种常见的使用缓存的策略

  • 查询规则

    • 先读缓存

    • 如果命中,直接返回

    • 如果缺失,查 DB 并将结果放入缓存,再返回

  • 增、删、改规则

    • 新增数据,直接存 DB

    • 修改、删除数据,先更新DB,再删缓存

为什么要先操作库,再操作缓存?

  • 假设操作库和缓存均能成功,如果先操作缓存,会大几率出现数据库与缓存不一致的情况

一致性分析 - 先清缓存,再更新库

一致性分析 - 先更新库,再清缓存

  • 会有短暂不一致,但最终会一致

  • 假设查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询,会如上图所示出现不一致

  • 但这种几率出现机会很小

用锁解决一致性

  • 缺点:影响吞吐量、分布式锁设计较为复杂

六、缓存原子性 

1.Redis 事务局限性 

单条命令是原子性,这是由 redis 单线程保障的,多条命令能否用 multi + exec 来保证其原子性呢?

Redis multi + exec 的认识

1. multi + exec 并不支持回滚,例如

set a 1000set b 1000 set c a

multidecr aincr bincr cexec

2. multi + exec 中的 读操作没有意义
3. 既然 multi + exec 中读没有意义,就无法保证 + 的原子性,例如

set a 1000set b 1000

get aget b(分别为 1000),现在转账 500

multiset a 500set b 1500exec

但如果在 get multi 之间其它客户端修改了 a b,会造成丢失更新

2.用乐观锁保证原子性

watch 命令,用来盯住 key(一到多个),如果这些 key 在事务期间:

l 没有被别的客户端修改,则 exec 才会成功
l 被别的客户端改了,则 exec 返回 nil

例如

set a 1000set b 1000

watch a b

multi

set a 500set b 1500

exec

3.用 lua 脚本保证原子性 

redis 支持 lua 脚本,能保证 lua 脚本执行的原子性,可以取代 multi + exec

l 例如: eval "local a = tonumber ( redis.call ('GET',KEYS[1]));local b = tonumber ( redis.call ('GET',KEYS[2]));local c = tonumber (ARGV[1]); if(a >= c) then redis.call ('SET', KEYS[1], a-c); redis.call ('SET', KEYS[2], b+c ); return 1;else return 0; end" 2 a b 500

 

七、LRU Cache 实现

1.LRU Cache 淘汰规则 

Least Recently Used,将最近最少使用的 key 从缓存中淘汰掉

  • 时间上,新的留下,老的淘汰
  • 如果访问了某个 key,则它就变成最新的

实现策略:

  • 链表法,最近访问的 key 移动到链表头,不常访问的自然靠近链表尾,如果超过容量、个数限制,移除尾部的
  • 随机取样法,链表法占用内存较多,redis 使用的是随机取样法,每次只抽 5 key,每个 key 记录了它们的最近访问时间,在这 5 个里挑出最老的移除

2.LRU Cache 链表实现

1.如何断开节点链接 

2.如何链入头节点

  •  删除逻辑
    • map 删除
    • 断开节点链接
  • 查询逻辑
    • map 获取
    • 断开节点链接
    • 链入头节点
  • 新增逻辑
    • map 获取,没有新建节点,存入 map
    • 有则更新节点值,断开链接
    • ①、② 完成后均链入头节点
    • 检查是否超过限额,是则删除最后一个节点,并断开它的链接
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、什么是 Redis? 2、Redis 相比 memcached 有哪些优势? 3、Redis 支持哪几种数据类型? 4、Redis 主要消耗什么物理资源? 5、Redis 的全称是什么? 6、Redis 有哪几种数据淘汰策略? 7、Redis 官方为什么不提供 Windows 版本? 8、一个字符串类型的值能存储最大容量是多少? 9、为什么 Redis 需要把所有数据放到内存中? 10、Redis 集群方案应该怎么做?都有哪些方案? 11、Redis 集群方案什么情况下会导致整个集群不可用? 12、MySQL 里有 2000w 数据,Redis 中只存 20w 的数据, 如何保证 Redis 中的数据都是热点数据? 13、Redis 有哪些适合的场景? 14、Redis 支持的 Java 客户端都有哪些?官方推荐用哪个? 15、RedisRedisson 有什么关系? 16、Jedis 与 Redisson 对比有什么优缺点? 17、Redis 如何设置密码及验证密码? 18、说说 Redis 哈希槽的概念? 19、Redis 集群的主从复制模型是怎样的? 20、Redis 集群会有写操作丢失吗?为什么? 21、Redis 集群之间是如何复制的? 22、Redis 集群最大节点个数是多少? 23、Redis 集群如何选择数据库? 24、怎么测试 Redis 的连通性? 25、Redis 中的管道有什么用? 26、怎么理解 Redis 事务? 27、Redis 事务相关的命令有哪几个? 28、Redis key 的过期时间和永久有效分别怎么设置? 29、Redis 如何做内存优化? 30、Redis 回收进程如何工作的? 31、Redis 回收使用的是什么算法? 32、Redis 如何做大量数据插入? 33、为什么要做 Redis 分区? 34、你知道有哪些 Redis 分区实现方案? 35、Redis 分区有什么缺点? 36、Redis 持久化数据和缓存怎么做扩容? 37、分布式 Redis 是前期做还是后期规模上来了再做好?为 什么? 38、Twemproxy 是什么? 39、支持一致性哈希的客户端有哪些? 40、Redis 与其他 key-value 存储有什么不同? 41、Redis 的内存占用情况怎么样? 42、都有哪些办法可以降低 Redis 的内存使用情况呢? 43、查看 Redis 使用情况及状态信息用什么命令? 44、Redis 的内存用完了会发生什么? 45、Redis 是单线程的,如何提高多核 CPU 的利用率? 46、一个 Redis 实例最多能存放多少的 keys?List、Set、 Sorted Set 他们最多能存放多少元素? 47、Redis 常见性能问和解决方案? 48、Redis 提供了哪几种持久化方式? 49、如何选择合适的持久化方式? 50、修改配置不重启 Redis 会实时生效吗?
回答: Redis缓存面试可以涉及到Redis的使用场景和常见问。其中一个常见的面试是关于Redis的会话缓存(session cache)。Redis的会话缓存可以通过持久化提供更好的存储一致性,相比其他存储(如Memcached)具有优势。如果用户的购物车信息丢失,大多数人会感到不高兴。幸运的是,Redis提供了持久化功能,可以确保数据不会丢失。甚至一些流行的商业平台如Magento也提供了Redis的插件来缓存会话。 另一个可能涉及到的面试是关于缓存穿透的问缓存穿透指的是在Redis缓存和数据库中都找不到相关的数据,可能由于非法查询导致。解决缓存穿透的方法包括过滤非法查询、缓存空对象和采用布隆过滤器。过滤非法查询可以在后台服务中对非法查询进行过滤,不让其落到Redis服务上。缓存空对象可以在Redis中存储一个表示数据不存在的空对象,避免重复查询。而采用布隆过滤器可以用于判断一个元素是否在集合中,具有较高的空间效率和查询时间,但也有一定的误识别率和删除困难。 以上是关于Redis缓存的一些面试的回答,希望对您有所帮助。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [面试 Redis 没底?这 40 道面试让你不再慌(附答案)](https://blog.csdn.net/xmt1139057136/article/details/115423283)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [66道史上最全Redis面试面试官能问的都被我找到了(附答案)](https://blog.csdn.net/m0_60478027/article/details/119215355)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [10经典Redis面试](https://blog.csdn.net/m0_52256357/article/details/125709819)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值