Redis常见面试题

房东的猫 提交于 2019-12-31 23:44:52

1、什么是Redis?

  Redis 是一个基于内存的高性能key-value数据库,不过在系统中一般充当高速缓存的角色。

 

2、为什么Redis需要把所有数据放到内存中? 

  访问内存的速度远高于访问硬盘的速度,如果不将数据放在内存中,磁盘I/O速度将严重影响Redis的性能。在内存越来越便宜的今天,Redis将会越来越受欢迎。

 

3、对Redis的访问为什么是单进程单线程的

  Redis利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销。Redis的开发者认为Redis的性能瓶颈不在CPU,而是网络等因素。所以采取单线程的方式是最快的。单线程能够有效避免CPU切换的开销,另外I/O多路复用技术也有效的提升了访问速度。

 

4、Reids的特点(Redis的好处)

  1. 访问速度快。原因归结为三个方面:一是数据存储在内存中;二是对数据的访问是单线程操作,避免了不必要的IO开销;三是底层的数据结构合理,类似于hashMap,存取的时间复杂度为O(1)。
  2. 拥有丰富数据类型。支持string,list,hash ,set,sorted set五种类型
  3. 支持事务。拥有与传统数据库不同的独特的事务特性。在Redis中,一个事务中所有命令操作具有原子性
  4. 可以持久化缓存数据。拥有AOF和RDB两种持久化方式,保证系统重启数据不丢失
  5. 拥有成熟的可扩展,高可用的分布式解决方案。像Redis Sentienl和Redis Cluster等
  6. 外部拥有众多的API类库可供使用。大部分编程语言都可以轻松的和Redis进行交互,如Java中常用是jedis和redisson
  7. 用途多样化。Redis可以作为缓存,消息队列,分布式锁,数据库等,应用范围及其广泛。

 

5、Redis与Memcached相比有哪些优势?

  1. Redis的速度比Memcached快很多
  2. Memcached所有的值均是简单的字符串,Redis作为其替代者,支持更为丰富的数据类型
  3. Redis可以持久化缓存数据,Memcached没有持久化的功能。
  4. Redis可以将部分数据放到虚拟内存中,一定程度上可以突破内存大小的限制存放数据;而Memcached完全受限于内存
  5. 使用底层模型不同。它们之间底层实现方式以及与客户端之间通信的应用协议不一样。Redis直接自己构建了VM 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

  

6、Redis持久化的几种方式

  1 · RDB(也被称快照方式)

  可以通过自动或手动的方式,将Redis在内存中的数据持久化到磁盘中。这个过程会耗费一定的资源,用来生成dump.rdb文件(也被称作为快照文件),所以一般仅在从库中进行。生成新的dump.rdb文件会自动覆盖旧的文件,使用完整的该文件,就可以还原出Redis中所有的数据。

  2 · AOF(append-only-file)

  快照模式并不十分健壮,当系统意外停止,或者无意中Redis被kill掉,最后写入Redis的数据就会丢失。比如我们设置每分钟生成一次dump.rdb文件,如果生成之后过了30s系统down机,那么就会丢失这30s中内的数据。

  这对某些应用也许不是大问题,但对于要求高可靠性的应用来说,RDB就不是一个最佳的选择。AOF是另一种选择。我们可以在配置文件中启用AOF模式。

  我们可以配置成每隔1s或者0.5s就备份一次数据。因为AOF是在原有的基础上附加数据,所以相对而言不是那么消耗性能。当然,这个过程最好也应该在从库上进行。

  3 · 虚拟内存方式

  这个东西官方都不建议使用,没啥好谈的。只要知道有这么个东西,知道当value很大(比如几百M)的时候,可以把key放在内存中,value放在虚拟内存(也就是磁盘)中即可

 

7、Redis的缓存失效策略(缓存过期策略)

  Redis的缓存过期策略整体分为两大类,分别是"过期策略"和"内存淘汰机制"。

  1、过期策略

  ① 定期删除:每隔100ms,会随机抽取一些key做检查,如果超出了设置的过期时间,就将其删除。

  ② 惰性删除:当使用到某key的时候,先检查其是否到期,如到期了则删除。

  这种策略的缺点就是会有部分无用的缓存逃脱死亡的命运。如运气较好的缓存,它在定期删除中一直没有被随机到;另外没有设置过期时间的缓存也不会被删除。不过在数据量较小的情况下,还是很好用的。

  2、内存淘汰机制

  Redis内置了八种内存淘汰机制供我们选择(其中有两种是4.0后新增)。较为常用的是allkeys-lru策略。在所有的key中,挑选出最近最少使用的淘汰。这种方式是针对所有key的,弥补了过期策略的不足。

 

8、怎么理解Redis事务?

  1. Redis中的事务是首先将事务中所有的命令放入队列中,当输入exec 命令后依次执行。
  2. 事务执行过程中如果某个命令执行失败,对其余命令不影响,其余命令会继续依次执行。
  3. 整个事务的执行具有原子性,在事务执行命令的过程(输入exec 命令后)中,不会有有别的命令插入进来。

 

9、Redis事务相关的命令有哪几个?

命令作用
MULTI 开启事务
EXEC 执行事务
DISCARD 放弃事务,一般命令输入失败后使用
WATCH 监视在事务开始前某些key的值是否发生了变化
UNWATCH 放弃对所有已监视key的监视

 

10、WATCH命令和基于CAS的乐观锁是如何使用的

  在Redis的事务中,WATCH 命令可用于提供CAS(check-and-set)功能。

  假设我们通过WATCH 命令在事务执行之前监控了多个Keys,倘若在WATCH 命令执行之后有任何Key的值发生了变化,EXEC 命令执行的事务都将被放弃,同时返回"Null multi-bulk"应答以通知调用者事务执行失败。

  这个问题实际上是如何解决Redis并发竞争问题的其中一个解决方案而已。

 

11、影响生存时间的一些操作

  ① 生存时间可以通过使用 DEL 命令来删除整个 key 来移除,或者被 SET 和 GETSET 命令覆盖原来的数据。也就是说,修改key对应的value和使用另外相同的key和value来覆盖以后,当前数据的生存时间不同; 

  ② 比如说,对一个 key 执行INCR命令,对一个列表进行LPUSH命令,或者对一个哈希表执行HSET命令,这类操作都不会修改 key 本身的生存时间。另一方面,如果使用RENAME对一个 key 进行改名,那么改名后的 key的生存时间和改名前一样;

  ③ RENAME命令的另一种可能是,尝试将一个带生存时间的 key 改名成另一个带生存时间的 another_key ,这时旧的 another_key (以及它的生存时间)会被删除,然后旧的 key 会改名为 another_key ,因此新的 another_key 的生存时间也和原本的 key 一样;

  ④ 使用PERSIST命令可以在不删除 key 的情况下,移除 key 的生存时间,让 key 重新成为一个persistent key

  ⑤ 可以对一个已经带有生存时间的 key 执行EXPIRE命令,新指定的生存时间会取代旧的生存时间。过期时间的精度已经被控制在1ms之内;

  ⑥ 在 Redis 中,允许用户设置最大使用内存大小,server.maxmemory默认为0,没有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置。redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略,使用频率高的key一般不容易被淘汰。

 

12、Redis支持的Java客户端都有哪些?官方推荐用哪个?

  Redisson、Jedis、Lettuce等等,官方推荐使用Redisson。不过平时Jedis也挺常用的

 

13、Redis和Redisson有什么关系?

  Redisson项目在github上的自我介绍:具有内存数据网格特性的Redis Java客户端。讲白了就是让我们能够在Java代码中操作Redis的一个类库,提供了一个供我们在Java中操作Redis的API,类似与jdbc和数据库之间的关系。

 

14、Jedis与Redisson对比有什么优缺点?

  jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;

  Redisson实现了分布式和可扩展的Java数据结构,和jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

 

15、Redis如何设置密码及验证密码?

设置密码:config set requirepass 123456

输入密码:auth 123456

 
 

 

16、Redis key的过期时间和永久有效分别怎么设置?

设置过期时间:EXPIRE key time

设置永久有效:PERSIST key

127.0.0.1:6379> config set requirepass 123   // 设置密码为123
OK
127.0.0.1:6379> auth 123    // 输入密码123登录
OK
127.0.0.1:6379> auth 1234   // 输入错误,登录失败
(error) ERR invalid password

  

17、怎么测试Redis的连通性?

  输入ping命令,返回pong表示连通。

127.0.0.1:6379> ping
PONG

 

18、Redis中的管道有什么用?

  常规的Redis请求是客户端发送一个请求,Redis服务器会回复一个响应。如果客户端需要连续请求三个key的值,那么需要连续请求3次Redis服务器。这就表示需要三次网络开销(请求+响应视为一次)。特别是如果Redis服务器和客户端在地域上距离较远的话,这种开销就更加明显了。

  为了减少这种开销,Redis管道就诞生了。我们可以将三次请求一次性的发送给Redis服务端,然后Redis服务端也会一次性的进行相应。这样就大大减少了网络开销,从而提高了Redis服务器的性能。

 

19、Redis的并发竞争问题如何解决?

  问题的背景:

  比如现需要给Redis中某个值 + 1,Redis没有像传统数据库一样的sql语句,无法像 value = value + 1这样自增,所以只能先读取该值,+1后再set回去。由于不是原子性操作,所以就产生了并发问题。

  比如value初始值为1,两个线程同时读取到value=1,然后各自+1再将运算结果赋值给value。这样操作两个线程都会把2赋值给value,得到value=2。但实际上正确结果应该是value=3。

  解决方案:

  1-当仅需要对值进行+1操作的时候,可以使用Redis提供的incr 命令,它的效果就和传统数据库中执行sql一样,具有原子性,不会有并发问题,如果不是+1的话(比如+2,+3等等),那就无能为力了。

  2-使用Redis事务的方式解决。先使用watch命令监视该资源,然后再在事务中执行读取,增加和赋值操作。这样如果被监视的key在这过程中已被其他线程改变了,那么该事务是会执行失败。这个相当于是乐观锁的概念。

  3-在客户端我们可以使用"锁"来控制并发。单机情况下使用jdk提供的锁即可(如synchronized或 lock)。在分布式情况下,可以使用分布式锁来完成该功能。

  4-利用redis的setnx实现内置的锁。

 

20、Redis常见性能问题和解决方案

  1. 尽量不要使用master(主节点)生成堆内存快照(生成dump.rdb文件)。save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以这种操作可以让slave去做
  2. 尽量不要在master上做AOF持久化。如果数据比较关键,希望保证较高的一致性,可以让某个Slave开启AOF策略备份数据,策略为每秒同步一次
  3. 1. Redis主从复制的性能问题。主从之间尽量不要使用图结构连接(一个master直连多个slave),而应使用链式结构(master—>slave1—>slave2—>slave3),这样如果master挂掉,只要将slave1变为master即可。另外为了主从复制的速度和连接的稳定性,slave和master最好在同一个局域网内

 

21、Redis集群如何选择数据库?

  Redis集群目前无法做数据库选择,默认在0数据库。

  只有单机的情况下可以通过select 命令选择数据库,一个Redis实例默认有16个数据库。

 

22、 Mysql有2000w数据,Redis中只存20w数据,如何保证Redis中的数据都是热点数据

  Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略(回收策略)。让被访问频率高的数据留下即可。所以这个问题相当于是问Redis中数据淘汰策略的具体应用。

  首先将Redis的内存设置为一个合适的大小(保证可以存放20w数据),当已使用内存大于该值时就会自动进行内存淘汰。该值可由maxmemory参数进行设置。然后设置一个合适的内存淘汰策略,比如allkeys-lru(淘汰最近最少使用的数据)策略。这样随着时间的推移,内存中剩下的数据自然就是热点数据了。

  

23、Redis集群的主从复制模型是怎样的?

  为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品(存疑,虽然Redis中文官网上写了N-1,但是完全看不出为什么是N-1,另外 N是什么也不知道)。

  Redis中一个主库可以同时连接多个从库,所以我们可以配置多台Redis的从库(slave)。master会不停的将自己的信息同步给slave,让slave和自己的信息保持一致。这样当master服务不可用时,我们可以将其中一个slave变成master。不过这个过程同样需要一些时间(因为不是自动的,需要人工介入)。

  另外作为slave是不可以写入数据的,但是我们可以从slave读取数据,因为它和主库的数据一致(同步机制),这样可以减轻master读的压力。不过写的操作还是只能落到master上,无法被slave分担。

24、Redis集群会有写操作丢失吗?为什么?

  Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。

  因为Redis的主从复制使用的是异步的方式,另外持久化(AOF)一般也是异步的方式,所以如果在数据写入主节点(master)之后,主节点同步到从节点(slave)之前主节点down掉了,那么这次写入的数据就会丢失。

 

25、说说Redis哈希槽的概念?

  这个概念是在Redis Cluster集群中使用的。Redis集群没有使用一致性hash,而是引入了哈希槽的概念。Redis集群共有16384(214)个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分哈希槽。

  哈希槽是按照主节点(master)来平均分配的,从节点不参与分配。比如有A,B,C三个主节点,那么它们每个节点负责的哈希槽就应该是如下情况:

  节点A覆盖:0-5460 节点B覆盖:5461-10922 节点C覆盖:10923-16383

  这样一共是16384个哈希槽。假设有个key通过CRC16校验后对16384取模得到25,那么很明显它应该存储到节点A上。

 

26、Redis集群方案什么情况下会导致整个集群不可用?

  Redis Cluster集群当某个主节点(master)及其所有的从节点(slave)都不可用的情况下,那么整个集群就不可用了。

  因为Redis Cluster集群是会根据主节点的个数来分配哈希槽的,如果某个主节点及其从节点全部丢失,那么就无法分配哈希槽了。

  假设有A,B,C三个主节点的集群,在没有复制模型的情况下,如果节点B失败了,那么整个集群就会以为缺少5501-11000这个范围的槽而不可用。

 

27、Redis集群最大节点个数是多少?

  16384(214)个。

  因为Redis Cluster集群是按照主节点个数来分配哈希槽的。共有16384个哈希槽供分配。为保证每个主节点至少要分配一个哈希槽,所以最大的节点个数就是16384个。

 

28、Redis集群方案应该怎么做?都有哪些方案?

  1.codis。

  目前用的最多的集群方案,基本和twemproxy一致的效果,但它支持在节点数量改变情况下,旧节点数据可恢复到新hash节点。

  2.Redis Cluster

  Redis3.0自带的集群,特点在于他的分布式算法不是一致性hash,而是hash槽的概念,以及自身支持主从复制。具体可以看官方文档介绍。

  3.在业务代码层实现

  起几个毫无关联的Redis实例,在代码层,对key 进行hash计算,然后去对应的Redis实例操作数据。这种方式对hash层代码要求比较高。考虑部分包括:节点失效后的替代算法方案,数据震荡后的自动脚本恢复,实例的监控,等等。

 

29、Redis如何做内存优化?

  尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。

  比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面。

 

30、Redis回收进程如何工作的?

  一个客户端运行了新的命令,添加了新的数据。Redis检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。

  Redis的内存会不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。

 

31、Redis 在实际应用中的场景  

  1、会话缓存(Session Cache)

  最常用的一种使用Redis的情景就是会话缓存(session cache)。用Redis缓存会话比使用其他存储方式(如Memcached)缓存更好的地方在于Redis提供持久化。比如用户的购物车信息就很适合使用Redis进行存储。

  2、全页缓存(FPC)

  除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP本地FPC。

  3、队列

  Reids在内存存储引擎领域的一大优点是提供 List 和 Set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如java)对 Queue 的 push/pop 操作。不过生产上更多的可能使用kafka之类的队列。

  4、排行榜/计数器

  Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。

 

32、请用Redis和任意语言实现一段恶意登录保护的代码,限制1小时内每用户Id最多只能登录5次。具体登录函数或功能用空函数即可,不用详细写出。

1、设计:

  判断一小时内最多只登陆5次的依据应该是距离现在1h的时间段内最多只有5次登录。

  如下图所示,第一种情况中第6次登录是符合要求的,因为第6次登录时1h内仅登录了4次,第1次和第2次的登录相较于第6次已经在1h之外了,属于过期数据,理应删除;

第二种情况中第6次登录的时候1h内共登录了六次,很明显第6次登录违反规则,不应登录成功。

  所以,想要判断当前登录是否合法,应该先找出历史登录记录中所有距离现在不超过1h的数据。如果这些数据<=4条,那么当前登录才合法,否则当前登录就不合法。

2、 代码逻辑:

  ① 使用用户的唯一标识做key,比如常用的userId。数据类型选择List类型,当用户登录的时候,我们可以将当前登录时间存储到该List中。此时List的大小即用户当前已经登录的次数(如上图所示的样子);

  ② 将List中超过1h的时间数据直接删除,因为已经失效。过滤留下1h之内的数据,判断这部分数据的条数是否小于5。当其小于5的时候才允许用户登录。

  ③ 注意客户端(Java代码)应该要使用分布式锁来保证数据一致性(单机情况下使用线程锁即可),避免Redis中的List因为并发出现问题。比如连续快速登录两次,两次请求落在不同的服务器(Java程序运行的服务器)上。每台服务器在读取List的时候大小都是4,然后同时写入,导致List中出在现六条记录。

  ④ 另外也可以使用Redis的事务来处理。在操作前使用watch命令监视该资源,如果两次请求发送watch命令的时候List的大小都是4,其中一个请求修改了该List,那么第二个请求的事务会执行失败;watch命令监视List的时候一个是5,一个是4,那么本身就没有问题。

3、示例代码(为了简便起见,下列代码没有添加分布式锁):
/** 传入userId。返回true-允许登录 false-不允许登录
 */
public static boolean userLogin(String userId) {
    // 连接Redis,正式使用改行需要封装,不应出现在这
    Jedis jedis = new Jedis("localhost");
    Date nowTime = new Date();  // 获取当前时间
    // 取出key="userId的List,判断其中的数据个数,按照此代码逻辑,不会超过5个元素,所以这么写
    List<String> recordList = jedis.lrange(userId,0L, 5L) ;
    // 移除距离现在超过1h的数据(移除无效数据),然后得到剩下的数据
    List<String> oneHoursList = recordList.stream().filter((String time) -> {
        try {
            long timeDiff = nowTime.getTime() - stringToDatePattern(time).getTime();
            if(timeDiff  > 1000 * 60 * 60) jedis.lrem(userId, 1, time); //删redis中无效数据
            else return true;  // 过滤留下recordList中的有效数据
        } catch (ParseException e) {
            System.out.println("判断时间大小出现异常,异常信息" + e);
        }
        return false;
    }).collect(Collectors.toList());
    // 判断1h内已经登录了几次
    if(oneHoursList.size() < 5) { // 可以登录
        jedis.lpush(userId, dateToStringByPattern(nowTime)) ;  // 放入最新登录时间
        System.out.println("允许登录,代码略----");
        return true;
    }
    System.out.println("禁止登录,代码略----");
    return false;
}
​
/** 将Date类型按照指定的格式转成String输出
 */
private static String dateToStringByPattern(Date date) {
    String pattern = "yyyyMMddHHmmss";
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
    return simpleDateFormat.format(date) ; //  格式化后的时间字符串
}
​
/** 将String类型的时间转成Date
 */
private static Date stringToDatePattern(String date) throws ParseException {
    String pattern = "yyyyMMddHHmmss";
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
    return simpleDateFormat.parse(date) ; //  格式化后的时间字符串
}

  

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!