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

前言

文本已收录至我的GitHub仓库,欢迎Star: https://github.com/bin3923282...
种一棵树最好的时间是十年前,其次是现在

Tips

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

https://www.processon.com/vie...

上面的是脑图地址

叨絮

这个系列也写了几篇了,今天我们来看看redis,前面的可以去github上看看。
然后下面是前面的文章汇总

说说什么是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 有我找的许多的资料送给大家

你可能感兴趣的:(redisjava后端)