今天你准备了吗——Redis面试题

92 阅读23分钟

redis面试题

1.什么是redis?请说下redis的原理?

redis是基于key-value类型的内存数据库,整个数据库系统加载到内存中进行操作,定期异步的把数据库中的数据刷新到磁盘中进行持久化。因为是纯内存操作,所以redis的读取速度非常快。redis还支持多种数据类型,单个value的最大限制是1G。

优点:

redis支持持久化,可以将内存中的数据保存到磁盘中。持久化的机制包括RDB和AOF,默认使用的aof;

redis支持多种数据类型,五种基本类型:String,List,Set,Zset,Hash。

Bitmaps(位图):布隆过滤器使用BitSet(位图),通过hash运算方式,将我们的数据存储在BitSet中。BitSet是一个很长的0/1序列,每一个序列中只存储0和1,每一个序列只占用1位。

HyperLogLog:通过它可以利用极小的内存空间完成独立数据的统计;只为了计算统计的数量,不需要获取单个数据;

redis支持数据的备份,主从复制;

高可用和分布式: 哨兵机制

缺点:

数据库的容量受到物理内存的限制:不能用作海量数据的高性能读写,一次redis适合在局限在较小数据量的高性能操作和运算上;

2.为什么使用缓存?为什么要用redis?

现在web应用,大部分页面读的比例占比非常大,如果当我们直接读取数据库时,数据库可能回去直接读取磁盘上的操作,这是个相对较慢的过程。如果我们把数据直接写入到内存中或者redis中,让服务器端直接读取redis中的数据,速度会明显提升不少,并且会极大较小数据库的压力。

使用redis我们也要考虑以下几个方面:

1.业务数据是否常用,命中率如何?如果命中率很低,就没必要进行将数据存放到redis中;

2.业务数据是读的多还是写的多?如果是写的多,就没有使用缓存;

3.业务数据的大小如何?如果存放的是几百兆的文件,会给缓存带来很大的压力,没有必要进行redis缓存。

3.使用缓存会出现哪些问题?

1.缓存雪崩:

什么时缓存雪崩: 缓存雪崩是同一时间大量缓存数据失效,导致原本请求redis的请求全部请求到数据库,造成数据库短期内CPU和内存压力激增,严重甚至会造成宕机。

为什么会出现: 缓存数据设置的过期时间是相同的,并且redis将这些数据全部删除。

怎样解决: 给缓存的数据设置过期时间时加一个随机值,这就会大大减少同一时间数据过期数量。redis宕机是很严重的问题,为了防止redis宕机,我们还可以从架构来考虑,保证缓存层的服务高可用:主从架构+集群(哨兵);如果redis真的挂了,我们可以通过本地缓存+限流的方式尽量避免我们数据库被干掉;同时还需要设置redis持久化,保证在redis挂掉后能够快速恢复数据。

2.缓存穿透:

什么时候缓存穿透: 查询一个数据库中不存在的数据,缓存中也不会存在。所以就会绕过缓存,直接查询数据库,查询多了也会将数据库拖垮;

怎样解决:

缓存空对象,如果缓存层不命中的话,仍将对象缓存到缓存中,设置为null,同时设置一个较短的过期时间,让其自动剔除;

布隆过滤器拦截:在缓存层和存储层之间将存在的key用布隆过滤器保存起来,做第一层拦截;当用户发送请求时先判断请求的值是否存在于布隆过滤器中。不存在的话就给前端返回参数错误告诉前端,存在的话才能继续走下面的逻辑。

3.缓存和数据库的双写一致性:

双写一致性是什么: 当更新操作发生时,导致数据发生不一致;同时更新redis和更新数据库的原子性无法得到保证,无轮是先更新数据库还是先更新缓存都有可能失败。

解决办法:

首先通过设置缓存的过期时间,保证缓存和数据库的数据最终一致性。

主要是先更新数据库,再删缓存:

一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生 (1)缓存刚好失效 (2)请求A查询数据库,得一个旧值 (3)请求B将新值写入数据库 (4)请求B删除缓存 (5)请求A将查到的旧值写入缓存

因为数据库的读操作速度远远大于写操作,所以这种情况基本不能发生。

解决办法:

(1)更新数据库数据; (2)缓存因为种种问题删除失败的key (3)将需要删除的key发送至消息队列 (4)自己消费消息,获得需要删除的key (5)继续重试删除操作,直到成功

4.缓存击穿:

缓存击穿是当前key是热点key,如果此时key失效,并发量非常大,在缓存失效的瞬间有大量的线程来访问,造成请求直接打到数据库中,给数据库造成很大的压力。

解决办法:

设置热点key永远不过期;代码实现简单。

互斥锁:此方法只允许一个线程重建缓存,让其他线程等待重建缓存的线程执行完,重新从缓存中获取数据即可。可以很好的保证一致性,降低后端的存储负载。

2.说一说你在项目中redis应用场景?

5大value类型:

String:字符串类型,是最基本的数据类型,字符串类型的数值可以是字符串、数字、甚至是二进制。可以用来进行缓存、计数

List:用来存储多个有序的字符串,可以对列表两端进行pop和push操作。

消息队列:

通过使用lpush+brpop命令可以实现阻塞队列。

通过使用lpush+rpop命令可以实现队列。

通过使用lpush+lpop命令可以实现栈。

Set:不能有重复元素,并且元素时无序的。

Hash:键值本身是个键值对。

Zset:不能有重复成员的特性,元素可以进行排序。 排行榜功能。

redis的使用场景还是基于我们使用场景要使用那种数据结构,对于不同的场景,我们选择不同的数据结构。

而且redis可以针对于服务无状态,比如我们项目中的登陆框架使用的是springOauth2+springSecutity,我们将获取到的token放到redis的RedisTokenStore中,这样我们每次在获取的时候可以直接从redis获取,如果放到内存中,每次重启服务token都会失效。

分布式锁: 在双机情况下对同一个数据的操作需要加上分布式锁。

5.为啥使用单线程?

因为redis是基于内存的操作,cpu不是redis的内存瓶颈,redis的瓶颈可能是机器的内存大小和网络带宽,既然单线程可以实现,那就可以直接使用单线程来使用;在redis4.0版本之后,引入一些多线程的命令: UNLINKFLUSHALL ASYNC

使用单线程的好处: 使用单线程可以带来更好的可维护性,方便开发和调试;加入了多线程,我们就必须要同时引入并发控制来保证在多个线程同时访问数据时程序行为的正确性。而且多线程还会带来上下文切换花费的时间。

使用单线程也能并发处理客户端的请求;(IO多路复用)

redis的性能瓶颈不是cpu;

6.Redis 为什么这么快?

1.纯内存的操作:读取不需要进行磁盘 I/O,所以比传统数据库要快上不少。

2.单线程,无锁竞争:这保证了没有线程的上下文切换,同时也不需要进行加锁控制数据的安全性,不会因为多线程的一些操作降低性能。 3.多路IO复用模型,非阻塞IO:

IO多路复用是一种同步的IO模型,实现一个线程可以监视多个文件句柄。一旦某个文件句柄已经就绪,就能够通 知响应的应用程序进行响应的读写操作。多路是指的网络连接,复用是指同一个线程。

而多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,变成了一次系统调用 + 内核层遍历这些文件描述符。

select 是操作系统提供的系统调用函数,通过它,我们可以把一个文件描述符的数组发给操作系统, 让操作系统去遍历,确定哪个文件描述符可以读写, 然后告诉我们去处理:

首先一个线程不断接受客户端连接,并把 socket 文件描述符放到一个 list 里。

然后,另一个线程不再自己遍历,而是调用 select,将这批文件描述符 list 交给操作系统去遍历。

不过,当 select 函数返回后,用户依然需要遍历刚刚提交给操作系统的 list,只不过,操作系统会将准备就绪的文件描述符做上标识,用户层将不会再有无意义的系统调用开销。

epoll: 内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可。

内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。

内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。

4.高效的数据结构,底层对数据做了大量优化:redis对底层的数据结构和内存占用做了优化,不同长度的字符串使用不同的结构体:redis内部提供了针对不同类型底层具体使用哪种数据结构。

List类型:

.ziplist:当元素个数小于list-max-ziplist-entries 配置(默认512)同时所有的值都小于list-max-ziplist-value配置;

.linkedlist:当无法满足ziplist时,就会使用linkedlist作为内部实现;

7.HyperLogLog 有了解吗?

是一种估算基数的近似最优算法,基数统计:一个集合中不重复的元素算法。利用极小的内存空间完成独立数据的统计。

8.布隆过滤器了解吗?

  布隆过滤器:一种数据结构,是由一串很长的二进制向量组成,可以将其看成一个二进制数组。既然是二进制,那么里面存放的不是0,就是1,但是初始默认值都是0。

使用场景

大数据量的判断是否存在:当布隆过滤器判断这个值不存在,那就一定不存在;判断这个值存在,那么这个值可能存在;

解决缓存穿透:

原理解析:

添加数据:当向容器中添加数据时,会使用多个hash函数对key值进行计算,每次hash计算都会得到一个位置,将该位置的数值由0改为1。

查询数据:只需要将这个新的数据通过上面自定义的几个哈希函数,分别算出各个值,然后看其对应的地方是否都是1,如果存在一个不是1的情况,那么我们可以说,该新数据一定不存在于这个布隆过滤器中。

9.什么是持久化?持久化原理?

redis的数据全部储存在内存中,当服务器发生宕机,数据就会全部丢失,所以为了保证在服务器宕机之后数据不会丢失,采用了持久化机制,将数据同步到磁盘中。

RDB:是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化的过程分为手动触发和自动触发

手动触发:save和 bgsave

save:同步阻塞,在主线程中执行,会导致主线程阻塞

bgsave:异步非阻塞:创建一个子进程,专门用于RDB持久化操作,避免了主线程阻塞,这也是Redis的默认配置

bgsave流程:

1.执行bgsave命令,redis父进程判断当前是否存在正在执行的子进程,如果存在则直接返回;

2.父进程执行fork创建子进程,在该过程中父进程是阻塞的,

3.父进程fork完成后,并不在阻塞父进程,可以继续响应其他命令;

4.子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文进行替换;

5.子进程发送信号给父进程表示完成,父进程更新系统信息;

自动触发:

save 900 1 ;在间隔时间为900s内,发生了一次数据的修改,自动触发RDB操作;

save 300 10;在间隔时间为300s内,发生了10次修改操作,自动触发RDB操作;

save 60 10000 ;在间隔时间60s内,发生了10000次修改操作,自动触发RDB操作;

优缺点:

优点:

  1. 只有一个文件 dump.rdb方便持久化
  2. 性能最大化fork 子进程来完成写操作,让主进程继续处理命令,所以使 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能
  3. 相对于数据集大时,比 AOF 的 启动效率 更高。

缺点:无法做到实时性的持久化、秒级持久化

AOF:以独立日志的方式记录每次写命令,重启时在重新执行AOF文件中的命令达到恢复数据的目的;

开启AOF功能需要设置:appendonly yes,默认文件名:appendonly.aof

AOF的工作流程:

1.所有的写命令都会追加到AOF的缓冲区,

2.AOF缓冲区根据对应的策略向硬盘做同步操作:同步策略包括:

always:命令写入到缓冲区之后,调用系统的fsync命令同步aof文件,fsync完成后线程返回。fsync将阻塞直到写入硬盘完成后,保证数据的持久化

everysec:命令写入到缓冲区之后,调用write命令操作;wirte写入系统缓冲区后直接返回,同步到硬盘中的操作由专门的线程进行调度来完成;(友好) no:命令写入到缓冲区之后,调用write命令操作,不对aof文件做fsync操作,同步操作由操作系统来负责。

3.随着AOF文件越来越大,所以需要进行重写,对文件进行压缩;

4.当redis服务器重启时,可以加载aof文件进行数据恢复;

redis4.0版本支持混合持久性:

将rdb文件内容和增量的AOF日志文件放到一起;当redis服务重启之后,可以先加载rdb的内容,然后再执行增量AOF日志,这样就可以代替之前的全量AOF日志。

10.请说下aof的重写机制?

AOF 日志重写时,依据的是数据库当前的状态,因此可以省略很多过程中产生的指令,只需生成达到当前数据状态需要的写入指令即可。

在重写期间,会将数据变化同时写入到 AOF重写缓冲区,用以记录重写期间发生的数据变更,当重写完成后,再将重写缓冲区中记录的变更追加到重写日志尾部,通过这种方式解决日志数据完整性的问题。

1.执行aof的重写请求;

2.父进程执行fork创建子线程;

3.主进程fork完成后,继续响应其他命令。所有的修改命令依然写入到aof缓冲区并将数据同步到硬盘中,保证原有的AOF的机制正确性;

4.由于父进程依然响应命令,redis使用“AOF重写缓冲区”保存这部分新的数据。

5.子进程根据内存快照,按照命令合并规则写入到新的AOF文件。

6.新AOF文件写入完成后,子进程发送信号给父进程,父进程更新统计信息;

7.父进程把AOF重写缓冲区的数据写入到新的AOF文件;

8.使用新的AOF文件替换老文件;

优缺点:

默认的 everysec 写回策略在保证良好性能的同时,即使发生了异常宕机,最多也只会丢失 1s 的日志数据。

Redis协议格式保存,因此日志具有较高的可读性,更易于对日志进行分析;

缺点:

  1. AOF 文件比 RDB 文件大,且 恢复速度慢
  2. 数据集大 的时候,比 rdb 启动效率低

11.Redis的数据恢复?

  1. 如果只配置 AOF ,重启时加载 AOF 文件恢复数据;
  2. 如果同时配置了 RDB 和 AOF ,启动只加载 AOF 文件恢复数据;
  3. 如果只配置 RDB,启动将加载 dump 文件恢复数据。

12.主从复制了解吗?

主从复制是将一台服务器的数据,复制到其他服务器;前者称为主节点,后者称为子节点,数据的复制是单向的,只能由主节点数据复制到从节点。

主从复制的作用: 数据冗余:主从复制实现了数据的热备份,是持久化的之外的一种数据冗余方式;

故障恢复:当主节点出现问题由从节点提供服务,实现快速的故障恢复。

负载均衡:在主节点提供写的服务,从节点提供读的服务,分担服务器的负载。

高可用基础:主从复制还是哨兵和集群的基础。

主从复制的原理:

当主从服务器建立链接的时候,进行全量同步;全量复制结束后,进行增量复制;

全量复制的流程:

1.slave链接上master后,发送psync命令;

2.master接收到命令后,开始执行bgsave命令生成RDB快照并使用缓存记录以后执行的命令;

3.master 在bgsave执行完之后就会向所有Slava服务器发送快照文件,并在发送期间继续在缓冲区内记录被执行的写命令;

4.slave接收到的RDB文件之后,会进行将数据写入到磁盘中,然后清空所有的旧数据。再将新数据加载到内存中,这个时间还是依靠旧版本的数据进行对外提供服务;

5.master发送完rdb文件后,开始向slave发送缓冲区的数据;

6.slave完成对快照的载入,开始接收命令请求,并执行来自缓冲区的命令请求。

增量复制的流程:

master每执行一个写命令就将写命令发送给slave,slave进行写命令的执行。

如果在传输的过程中出现网络断开的问题,那么该怎么办?

现在redis支持断点续传,只从中断处继续进行复制,而不必重新同步;

1.master会在缓冲区内给每个slave服务器都维护一个同步备份日志,缓存最近一段时间的数据;

2.master和slave都会维护一个复制偏移量和master线程id,每次slave进行跟master进行同步时,都携带这两个参数,当因某种原因slave断开,再次重连时,判断这两个参数都在各自的备份中是否存并有效,如果存在并有效就在之前的同步进度上进行同步,否则就全量同步;

3.master会将本地的同步记录发送给slave,slave进行。

13.redis的过期清除策略和内存淘汰策略?

过期清除策略:定期删除+惰性删除

定期删除:redis默认每100ms会随机抽取一部分设置了过期时间的key,判断这些key是否过期,有过期就删除。

惰性删除:当你在请求某个key时,就会判断这个key是否已经过期,过期就删除。

这时就会出现一个问题,如果某些key在定期删除没有删除掉,又没有去请求这些key,导致这些key一直存储在内存中,这些key越积越多时,就会造成内存的消耗。

内存淘汰策略:

1、volatile-ttl:在设置了过期时间的键值对中,移除即将过期的键值对。

2、volatile-random:在设置了过期时间的键值对中,随机移除某个键值对。

3、volatile-lru:在设置了过期时间的键值对中,移除最近最少使用的键值对。

4、volatile-lfu:在设置了过期时间的键值对中,移除最近最不频繁使用的键值对

5、allkeys-random:在所有键值对中,随机移除某个key。

6、allkeys-lru:在所有的键值对中,移除最近最少使用的键值对。(推荐使用)

7、allkeys-lfu:在所有的键值对中,移除最近最不频繁使用的键值对

8、noeviction:默认策略:不会删除任何数据,拒绝所有写入操作并返回客户端错误信息;

14.哨兵机制是什么?

哨兵的出现是因为在主从复制中,master负责写,slave负责读,当master发生宕机了,需要我们手动的去在slave中找到一个晋升为主节点,并且需要切换客户端的连接数据源。

哨兵是redis高可用实现方式;

作用:

监控:哨兵会不断的监控你的Master和Slave是否是正常的;

通知(Notification):哨兵可以将故障转移的结果发送给客户端。

自动故障转移:当一个Master不能正常工作时,哨兵会进行自动故障迁移操作,将失效Master的其中一个Slave升级为新的Master,并让失效Master的其他Slave改为复制新的Master。

如何判断master是否是下线?

1.每个sentinel会向所有的master和slave发送ping命令,作为心跳检测。每个master会回复ping命令的时间如果超过阈值(30s),则这个master会被sentinel标记为主观下线;

2.这个哨兵节点会向其他的哨兵节点发送消息来确定该master是否下线;每个sentinel节点都会发送自己判断这个master节点是否已经下线;

3.sentinel收到回复后,判断同意下线的个数是否大于quorum ,如果大于则被标记为客观下线。

如何从slave节点中选出master?

① 选择优先级最高的节点,通过sentinel配置文件中的replica-priority配置项,这个参数越小,表示优先级越高

② 如果第一步中的优先级相同,选择offset最大的,offset表示主节点向从节点同步数据的偏移量,越大表示同步的数据越多

③ 如果第二步offset也相同,选择run id较小的

15.能解释一下什么redis分布式锁?

分布式锁:保证这个任务在同一时刻只能被多个进程中的某个进程执行。

特性:

1)互斥性

任意时刻只有一个客户端可以获取锁。这个很容易理解,所有的系统中只能有一个系统持有锁。

2)防死锁

假如一个客户端在持有锁的时候崩溃了,没有释放锁,那么别的客户端无法获得锁,则会造成死锁,所以要保证客户端一定会释放锁。Redis中我们可以设置锁的过期时间来保证不会发生死锁。

3)持锁人解锁

解铃还须系铃人,加锁和解锁必须是同一个客户端,客户端A的线程加的锁必须是客户端A的线程来解锁,客户端不能解开别的客户端的锁。

4)可重入

当一个客户端获取对象锁之后,这个客户端可以再次获取这个对象上的锁。

数据库:

基于数据库的增删改查:类的全路径名+方法名,时间戳等字段。当需要锁住某个方法时,往该表中插入一条相关的记录。类的全路径名+方法名是有唯一性约束的,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就认为操作成功的那个线程获得了该方法的锁,

基于排它锁: 在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。

redis:

实现方式1:setnx key value ex seconds nx

发送命令,Redis返回的结果是否为1,结果是1就表示setnx成功了,那本次就获得锁了,可以继续执行业务逻辑;如果结果是0,则表示setnx失败了,那本次就没有获取到锁,可以通过循环的方式一直尝试获取锁,直至其他客户端释放了锁(delete掉key)后,就可以正常执行setnx命令获取到锁。

实现方式2:结合spring中的框架

Boolean lockStatus = redisTemplate.opsForValue().setIfAbsent(RedisKeyPrefix.RECORD_OUT_PUT, new Date(), 1, TimeUnit.MINUTES);

16.MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?

保证Redis 中的 20w 数据都是热点数据说明是被频繁访问的数据,并且要保证Redis的内存能够存放20w数据,要计算出Redis内存的大小。

  1. 保留热点数据:对于保留 Redis 热点数据来说,我们可以使用 Redis 的内存淘汰策略来实现,可以使用allkeys-lru淘汰策略,该淘汰策略是从 Redis 的数据中挑选最近最少使用的数据删除,这样频繁被访问的数据就可以保留下来了。
  2. 在redis.conf配置文件中,可以对最大内存进行设置,单位为bytes:当使用的内存到达指定的限制时,Redis会根据内存淘汰策略删除键,以释放空间。

\