其他资料
每日速记10道java面试题16-CSDN博客
每日速记10道java面试题17-CSDN博客
目录
6.为什么redis设计为单线程,却要在6.0版本引入多线程?
1.讲一下Redis底层的数据结构
Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)
结构类型 | 结构存储的值 | 结构的读写能力 |
---|---|---|
String 字符串 | 可以是字符串、整数或浮点数 | 对整个字符串或字符串的一部分进行操作;对整数或浮点数进行自增或自减操作; |
List 列表 | 一个链表,链表上的每个节点都包含一个字符串 | 对链表的两端进行push和pop操作,读取单个或多个元素;根据值查找或删除元素; |
Set 集合 | 包含字符串的无序集合 | 字符串的集合,包含基础的方法有查看是否存在添加、获取、删除;还包含计算交集、并集、差集等 |
Hash 散列 | 包含键值对的无序散列表 | 包含方法有添加、获取、删除单个元素 |
Zset 有序集合 | 和散列一样,用于存储键值对 | 字符串成员与浮点数分数之间的有序映射;元素的排列顺序由分数的大小决定;包含方法有添加、获取、删除单个元素以及根据分值范围或成员来获取元素 |
2.ZSet底层是怎么实现的?
Zset底层数据结构是有压缩列表或跳表实现的:
- 当有序集合的元素个数小于128个,并且每个元素的值小于64个字节时,Redis会使用压缩列表作为Zset类型的底层数据结构
- 如果有序集合的元素不满足以上条件,Redis会使用调表作为Zset类型的底层数据结构;
延伸→跳表是怎么实现的?
跳表主要是通过多层链表来实现,底层链表保存所有元素,而每一层链表都是下一层的子集。
在进行查找的时候,会先在有着最高层级的头结点的最高层开始查找,一旦下个节点的值为null或者大于当前节点,此时就会在当前节点降一层继续往前走,以此类推,直到找到最终节点。查找效率还是很高的,时间复杂度是O(logn)。
在插入的时候,会从最高层逐层去查找插入的位置,查找的逻辑跟前面的一样,找到插入位置之后会随机决定节点的层级,最后插入节点并更新指针。
删除的时候,也是从最高层逐层去查找插入的位置,最后删除结点并更新指针。
延伸→层级是如何决定的?
一个节点的层级是有一个概率函数来决定的,这个概率是25%,能够让跳表的层级分布在一个比较理想的金字塔形,下多上少。
延伸→为什么redis跳表实现还多了个回退指针?
回退指针主要是为了提高跳表的操作效率和灵活性。
例如在进行删除操作时,需要找到要删除节点的前驱节点以便更新指针。回退指针使得这一过程更为高效,避免了从最高层开始逐层查找。
尤其是在频繁插入和删除的场景中,回退指针减少了节点之间指针的更新复杂度,提升性能。
延伸→要是面试官想拷打你的话,这里可能会让你直接手撕跳表...
3.Redis为什么使用跳表而不是用B+树?
这个问题有很多个说法,我去搜索了以下最主流的说法就是从内存占用和范围查找两个方面,跳表会比平衡树更加灵活和简单,但是我在B站上看了一个up主对这两个数据结构的性能分析,似乎在查找和插入上,两个数据结构的性能都差不多,在删除上,跳表就相对比B+树快一点,我个人想法就是之所以不用B+树而采用跳表,可能就是Redis作者觉得用跳表实现比B+树实现更简单方便一点吧。
4.Redis为什么快?
Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销而且也不会导致死锁问题。
Redis 采用了I/O 多路复用机制处理大量的客户端 Socket 请求,I/O 多路复用机制是指一个线程处理多个I/O 流。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 I/O流的效果。
相当于就是去餐馆点餐,不用排队,想好了直接跟服务员说,避免前面的人一直在考虑吃什么导致后面的人一直在等。
5.Redis是怎么实现的IO多路复用?
Redis 的服务端,他会通过 一个线程 来处理 多个 Socket 中的事件,包括连接建立、读、写事件等,当有 Socket 发生了事件,就会将该 Socket 加入到 任务队列 中等待事件分发器来处理即可;
IO 多路复用 的核心就是通过一个线程处理多个 Socket 上的事件,常见的 IO 多路复用底层实现 有:select、poll、epoll。
select实现IO多路复用的思路如下:
简单来说就是将每个Socket都封装成一个FD(linux中的文件描述符 ),然后在用户空间创建一个大小为1024的位图来表示,1表示监听,0表示不监听,例如有三个socket,处理过后的fd=1,2,5,那么在这个位图的第一位、第二位、第五位为1。
位图准备好了就开始调用select函数将位图作为参数带过去内核空间,将位图从用户态拷贝到内核态,此时内核空间会遍历这个位图并监测,如果某个socket就绪了,就遍历整个位图找出监测的fd,然后保留就绪的fd,其他fd设置为0,然后把位图结果拷贝回去覆盖掉用户态的位图。
然后用户态再遍历新位图,找出标记为1的fd,然后去读里面的数据。
总结来说:select来实现IO多路复用,在内存上是有优势的,但是在时间上来说会相对比较慢,毕竟涉及到很多次遍历和拷贝。
poll实现IO多路复用的思路如下:(换汤不换药,位图换数组)
poll模式对select模式做了简单改进,但性能提升不明显,部分关键代码如下:
IO流程:
-
创建pollfd数组,向其中添加关注的fd信息,数组大小自定义
-
调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限
-
内核遍历fd,判断是否就绪
-
数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n
-
用户进程判断n是否大于0,大于0则遍历pollfd数组,找到就绪的fd
与select对比:
-
select模式中的fd_set大小固定为1024,而pollfd在内核中采用链表,理论上无上限
-
监听FD越多,每次遍历消耗时间也越久,性能反而会下降
epoll实现IO多路复用的思路如下:
epoll模式是对select和poll的改进,它提供了三个函数:
第一个是:eventpoll的函数,他内部包含两个东西
一个是:
1、红黑树-> 记录的事要监听的FD
2、一个是链表->一个链表,记录的是就绪的FD
紧接着调用epoll_ctl操作,将要监听的数据添加到红黑树上去,并且给每个fd设置一个监听函数,这个函数会在fd数据就绪时触发,就是准备好了,现在就把fd把数据添加到list_head中去
3、调用epoll_wait函数
就去等待,在用户态创建一个空的events数组,当就绪之后,我们的回调函数会把数据添加到list_head中去,当调用这个函数的时候,会去检查list_head,当然这个过程需要参考配置的等待时间,可以等一定时间,也可以一直等, 如果在此过程中,检查到了list_head中有数据会将数据添加到链表中,此时将数据放入到events数组中,并且返回对应的操作的数量,用户态的此时收到响应后,从events中拿到对应准备好的数据的节点,再去调用方法去拿数据。
小总结:
select模式存在的三个问题:
-
能监听的FD最大不超过1024
-
每次select都需要把所有要监听的FD都拷贝到内核空间
-
每次都要遍历所有FD来判断就绪状态
poll模式的问题:
-
poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降
epoll模式中如何解决这些问题的?
-
基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高
-
每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间
-
利用ep_poll_callback机制来监听FD状态,无需遍历所有FD,因此性能不会随监听的FD数量增多而下降
6.为什么redis设计为单线程,却要在6.0版本引入多线程?
redis在6.0版本之前使用单线程的是因为redis是基于内存的,性能瓶颈一般不在CPU上而是在内存和网络带宽上,因此使用单线程能在简化代码的同时减少上下文切换的开销和多线程之间因数据争夺导致的性能问题,同时redis在单线程中使用了IO多路复用在提高IO利用率。
而在6.0版本在网络IO上去引入多线程,其实为了解决高并发的场景,进一步去提供IO利用率,因为随着硬件提升,redis的性能瓶颈就卡在了网络上,使用多线程能够很好地解决这一个问题。
延伸→那引入了多线程,会不会引发一些线程安全呢?
不会的,因为这个多线程是应用在网络IO上,对于redis的命令执行,仍旧是单线程的。
7.redis中有没有事务?
有的,但是redis中的事务跟我们理解的MySQL中的事务不太一样,MySQL中的事务符合ACID,但是redis中的事务只能保证多条命令执行的原子性。
我们可以使用MULTI 和 EXEC和实现原子性,MULTI命令就是开启一个事务,之后的所有命令会排队执行。EXEC命令就是执行队列里的所有命令,确保原子性。
延伸→redis中的事务有没有回滚机制?
没有的,EXEC命令虽然能保证“要么全部执行,要么全部不执行”,但是这里的“全部不执行”并不是说执行到一半发现命令错误然后回滚,而是在执行EXEC命令之前会先检查队列中的命令,只要没有语法错误,那就会全部执行,在确定执行之后,即使队列中的命令临时出错,也会继续执行下去。所以redis的事务是没有回滚机制的。
延伸→那为什么redis不搞个回滚机制呢?
因为redis设计的初衷就是简单、快速、高效,虽然说有回滚机制可能会让整个系统更加“完美”,但是也会增加系统的复杂性,不符合设计初衷,所以索性就不搞回滚了。
延伸→除了这两个命令还能怎么样来实现原子性呢?
还可以编写Lua脚本,redis会将Lua脚本看成一个整体,因此我们可以在Lua脚本里去编写多条执行命令。
8.Redis如何保证数据的持久化?
有两种方式来保证数据持久化,分别是RDB快照和AOF日志。
RDB快照就是在将Redis某个时刻的数据快照存储在磁盘文件当中,当下次启动Redis之后读取RDB文件,将数据恢复。可以是用save和bgsave,两者的区别就是save会在主线程执行,而bgsave会在单独创建一个子线程执行。
AOF日志就是将Redis的每一次写命令追加到AOF文件当中,下次重启Redis的时候执行文件里面的写命令同样也可以恢复数据,通常可以在redis中设置AOF的写入时机,
写回策略 | 写回时机 | 优点 | 缺点 |
---|---|---|---|
Always | 同步写回 | 可靠性高,最大程度保证数据不丢失 | 每个写命令都要写回硬盘,性能开销大 |
Everysec | 每秒写回 | 性能适中 | 宕机时会丢失1秒内的数据 |
No | 由操作系统控制写回 | 性能好 | 宕机时丢失的数据可能会很多 |
RDB和AOF的优缺点分别是什么?
RDB的优点就是文件体积小,恢复速度快,而且是有主进程fork出来的子进程去恢复的,不会阻塞服务器执行当前命令。
RDB的缺点就是两次RDB之间如果出现Redis宕机,那之间的数据就会丢失。以及在RDB快照恢复期间有其他写操作执行,也会导致数据的不一致。
AOF的优点就是提供了更好地数据安全性,因为它默认是每次写操作都会追加到AOF文件末尾,即使Redis宕机,也只会丢失宕机前的一条写操作数据。
AOF的缺点就是如果写操作过多,可能会导致AOF文件太大,占用过多的磁盘空间。而且如果给写如AOF策略设置为always的话,可能会影响redis的性能
9.谈谈Redis的内存淘汰和过期删除?
内存淘汰就是当redis的内存满了的时候,就会触发内存淘汰机制,去淘汰掉一些不必要的内存资源,腾出空间去存新的数据。
过期删除就是对过期的键值对进行删除,删除策略是定期删除+惰性删除。
延伸→讲一下内存淘汰机制有哪些策略?
内存淘汰策略有八种,分为两类:不进行数据淘汰的策略和进行数据淘汰的策略
不进行数据淘汰的策略:
noeviction:当redis内存达到设置的最大内存时,不会淘汰数据,会对之后的写操作报错通知禁止写入,此时是可以查询删除修改的,唯独写不了。
进行数据淘汰的策略(分为有过期值和所有数据范围)
有过期值:
volatile-random:在有过期值的数据中随机删除
volatile-ttl:删除离过期时间最短的数据
volatile-lru:删除有过期值且最长时间未使用的数据
volatile-lfu:删除有过期值且使用次数最少的数据
所有数据范围:
allkeys-random:在所有数据范围内随机删除
allkeys-lru:删除在所有数据范围内最长时间未使用的数据
allkeys-lfu:删除在所有数据范围内使用次数最少得数据
延伸→讲一讲过期删除策略?
删除策略为定期删除+惰性删除
所谓的惰性删除就是在每次访问和修改键值对前,reids会调用expireIfNeeded函数去判断键值是否过期,如果发现过期就直接删除键值对并返回过期信息,没过期就正常操作返回数据。
而定期删除就是redis会隔一段时间(默认一百毫秒)去抽取二十个键值对来检查是否过期,过期删除反之不删除,如果发现抽到的20个键中超过25%的键过期了,那会再抽取20个,直到过期比率小于25%。
两者的优缺点如下:
惰性删除就是及时但是占用CPU,定期删除就是CPU花销小但占用内存空间多。
10.Redis的缓存失效会不会立即删除?
不会,Redis 的过期删除策略是选择「惰性删除+定期删除」这两种策略配和使用。
- 惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
- 定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
延伸→为什么不选择过期直接删除呢?非要搞这些策略干嘛?
在过期 key 比较多的情况下,删除过期 key 可能会占用相当一部分 CPU 时间,在内存不紧张但 CPU 时间紧张的情况下,将 CPU 时间用于删除和当前任务无关的过期键上,无疑会对服务器的响应时间和吞吐量造成影响。所以,定时删除策略对 CPU 不友好。