写点什么

2022-Java 后端工程师面试指南 -(Redis)

作者:自然
  • 2022 年 8 月 06 日
  • 本文字数:6973 字

    阅读完需:约 23 分钟

前言

文本已收录至我的 GitHub 仓库,欢迎 Star:https://github.com/bin392328206/six-finger

种一棵树最好的时间是十年前,其次是现在

Tips

面试指南系列,很多情况下不会去深挖细节,是小六六以被面试者的角色去回顾知识的一种方式,所以我默认大部分的东西,作为面试官的你,肯定是懂的。


https://www.processon.com/view/link/600ed9e9637689349038b0e4


上面的是脑图地址

说说什么是 redis 吧

Redis 是一个开放源代码(BSD 许可)的内存中数据结构存储,用作数据库,缓存和消息代理。它支持数据结构,例如字符串,哈希,列表,集合,带范围查询的排序集合,位图,超日志,带有半径查询和流的地理空间索引。Redis 具有内置的复制,Lua 脚本,LRU 逐出,事务和不同级别的磁盘持久性,并通过 Redis Sentinel 和 Redis Cluster 自动分区提供了高可用性。

说说 Redis 有哪些优缺点

优点


  • 性能优异, Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s。

  • 数据持久化,支持 AOF 和 RDB 两种持久化方式。

  • 事务,Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作合并后的原子性执行。

  • 结构丰富,除了支持 string 类型的 value 外还支持 hash、set、zset、list 等数据结构。

  • 主从复制,主机会自动将数据同步到从机,可以进行读写分离。


缺点


  • 库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。

  • 宕机,宕机前有部分数据未能及时同步到从机,切换 IP 后还会引入数据不一致的问题,降低了系统的可用性。

  • redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。

说说为啥要用缓存

主要是为了提高系统的吞吐量,应对高并发,高性能场景

为什么要用 Redis 而不用 map/guava 做缓存?

  • Java 实现的 Map 是本地缓存,如果有多台实例(机器)的话,每个实例都需要各自保存一份缓存,缓存不具有一致性

  • Redis 实现的是分布式缓存,如果有多台实例(机器)的话,每个实例都共享一份缓存,缓存具有一致性。

  • Java 实现的 Map 不是专业做缓存的,JVM 内存太大容易挂掉的。一般用做于容器来存储临时数据,缓存的数据随着 JVM 销毁而结束。Map 所存储的数据结构,缓存过期机制等等是需要程序员自己手写的。

  • Redis 是专业做缓存的,可以用几十个 G 内存来做缓存。Redis 一般用作于缓存,可以将缓存数据保存在硬盘中,Redis 重启了后可以将其恢复。原生提供丰富的数据结构、缓存过期机制等等简单好用的功能。

Redis 为什么这么快

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O(1);


2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;


3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗(绝大多数的瓶颈不在 cpu)


4、使用多路 I/O 复用模型,非阻塞 IO;


5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,使用了 resp 协议

聊聊 resp 协议吧

Redis 是 Redis 序列化协议,Redis 客户端 RESP 协议与 Redis 服务器通信。Redis 协议在以下几点之间做出了折衷:


  • 简单的实现

  • 快速地被计算机解析

  • 简单得可以能被人工解析


其实就是一个二进制的序列化协议,举几个简单的例子哈在 RESP 中,某些数据的类型取决于第一个字节:


“+”代表简单字符串 Simple Strings


“+”代表错误类型


“:”代表整数


基于这种协议的话,其实我们可以自己去实现一个 redis 的客户端,以后有机会给大家写写。

如果万一 CPU 成为你的 Redis 瓶颈了,或者,你就是不想让服务器其他核闲置,那怎么办?

那也很简单,你多起几个 Redis 进程就好了。Redis 是 key-value 数据库,又不是关系数据库,数据之间没有约束。只要客户端分清哪些 key 放在哪个 Redis 进程上就可以了。redis-cluster 可以帮你做的更好。

说说 Redis 的基本数据结构

  • String 整数,浮点数或者字符串

  • Set 集合

  • Zset 有序集合

  • Hash 散列表

  • List 列表

那说说有序集合的实现方式是哪种数据结构?

跳跃表。


  • 当数据较少时,sorted set 是由一个 ziplist 来实现的。

  • 当数据多的时候,sorted set 是由一个 dict + 一个 skiplist 来实现的。简单来讲,dict 用来查询数据到分数的对应关系,而 skiplist 用来根据分数查询数据(可能是范围查找)。

说说 redis 的底层数据结构

sds


Redis 的字符串,不是 C 语言中的字符串(即以空字符’\0’结尾的字符数组),它是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis 的默认字符串表示。


1、len 保存了 SDS 保存字符串的长度


2、buf[] 数组用来保存字符串的每个元素


3、free j 记录了 buf 数组中未使用的字节数量


链表


链表是一种常用的数据结构,C 语言内部是没有内置这种数据结构的实现,所以 Redis 自己构建了链表的实现


字典


字典又称为符号表或者关联数组、或映射(map),是一种用于保存键值对的抽象数据结构。字典中的每一个键 key 都是唯一的,通过 key 可以对值来进行查找或修改。C 语言中没有内置这种数据结构的实现,所以字典依然是 Redis 自己构建的。


跳跃表跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。具有如下性质:


1、由很多层结构组成;


2、每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的 head 节点和后面的 nil 节点;


3、最底层的链表包含了所有的元素;


4、如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集);


5、链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点;


压缩列表


压缩列表(ziplist)是 Redis 为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。


压缩列表的原理:压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。

说说缓存雪崩

一个缓存雪崩发过程


  • redis 集群大面积故障

  • 缓存失效,但依然大量请求访问缓存服务 redis

  • redis 大量失效后,大量请求转向到 mysql 数据库

  • mysql 的调用量暴增,很快就扛不住了,甚至直接宕机

  • 由于大量的应用服务依赖 mysql 和 redis 的服务,这个时候很快会演变成各服务器集群的雪崩,最后网站彻底崩溃。


如何解决缓存雪崩


第一种方案: 缓存层设计成高可用,防止缓存大面积故障。即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如 Redis Sentinel 和 Redis Cluster 都实现了高可用。


第二种方案:在批量往 Redis 存数据的时候,把每个 Key 的失效时间都加个随机值就好了,这样可以保证数据不会在同一时间大面积失效,我相信,Redis 这点流量还是顶得住的。

那你聊聊缓存击穿

我个人理解 击穿 就是正面刚 比如我是矛 你是盾 我直接把你的盾击穿, 就是比如 几个热点 Key 同时几百万并发直接把 redis 干掉了, 然后数据全部打到数据库的情况,或者是 redis 的这几个热点数据失效的情景下,同时全部的并发查这个热数据,导致最后打到数据库的情况 这个就是缓存击穿。


如何解决缓存击穿


还是分布式锁 哈哈 因为分布式锁能控制到数据库的最后一到防线 redis 做集群 哨兵


正常来说一般系统的 qps 都有一个峰值,一般我们使用能抗住这个峰值的内存去做这个缓存

那你说说缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为 id 为“-1”的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。


如何解决缓存穿透


第一种方案 和上面的双重锁一样 如果是拿到数据库为空 那么就给这个 key 设置一个 null 值 时间设置短一点 30s, 这样下次并发进来就不会说把数据打到我们的数据库上了


还有就是我们写代码的时候 要对一些非法的请求参数校验 我相信大家都是这样做的。


第二种方案 采用我们第一篇中学到的一个高级用法 bitMap,查询的时候先查 bitmap 确定是否含有这个 key

说说你是怎么解决缓存一致性问题的

几种方式缓存不一致的原因和解决方案


方案一 先更新数据库,再删缓存这个方案的问题是什么呢? 就是假设我们更新数据成功了 然后去删除缓存的时候失败了 这就导致了缓存中是老数据,会造成缓存不一致


那我们就要保证删除一定要成功,我们可以在最后删除的时候 多删除几次,第二个就是用一个中间件 canal 去兼听 mysql 的 binlog 然后 从 binlong 中解析出要删除的字段 然后 继续上面第一个的方式(这个方式的好处 全程也算是异步的跟业务代码是没有关系的)


方案二 先更新数据库,再更新缓存


这个操作 问题更多感觉 首先 更新数据成功 更新缓存失败,或者是开始更新数据库成功 然后更新缓存成功 然后事务回滚,也是缓存不一致。


方案三 删除缓存 再更新数据库看起来好像最好 我反正是删除缓存了 就算更新失败 下次去读也是最新的数据(一切看起来很美好),其实不然,试想 2 个并发一个更新 一个查询 你先更新的时候 删除了缓存 但是此时 查询发现没有缓存 然后吧数据缓存到了数据库 就会去查数据库 但是此时更新的又更新成功,最后就会再很长的一个时间内 缓存和数据库是不一致的,所以这种是方案是不可取的


综上所诉,我觉得最好的方式先查再删除 然后再配合订阅 binlong 来做多重删除的方式是不错的,可能我接触的不是很多,希望各位大佬有更好的方式提出

说说 Redis 的淘汰策略

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。

  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key。

  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key。

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key。

  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。

  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。


其实我觉得用 volatile-lru 就好了 毕竟报错是完全没有必要的 还有就是设置一个报警装置 如果不够了 就搞主从 哈哈

聊聊 redis 的持久化策略

Redis 为持久化提供了两种方式:


  • RDB:在指定的时间间隔能对你的数据进行快照存储。

  • AOF:记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据。

聊聊 RDB

rdb 是默认的持久化方式,打个比方,你可以设置比如 90s 内 有一次写入,就持久化一次,30s 内 5 次写入就持久化一次等等,当然如果你想要禁用 RDB 配置,也是非常容易的,只需要在 save 的最后一行写上:save ""。


在 Redis 中 RDB 持久化的触发分为两种:自己手动触发与 Redis 定时触发。针对 RDB 方式的持久化,手动触发可以使用:


  • save:会阻塞当前 Redis 服务器,直到持久化完成,线上应该禁止使用。

  • bgsave:该触发方式会 fork 一个子进程,由子进程负责持久化过程,因此阻塞只会发生在 fork 子进程的时候。

说说 AOF

appendonly yes 首先是要开启 aof。appendfsync everysec 它其实有三种模式:


  • always:把每个写命令都立即同步到 aof,很慢,但是很安全

  • everysec:每秒同步一次,是折中方案(默认也是这个)

  • no:redis 不处理交给 OS 来处理,非常快,但是也最不安全


AOF 的整个流程大体来看可以分为两步,一步是命令的实时写入(如果是 appendfsync everysec 配置,会有 1s 损耗),第二步是对 aof 文件的重写。


对于增量追加到文件这一步主要的流程是:命令写入=》追加到 aof_buf =》同步到 aof 磁盘。那么这里为什么要先写入 buf 在同步到磁盘呢?如果实时写入磁盘会带来非常高的磁盘 IO,影响整体性能。

如何恢复 redis 的数据呢

启动时会先检查 AOF 文件是否存在,如果不存在就尝试加载 RDB。那么为什么会优先加载 AOF 呢?因为 AOF 保存的数据更完整,通过上面的分析我们知道 AOF 基本上最多损失 1s 的数据。

说说持久化的性能实战

一些线上经验


  • 如果 Redis 中的数据并不是特别敏感或者可以通过其它方式重写生成数据,可以关闭持久化,如果丢失数据可以通过其它途径补回;

  • 自己制定策略定期检查 Redis 的情况,然后可以手动触发备份、重写数据;

  • 可以加入主从机器,利用一台从机器进行备份处理,其它机器正常响应客户端的命令;

聊聊 Redis 中的 Master-Slave 模式

主从架构的特点


  • 主服务器负责接收写请求

  • 从服务器负责接收读请求

  • 从服务器的数据由主服务器复制过去。主从服务器的数据是一致的


主从架构的好处


  • 读写分离(主服务器负责写,从服务器负责读)

  • 高可用(某一台从服务器挂了,其他从服务器还能继续接收请求,不影响服务)

  • 处理更多的并发量(每台从服务器都可以接收读请求,读 QPS 就上去了)

说说主从同步呗

主从架构的特点之一:主服务器和从服务器的数据是一致的。主从同步的 2 种情况


完整的同步


  • 从服务器向主服务器发送 PSYNC 命令

  • 收到 PSYNC 命令的主服务器执行 BGSAVE 命令,在后台生成一个 RDB 文件。并用一个缓冲区来记录从现在开始执行的所有写命令。

  • 当主服务器的 BGSAVE 命令执行完后,将生成的 RDB 文件发送给从服务器,从服务器接收和载入 RBD 文件。将自己的数据库状态更新至与主服务器执行 BGSAVE 命令时的状态。

  • 主服务器将所有缓冲区的写命令发送给从服务器,从服务器执行这些写命令,达到数据最终一致性。


部分重同步


  • 主从服务器的复制偏移量 主服务器每次传播 N 个字节,就将自己的复制偏移量加上 N

  • 从服务器每次收到主服务器的 N 个字节,就将自己的复制偏移量加上 N

  • 通过对比主从复制的偏移量,就很容易知道主从服务器的数据是否处于一致性的状态!

那你说说 redis 的高可用方案呗

Redis 一般以主/从方式部署(这里讨论的应用从实例主要用于备份,主实例提供读写)该方式要实现 HA 主要有如下几种方案:


  • keepalived: 通过 keepalived 的虚拟 IP,提供主从的统一访问,在主出现问题时, 通过 keepalived 运行脚本将从提升为主,待主恢复后先同步后自动变为主,该方案的好处是主从切换后,应用程序不需要知道(因为访问的虚拟 IP 不变),坏处是引入 keepalived 增加部署复杂性,在有些情况下会导致数据丢失

  • zookeeper: 通过 zookeeper 来监控主从实例, 维护最新有效的 IP, 应用通过 zookeeper 取得 IP,对 Redis 进行访问,该方案需要编写大量的监控代码

  • sentinel: 通过 Sentinel 监控主从实例,自动进行故障恢复,该方案有个缺陷:因为主从实例地址( IP & PORT )是不同的,当故障发生进行主从切换后,应用程序无法知道新地址,故在 Jedis2.2.2 中新增了对 Sentinel 的支持,应用通过 redis.clients.jedis.JedisSentinelPool.getResource() 取得的 Jedis 实例会及时更新到新的主实例地址

那你说说 Redis 哈希槽的概念? 一致性 hash 和哈希槽的概念和区别

这个问题其实就是问 再集群环境下, redis 不同的 key 存储到哪个节点的问题 ,Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。


Redis Cluster 是自己做的 crc16 的简单 hash 算法,没有用一致性 hash。Redis 的作者认为它的 crc16(key) mod 16384 的效果已经不错了,虽然没有一致性 hash 灵活,但实现很简单,节点增删时处理起来也很方便。

聊聊分布式锁

这个话题基本上是分布式系统开发的一个必问的题目了问你分布式锁是怎么实现的,然后大家可能就搭用它的 set NX EX 命令 然后用 lua 脚本做成一个原子性操作来实现分布式锁。其实这么搭也可以吧,然后我们一般在生产环境的话,可能会用一些开源框架,你不如说 Redisson 来实现分布式锁。

聊聊 Redisson 是怎么实现分布式锁的

  • 第一步先尝试去加锁,返回过期时间,如果为空则可以获得锁 (返回获取锁成功)(,在 lua 脚本里面会判断你的 key 和 value 是不是已经持有锁了,如果是,就是给你重试次数加,然后获取锁也是失败)

  • 如果第一次加锁失败之后,就会去判断你最大等待时间,如果走到这的时候已经超过最大等待时间(直接返回获取锁失败,)

  • 接下来就是说我要去订阅 redis 解锁这个事件,一旦有人把锁释放就会继续通知所有的线程去竞争锁(减少 cpu 的损耗)

  • 然后是一个死循环的去获取锁,当时每次执行这个循环的时候,每次去获取锁之前都要去判断当前是否已经超过最大的等待时间,如果超过了就直接释放锁。只有当获得锁,或者是最大的等待时间超过之后才会返回是否成功获取锁的标志。(里面也是需要被通知才继续循环)

  • 通过 Redisson 实现分布式可重入锁,比纯自己通过 set key value px milliseconds nx +lua 实现(实现一)的效果更好些,虽然基本原理都一样,因为通过分析源码可知,RedissonLock

  • 是可重入的,并且考虑了失败重试,可以设置锁的最大等待时间, 在实现上也做了一些优化,减少了无效的锁申请,提升了资源的利用率。

结束

redis 就这些吧,接下来复习下 es 吧

日常求赞

好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是真粉


创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见


微信 搜 "六脉神剑的程序人生" 回复 888 有我找的许多的资料送给大家

发布于: 刚刚阅读数: 3
用户头像

自然

关注

还未添加个人签名 2020.03.01 加入

小六六,目前负责营收超百亿的支付中台

评论

发布
暂无评论
2022-Java后端工程师面试指南-(Redis)_面试题_自然_InfoQ写作社区