Redis经典面试题:你知道缓存击穿、缓存穿透、缓存雪崩吗?

末蓝、 2024-04-25 08:35 182阅读 0赞

在这里插入图片描述

文章目录

  • 前言
  • 面试题剖析
    • 花里胡哨的名词
    • 透过现象看本质
  • 面试题解决方案
    • 透过现象看本质
    • 提高缓存命中率一:完美处理热点Key的消失
    • 提高缓存命中率二:避免查询不存在的数据
    • 提高缓存命中率三:降低缓存服务的不可用
  • 面试题案例
    • 模拟案例
    • 案例代码
    • 案例执行效果
  • 总结

前言

又快到一年一度的金三银四了,大家在面试的时候一定被问到过Redis缓存问题吧。可能有些初学者对“缓存击穿、缓存穿透、缓存雪崩”这几个名词感到陌生,或者了解过但是一时半会没办法理解。没关系,希望通过本文可以让你轻松理解这些概念并掌握其解决方案,然后在即将到来的金三银四面试中对你有所帮助。

面试题剖析

花里胡哨的名词

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

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

缓存击穿是指一个热点的Key在某个瞬间过期失效了,大量的并发请求在缓存获取不到数据后直接请求数据库的现象。
缓存穿透是指查询一个根本不存在的数据,缓存和数据库都不会命中,导致每次请求都要到数据库去查询。
缓存雪崩指的是缓存由于宕机或者某些原因不能提供服务,导致所有的请求去访问数据库,造成数据库查询压力骤增从而宕机。

在这里插入图片描述

透过现象看本质

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

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

缓存没命中,请求落到数据库了 \color{blue}{缓存没命中,请求落到数据库了} 缓存没命中,请求落到数据库了

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

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

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

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

面试题解决方案

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

透过现象看本质

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

如何提高缓存命中率。 \color{blue}{如何提高缓存命中率。} 如何提高缓存命中率。

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

  1. 缓存组件要可靠:首先要确保缓存组件足够可靠。
  2. 代码逻辑要严谨:在编写代码使用缓存时尽量要把各种场景考虑进去,把问题当作功能的一部分。

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

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

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

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

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

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

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

非常不建议。线上遇到过一个问题:用户每次登录之后会莫名其妙退出。原因是因为Redis服务容量不足,所以最近登录生成的token一直被淘汰。虽然没有报错,但是给用户带来不好的体验,对产品造成非常不好的影响。

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

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

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

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

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

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

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

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

在这里插入图片描述

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

ps:大家可以先去其他平台看下同名写的布隆过滤器的应用和原理,这里不好挂链接,后续再搬过来。

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

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

面试题案例

模拟案例

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

服务器环境:1核1G
编程语言:Java

案例代码

  1. public class MainTest {
  2. private static final String DB_URL = "jdbc:mysql://127.0.0.1:3306/test";
  3. private static final String USER = "root";
  4. private static final String PASS = "Mysql123.";
  5. public static void main(String[] args) throws InterruptedException {
  6. Timer timer = new Timer();
  7. TimerTask task = new TimerTask() {
  8. @Override
  9. public void run() {
  10. QueryTask.cacheExist = false;
  11. }
  12. };
  13. timer.schedule(task, 60 * 1000);
  14. while (true) {
  15. ExecutorService executorService = Executors.newFixedThreadPool(1500);
  16. for (int i = 0; i <1500 ; i++) {
  17. executorService.submit(new MainTest.QueryTask());
  18. System.gc();
  19. }
  20. }
  21. }
  22. static class QueryTask implements Runnable {
  23. static boolean cacheExist = true;
  24. @Override
  25. public void run() {
  26. try {
  27. if (cacheExist) {
  28. System.out.println("访问缓存");
  29. } else {
  30. Class.forName("com.mysql.jdbc.Driver");
  31. Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
  32. Statement statement = conn.createStatement();
  33. Thread.sleep(3000);
  34. String query = "SELECT * FROM test_cache";
  35. ResultSet rs = statement.executeQuery(query);
  36. while (rs.next()) {
  37. int id = rs.getInt("id");
  38. String value = rs.getString("value");
  39. System.out.println("ID: " + id + ", Value: " + value);
  40. }
  41. rs.close();
  42. statement.close();
  43. conn.close();
  44. }
  45. } catch (Exception e) {
  46. e.printStackTrace();
  47. }
  48. }
  49. }
  50. }

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

  1. 模拟1500个线程去查询数据。cacheExist为true时访问缓存,为false时去请求数据库。
  2. 通过定时任务在1分钟后将cacheExist设置为false。各位就想象成热点Key的突然消失、查询不存在的数据、redis的宕机。

案例执行效果

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

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

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

  1. mysql> show variables like '%max_connections%';
  2. +-----------------+-------+
  3. | Variable_name | Value |
  4. +-----------------+-------+
  5. | max_connections | 151 |
  6. +-----------------+-------+

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

  1. mysql> SET GLOBAL max_connections = 1500;
  2. Query OK, 0 rows affected (0.00 sec)
  3. mysql> show variables like '%max_connections%';
  4. +-----------------+-------+
  5. | Variable_name | Value |
  6. +-----------------+-------+
  7. | max_connections | 1500 |
  8. +-----------------+-------+

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

MySQL压力过高宕机

总结

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

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

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

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

  1. 提高缓存命中率。比如,要解决热点Key的突然消失、要避免查询不存在的数据等。
  2. 数据库并发请求要设置合理。太低了浪费资源,太高了就会出现MySQL服务宕机情况。

写完这篇文章正好是年前最后一天班了,祝大家新年快乐,恭喜发财。

发表评论

表情:
评论列表 (有 0 条评论,182人围观)

还没有评论,来说两句吧...

相关阅读