面试官:让我看看你的Redis功力如何

以下是本文精心挑选的15道Redis面试题。

1、为什么要使用Redis做缓存?

主要是Redis的功能强大。

相较于其他缓存产品,Redis主要具备以下几个优势:

  1. 数据结构丰富:Redis支持多种数据类型,包括字符串、哈希、列表、集合、有序集合等。而像其他缓存产品,比如Memcached,只支持简单的key-value数据结构。

  2. 持久化和可靠性:虽然作为一个缓存产品,Redis为防止数据丢失也支持将数据持久化到磁盘。相比之下,Memcache不支持持久化,

  3. 数据淘汰:Redis提供了丰富的数据淘汰策略和过期时间设置,开发者可以更加灵活地管理缓存数据。而Memcache在这方面的支持相对有限。

  4. 实现其他功能:借助Redis可以实现消息队列、分布式锁、布隆过滤器等其他功能。

  5. 分布式与集群:Redis的分布式部署和集群功能可以方便地构建大规模、高可用的缓存集群。而Memcache需要通过一些手段去实现。

2、为什么Redis单线程模型效率也能那么高?

首先,Redis使用了高度优化的数据结构和算法,比如跳跃表、压缩列表,在访问速度上进行了优化提升了性能。

其次,单线程避免了多线程中常见的上下文切换问题,减少了资源开销,专注干活。

另外,Redis使用了事件驱动的非阻塞IO机制,这意味着Redis能够在等待数据IO时不会阻塞主线程。(主线程负责执行命令)

3、Redis常见数据结构以及使用场景?

以下是Redis的五种主要数据结构及其使用场景:

  1. 字符串(String)

    • 使用场景:存储简单的键值对,如缓存数据、计数器、分布式锁等。

    • 案例:缓存用户信息,如 SET user:1001 "{ 'id': 1001, 'name': 'John Doe', 'age': 30 }"

  2. 哈希(Hash)

    • 使用场景:存储对象,每个对象都有多个字段,适合存储结构化数据。

    • 案例:存储用户信息,如 HSET user:1001 id 1001HSET user:1001 name "John Doe"HSET user:1001 age 30

  3. 列表(List)

    • 使用场景:适合存储有序集合,常用于实现队列、栈等结构。

    • 应用场景:例如,使用列表实现消息队列,用于存储待处理的消息。

  4. 集合(Set)

    • 使用场景:无序集合,可以用于实现交集、并集、差集等操作,常用于去重场景。

    • 案例:存储用户关注的话题标签,利用集合的自动去重特性,避免重复存储。

  5. 有序集合(Zset)

    • 使用场景:与集合类似,但元素是有序的,通过分数进行排序,可以用于实现排行榜等功能。

    • 案例:存储游戏玩家的分数排行榜,根据分数高低进行排序。

  6. HyperLogLog

    • 使用场景:HyperLogLog主要用于进行大规模数据去重或数据集基数估计。

    • 案例:使用HyperLogLog满足UV统计的需求,同时可以节约存储空间。

  7. Geo

    • 使用场景:Geo是Redis中用于地理位置相关的功能的数据结构。

    • 案例:实现附近的人或者地点功能,如找到附近的餐厅、酒店、商店等。

  8. BloomFilter

    • 使用场景:不需要存储数据本身的情况下,判断一个元素是否存在于某个集合中。

    • 案例:使用BloomFilter解决缓存穿透问题。

4、Redis的数据结构是如何组织的?

为了实现从键到值的快速访问,Redis 使用了一个全局哈希表来保存所有键值对。

哈希表的最大好处很明显,可以用 O(1) 的时间复杂度来快速查找到键值对。

5、pipeline有什么好处,为什么要用 pipeline?

Redis客户端执行一条命令需要4个步骤:

  1. 发送命令

  2. 命令排队

  3. 命令执行

  4. 返回结果。

其中1和4花费的时间称为Round Trip Time (RTT,往返时间),也就是数据在网络上传输的时间,占用了绝大多的时间。所以才会有Redis性能瓶颈是网络这样的说法。

Pipeline机制能改善上面这类问题。在有需要的时候,客户端可以通过Pipeline一次性发送一组Redis命令,随后Redis再将这组命令的执行结果按顺序返回给客户端。这种方式可以减少网络上传输的时间,从而提高性能。

图片

非Pipeline和Pipeline执行10000次set操作的效果,在执行时间上的比对如下

图片

6、Redis官方为什么不提供 Windows版本?

相比于Windows,Linux/Unix系统在稳定性、并发性上有一定优势,更适合Redis这种高性能数据库系统。提供Windows版本会消耗较多的资源。

7、Redis 持久化方式有哪些?有什么区别?

Redis 提供两种持久化机制 RDB (Redis DataBase)和 AOF(Append-Only File)

  • RDB 是 Redis 默认的持久化方式。会在某个时间点将内存中的数据以二进制格式写入到磁盘的 RDB 文件中。

  • AOF 是将 Redis 的所有写操作(如 set、del 等)以日志的形式追加到文件中。

两者的优缺点也显而易见。

由于RDB是定时快照,所以当意外宕机后,就会丢失最后一次持久化之后的数据。而AOF以日志的形式追加到文件中,只会丢失最后一次的写操作数据,AOF数据安全性较高。也正是因为AOF会把所有的写操作记录下来,所以在重启恢复数据时会执行所有的写操作,数据恢复速度比RDB慢

8、什么是Redis事务?原理是什么?

Redis 中的事务是一组命令的集合,将一组需要一起执行的命令放到multi和exec两个命令之间。multi 命令代表事务开始,exec命令代表事务结束。它可以保证一次执行多个命令,每个事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行。

但是要注意Redis的事务功能很弱。在事务回滚机制上,Redis只能对基本的语法错误进行判断

当语法命令错误时,会造成整个事务无法执行,事务内的操作都没有执行。

而当命令错误时,虽然有异常提示,但是事务会执行成功。

9、Redis6.0为什么要引入多线程?

Redis 6.0引入多线程的主要原因是为了解决网络IO的性能瓶颈

传统的单线程模型在处理大量网络请求时,只能串行处理,无法充分利用多核CPU的性能。所以,Redis 6.0引入了多线程,分别是主线程和IO线程。

主线程负责接收这些连接请求并分发给IO线程,IO线程负责读取和解析请求数据,随后将解析出的命令传递给主线程,由主线程负责执行这些命令。

所以,引入多线程主要是为了并行处理网络IO,命令执行仍然是单线程的

10、如何在100个亿URL中快速判断某URL是否存在?

这个问题可以移步至《面试官:如何在海量数据中快速检测某个数据

11、什么是渐进式rehash?

渐进式rehash是Redis中一种用于对hash表进行扩容和缩容的操作方法。

通常在对hash表进行扩容时,需要一下几个步骤:

  1. 创建一个新的hash表,大小通常是原始hash表的两倍。

  2. 将原始hash表中的数据迁移到新hash表中。

这中间会存在一个问题:如果要一次性把哈希表中的数据都迁移完,会造成 Redis 线程阻塞(在迁移期间要保证数据一致性,所以写操作会阻塞)。

为了避免阻塞,Redis在扩容时是这样操作的:

  1. 创建一个新的hash表,大小通常是原始hash表的两倍。

  2. 每次迁移一个槽位的数据。

  3. 新写入的数据直接存储在新hash表

这样的话,就避免了一次性、集中式地完成rehash动作导致的长时间阻塞,影响用户体验。而在此期间,客户端访问数据时,会同时在两个hash表中查找数据,不会存在因迁移而导致数据不一致问题。

12、Redis有哪些的过期策略?

Redis的过期策略主要包括以下几种:

  1. 立即删除:当键的过期时间到达时,Redis会立即删除该键。但是,如果同一时间有大量键过期,可能会导致Redis线程过于繁忙,从而影响读写指令的处理速度。

  2. 惰性删除:当客户端访问一个已经过期的键时,Redis才会删除该键。如果过期键一直不被访问,那么这些键就会一直占用内存。

  3. 定期删除:Redis定时检查数据库中的过期键,通过随机抽样的方式来删除过期键。平衡立即删除和惰性删除带来的CPU资源或内存空间问题。

13、Redis有哪些的淘汰策略?

Redis的淘汰策略主要有以下几种:

  1. LRU(Least Recently Used)算法:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的键。

  2. LFU(Least Frequently Used)算法:当内存不足以容纳新写入数据时,在键空间中,移除最不常用的键。

  3. FIFO(First In First Out)算法:最早放入缓存的数据最先被删除。

  4. Random算法:随机移除某个键。

当涉及到设置了过期时间的键时,还有以下策略:

  1. volatile-lru:从设置了过期时间的键中选择最近最少使用的键淘汰。

  2. volatile-lfu:从设置了过期时间的键中选择最不常用的键淘汰。

  3. volatile-random:从设置了过期时间的键中随机选择键淘汰。

  4. volatile-ttl:从设置了过期时间的键中选择离过期时间最近的键淘汰。

14、什么是BigKey?

BigKey是指在Redis中,某个key对应的value所占用的内存空间非常大

如果value是字符串类型,最大可以达到512MB的存储空间。如果value是列表类型,最多可以存储2^32 - 1个元素。一般来说,超过10KB的value就可以被认为是字符串类型的BigKey。

BigKey带来的问题就是:操作BigKey比较耗时,可能导致Redis发生阻塞,从而降低Redis性能。在并发场景下,还会带来网络堵塞问题

假设一个bigkey为1MB,每秒访问量为1000,每秒就会产生1000MB 的流量。对于普通的服务器来说简直是灭顶之灾。

15、什么是缓存击穿、缓存穿透、缓存雪崩?

刚开始我以为“缓存击穿、缓存穿透、缓存雪崩”说的是3个问题,在各个博客以及视频的讲解下越来越绕。最后我捋了一下,这TM不是一个问题吗。

为了让大家也绕一绕,我把各博客对“缓存击穿、缓存穿透、缓存雪崩”的描述贴在这里:

缓存击穿是指一个热点的Key在某个瞬间过期失效了,大量的并发请求在缓存获取不到数据后直接请求数据库的现象。

缓存穿透是指查询一个根本不存在的数据,缓存和数据库都不会命中,导致每次请求都要到数据库去查询。

缓存雪崩指的是缓存由于宕机或者某些原因不能提供服务,导致所有的请求去访问数据库,造成数据库查询压力骤增从而宕机。

图片

绕的我

透过现象看本质

我就非常不理解了,为什么把缓存带来的一个问题分好几个场景去描述,还这解决方案,那解决方案的,花里胡哨的增加了大家的理解难度。

在我看来“缓存击穿、缓存穿透,缓存雪崩”都是在说一个问题,那就是:

缓存没命中,请求落到数据库了

而“缓存雪崩”才突出了问题的本质:

没有缓存的缓冲,数据库承受不了那么大的压力,可能会造成宕机等问题。

仔细想想是不是这样?“缓存击穿、缓存穿透、缓存雪崩”最终的描述都是请求落到数据库了,只不过场景不同罢了。但不论哪种场景,在并发高的情况下都会给数据库带来压力。

所以,一个问题分这么多场景,引出这么多名词,我认为就是在增加大家的理解难度。

面试题解决方案

有问题就会有解决方案,既然看了这篇文章就不要死记硬背了,不然过段时间又会忘记,跟着思路顺其自然的理解。

透过现象看本质

对于以上的几个场景,要解决的问题就是:

如何提高缓存命中率。

也就是尽量避免请求打到数据库中,尤其是高并发的请求。主要涉及两个层面:

  1. 缓存组件要可靠:首先要确保缓存组件足够可靠

  2. 代码逻辑要严谨:在编写代码使用缓存时尽量要把各种场景考虑进去,把问题当作功能的一部分。

像“缓存击穿、缓存穿透”问题的产生都属于代码逻辑不严谨。热点Key怎么能突然消失呢?一个相同的请求怎么能并发访问到数据库呢?怎么能允许一个不存在的数据一直请求呢?这些问题在我看来都是不应该发生的

接下来就针对引起“缓存击穿、缓存穿透、缓存雪崩”的几个问题进行剖析解决。

提高缓存命中率一:完美处理热点Key的消失

热点数据通常分为可控和不可控。拿电商系统来讲,商品分类属于可控,因为基本上这类数据是通过后台配置的。而一些商品可能会因为某个原因突然爆火成为热点数据,这类数据属于不可控。

不论可控或不可控,热点数据不可以突然就消失,所以在缓存时要有对应的策略。

  • 像商品分类这类数据就可以不设置过期时间。

  • 而像不可控的热点数据,要靠一些策略避免其过期,比如通过“看门狗”方式监控热点Key,快过期时进行“续命”。

可以都不设置过期时间,让淘汰策略去淘汰数据吗?

非常不建议。

之前生产环境曾遇到过一个问题:用户每次登录之后会莫名其妙退出。经过排查发现,原来是因为Redis服务容量不足,所以最近登录生成的token一直被淘汰。

虽然没有报错,但是给用户带来不好的体验,对产品造成非常不好的影响。

当然,避免不了热点Key被人为删除或者其他恶意破坏,当发生这种情况怎么办?

如果热点Key不存在缓存中,势必要去数据库中查询了。此时,如果并发请求过高,一定不能让所有请求打到数据库,可以对该key进行加锁处理,获取到锁的请求去数据库访问并缓存,其他请求则等待该key缓存后再访问缓存。

因为平时写代码会很自然考虑到这一点,所以这也是为什么我刚开始一直不理解“缓存击穿”这样的问题。

提高缓存命中率二:避免查询不存在的数据

造成“查询不存在的数据”的原因要么是代码或数据出现问题,要么是遭到恶意的攻击造成的空命中。总之,这种情况无法完全避免。

但是,我们知道哪些数据会被缓存。这样的话,我们可以将这些数据放在一个“大集合”中,当请求的数据不存在这个“大集合”时,直接返回NULL即可。

那么问题来了:这个“大集合”放在哪里?肯定不能是数据库,但是内存容量又是有限的。怎么办?

有一个叫布隆过滤器的数据结构可以解决这个问题。其主要用于检测一个元素是否在一个集合里,其原理是:数据通过一组哈希函数映射到位图中,不论该元素多大都只需要占用1位,从而节省大量空间。如下图

图片

布隆过滤器原理

这样的话,我就可以将要缓存的数据先放在布隆过滤器中,当查询的数据不在布隆过滤器时就可以直接返回NULL了。

感兴趣的可以看下 面试官:如何在海量数据中快速检测某个数据

提高缓存命中率三:降低缓存服务的不可用

降低缓存服务的不可用也就是提高缓存服务的可用性,也就是Redis的高可用,这个没有什么逻辑就不展开了。

面试题案例

模拟案例

现在,通过代码模拟一个因“缓存击穿、缓存穿透、缓存雪崩”,请求并发到MySQL服务上,看会发生什么事。

服务器环境:1核1G

编程语言:Java

案例代码

public class MainTest {
    private static final String DB_URL = "jdbc:mysql://127.0.0.1:3306/test";
    private static final String USER = "root";
    private static final String PASS = "Mysql123.";

    public static void main(String[] args) throws InterruptedException {

        Timer timer = new Timer();
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                QueryTask.cacheExist = false;
            }
        };
        timer.schedule(task, 60 * 1000);

        while (true) {

            ExecutorService executorService = Executors.newFixedThreadPool(1500);
            for (int i = 0; i <1500 ; i++) {
                executorService.submit(new MainTest.QueryTask());
                System.gc();
            }
        }
    }

    static class QueryTask implements Runnable {
        static boolean cacheExist = true;

        @Override
        public void run() {
            try {

                if (cacheExist) {
                    System.out.println("访问缓存");
                } else {
                    Class.forName("com.mysql.jdbc.Driver");
                    Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
                    Statement statement = conn.createStatement();
                    Thread.sleep(3000);
                    String query = "SELECT * FROM test_cache";
                    ResultSet rs = statement.executeQuery(query);
                    while (rs.next()) {
                        int id = rs.getInt("id");
                        String value = rs.getString("value");
                        System.out.println("ID: " + id + ", Value: " + value);
                    }

                    rs.close();
                    statement.close();
                    conn.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

上面的代码主要做了两件事:

  1. 模拟1500个线程去查询数据。cacheExist为true时访问缓存,为false时去请求数据库。

  2. 通过定时任务在1分钟后将cacheExist设置为false。各位就想象成热点Key的突然消失、查询不存在的数据、redis的宕机。

案例执行效果

代码在执行1分钟后就会报下面的错误信息:

com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Data source rejected establishment of connection,  message from server: "Too many connections"

这是因为MySQL最大连接数只有151,远远低于并发线程数1500。

mysql> show variables like '%max_connections%';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 151   |
+-----------------+-------+

此时,我将MySQL最大连接数设置为1500。

mysql> SET GLOBAL max_connections = 1500;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like '%max_connections%';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 1500  |
+-----------------+-------+

现在执行 SHOW STATUS LIKE 'Threads_connected' 去查看MySQL连接线程数会发现数值突然升高,当连接数为1283 左右时,就会发现MySQL服务已经断开连接或者服务器宕机,也就是缓存雪崩的效果。

图片

MySQL压力过高宕机

总结

面试时不要被花里胡哨的问题迷惑住,要思考一下问题的本质。

“缓存击穿、缓存穿透、缓存雪崩”问题的本质就是:

当缓存没命中或失效,并发的请求打到数据库怎么办?

通过上面的描述,此类问题要有以下考虑:

  1. 提高缓存命中率。比如,要解决热点Key的突然消失、要避免查询不存在的数据等。

  2. 数据库并发请求要设置合理。太低了浪费资源,太高了就会出现MySQL服务宕机情况。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值