Redis面试相关-01(基础篇)
文章目录引言1、Redis是用来干嘛的?2、Redis为什么快?3、Redis的数据类型4、Redis的过期策略内存淘汰机制手写一下LRU代码的实现小结
文章目录
引言
现在大大小小的项目基本都会使用到redis,无论是用于登录还是xxx等场景,相信大家都会在简历中写进这个缓存工具。
接下来分享的是我在面试时遇到过的各种关于redis的问题以及自己的见解,如有遗漏或者理解有误的地方欢迎大家留言指出。
1、Redis是用来干嘛的?
这是一个很简单但也是很常见的问题,有的面试官会以此为开头,开始对你redis知识储备的考验。
我一般是从这三个角度来考虑和解答的:
项目中缓存是怎么使用的?
用缓存,主要是用来解决两个项目常见问题,高性能和高并发不使用缓存会有什么后果?
高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。如果使用了缓存会不会有什么问题
(1)缓存与数据库双写不一致
(2)缓存雪崩
(3)缓存穿透
(4)缓存并发竞争
2、Redis为什么快?
当我从上面三个角度描述完问题后,面试官就会顺着我的思路开始问我。
既然我说使用缓存快,那么就讲讲为什么Redis效率高,速度快?
首先我会先说redis是单线程模型的,比较多线程而言,避免了多线程的频繁上下文切换问题[1]。
然后详细描述一下这个单线程模型的结构:
redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
多个 socket
IO 多路复用程序
文件事件分派器
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。这个过程是纯内存且非阻塞的操作。
3、Redis的数据类型
这是基础知识的考察了,一般还会同时问我分别在哪些场景下比较合适?
- string:
这是最基本的类型了,没啥可说的,就是普通的set和get,做简单的kv缓存 - hash:
hash类的数据结构,是一种类似map的结构,主要是用来存放一些对象,把一些简单的对象给缓存起来,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 - list:
有序列表。
场景1:比如可以通过list存储一些列表型的数据结构,类似粉丝列表了、文章的评论列表了之类的东西。
比如可以通过lrange命令,就是从某个元素开始读取多少个元素,可以基于list实现分页查询,这个很棒的一个功能,基于redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。
场景2:比如可以搞个简单的消息队列,从list头怼进去,从list尾巴那里弄出来。
- set:
无序集合,自动去重
直接基于set将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于jvm内存里的HashSet进行去重,但是如果你的某个系统部署在多台机器上呢?得基于redis进行全局的set去重。
场景1:可以基于set玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?对吧把两个大v的粉丝都放在两个set中,对两个set做交集。
- sorted set:
排序集合,去重但是可以排序,写进去的时候给一个分数,自动根据分数排序,这个可以玩儿很多的花样,最大的特点是有个分数可以自定义排序规则。
场景1:做排行榜,将每个用户以及其对应的什么分数写入进去,zadd board score username,接着zrevrange board 0 99,就可以获取排名前100的用户;zrank board username,可以看到用户在排行榜里的排名。
4、Redis的过期策略
过期策略是定期删除和惰性删除。
定期删除就是redis默认每隔100ms就随机抽取一些设置了过期时间的key,检查是否过期,如果过期就删除。但是如果你设置了过期时间的key很多,CPU负载就会很高基本都消耗在检查过期key上,所以要注意控制数量。
惰性删除是指当我去查询某个key的时候,如果这个key有设置过期时间,那么redis就会先去判断该key是否过期,如果过期就不会返回任何东西。
以上两种方式结合保证过期key全部被干掉。场景1:有时候设置了过期时间,然而内存占用依然很高,就是因为定期删除并删除所有过期key导致。场景2:当定期删除漏掉了很多key没有删掉,同时也没有触发惰性删除导致大量过期key堆积在内存里,导致redis内存快耗尽了。这时候就要走内存淘汰机制。
内存淘汰机制
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的key给干掉啊
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
手写一下LRU代码的实现
当你的面试官额外带了纸和笔,就做好手写代码的准备吧
我是利用已有的jdk数据结构实现一个java版的LRU,使用LinkedHashMap来实现。
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
// 这里就是传递进来最多能缓存多少数据
public LRUCache(int cacheSize) {
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
// 这块就是设置一个hashmap的初始大小,同时最后一个true指的是让linkedhashmap按照访问顺序来进行排序,最近访问的放在头,最老访问的就在尾
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
// 这个意思就是说当map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据
return size() > CACHE_SIZE;
}
}
小结
自此,关于Redis基础知识方面的问题就问的差不多了,接下来就会结合项目来考察有关Redis的集群架构,哨兵架构,持久化等相关方面的知识了,这些我放在下一篇文章中进行讲述。
[1]: 多线程的上下文切换是指在一个进程中,一个线程被暂停或者说被剥夺使用权,另一个线程开始或者继续运行的过程就叫线程上下文切换。上下文是指在切入切出的时候操作系统要保存和恢复相应的进度信息。而频繁的上下文切换就会有保存和恢复上下文的时间开销以及线程调度器进行线程调度的开销和处理器高速缓存重新加载的开销。而且也可能会产生死锁,活锁之类的故障。