【搞定面试官】系列:缓存雪崩、缓存击穿、缓存穿透

149 阅读6分钟

前言

Redis在当今电商环境中炙手可热,但是使用Redis除了能大幅提高性能之外,也会带来很多问题。最常见的有:缓存雪崩、缓存击穿、缓存穿透。本篇将重点介绍这三个问题及解决方案。
关于Redis面试基础可以看该系列的第一篇:
【阿里面试中的Redis】系列:Redis基础\

正文:面试开始

小伙子,我看你简历上写到了Redis,那么你了解过什么是缓存雪崩吗?
缓存雪崩是指同一时间Redis中的key大面积失效,一瞬间缓存好像没有一样,所有的请求都打到的数据库。这个时候如果是瞬时高并发大流量的话,对于数据库而言可是灾难性的。并且如果系统没有做熔断降级的话,基本上就是瞬间挂一片的节奏。但是缓存雪崩常见于哪些场景呢?

这里我就举个例子:比如在秒杀系统,经常在活动开始前会有数据预热,什么意思呢,就是提前把相关的SKU和库存信息塞进缓存,一般入缓存的数据会设置一个过期时间,假如此时把这些SKU的过期时间设置一样。假设秒杀开始的时候瞬时来了5000个请求,而这些key同时都失效了,本来单机Redis扛住5000QPS毫无压力,这下缓存崩了,所有请求直接打到了MySQL,数据库必然扛不住,也许它会“嗷呜”一声报个警,但是还没等到DBA听到响就直接挂了。并且无论你怎么重启数据库,只要Redis没热点数据,MySQL立马又会被新的流量打得爬不起来。

在这里插入图片描述


拓展:
这里少侠用Jmeter对单机数据库进行了压测,设置的参数如下图所示,参数说明:

  • Number of Threads(users) :线程数或者请求的用户数。这里设置了2000个并发。
  • Ramp-up period(seconds) :请求的间隔时间。这里设置为0,表示同时请求。
  • Loop Count:每个线程循环请求次数。
    在这里插入图片描述


当热点数据全部可以从Redis中获取时候,压测结果如下,参数说明:

  • Samples:表示你这次测试中一共发出了多少个请求,如果模拟10个用户,每个用户循环10次,那么这里显示100。
  • Average:平均响应时间——默认情况下是单个 Request 的平均响应时间,当使用了 Transaction Controller 时,也可以以Transaction 为单位显示平均响应时间。
  • Median:中位数,也就是 50% 用户的响应时间。
  • X% Line:X% 用户的响应时间。
  • Min:最小响应时间。
  • Max:最大响应时间。
  • Error% :本次测试中出现错误的请求的数量/请求的总数。

Throughput:吞吐量,即默认情况下表示每秒完成的请求数(Request per Second)。
注意看下图,99%的请求响应是7ms,MySQL的吞吐量是265.4/s,响应正常。\

在这里插入图片描述


当Redis发生缓存雪崩时,注意看下图,99% 请求的响应都到了5136ms了,吞吐量也下降了一半以上。

在这里插入图片描述


好的,那再说说你在开发中是如何解决缓存雪崩的?
这个一般是三种方式:一是设置过期时间时为每个key的失效时间都加个随机值,保证它们不会在统一时间失效;二是在集群Redis环境下,将这些热点数据分布到不同的Redis库中也能规避同时全部失效的风险;第三种就比较简单粗暴了,根本就不设置过期时间,只有当MySQL数据更新时同步更新缓存就好了。
那你了解缓存击穿和缓存穿透么,可以说说它们跟缓存雪崩的关系吗?
缓存击穿缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了数据库,而缓存击穿不同的是缓存击穿是指一个key非常热点,在不停的扛着高并发,高并发流量集中对这一个key进行访问,当这个Key在失效的瞬间,持续的大并发就击穿缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。
缓存穿透是指缓存和数据库中都没有的数据。可能是用户恶意请求,也有可能是用户访问了过期被物理删除的数据。这里举个简单的例子,MySQL自增主键一般都是从1开始的,如果用户查询了id=-1的数据,正常缓存和数据库中都是没有的,如果这种请求量又很大的话,数据库保不准就挂了。
那说说这两种问题分别如何解决?
缓存击穿的解决方案主要有两种,一是设置热点key永不过期。二是利用互斥锁,每次只能有一个用户获取到key,如果key失效了,会从MySQL中查询并更新到Redis,此时最多也就一次请求MySQL。
缓存穿透就比较讲究了,缓存穿透主要可能是黑客或者行业竞争对手的恶意攻击,尤其对于对外API接口,比如分页参数如果不做校验,调用方二话不说,先请求它一个亿!再来几个并发调用,如果这是在618期间,会发生什么就不言而喻了。所以我们首先可以做的就是接口里关键参数必须加校验,把这些请求拦截在代码层。事实上在开发中我们要有一颗“不信任”的心态,因为你不知道调用你接口的人是谁。第二种方式就是大名鼎鼎的布隆过滤器了,原理也很简单,就是利用高效的数据结构和算法快速判断出你这个key是否在数据库中存在,不存在你return就好了,存在你就去查MySQL刷新到Redis并将结果集return。\

小结

Redis是现在的NoSQL的代表,熟练运用和掌握其原理对个人的成长大有裨益。\

本例相关代码如下,仅供参考(如需完整源码请留言):

/**
 * @author Carson
 * @date 20-8-20 下午5:00
 */
@RestController
public class CommonController {
    private final Logger logger = LoggerFactory.getLogger(CommonController.class);
    @Autowired
    private JdbcTemplate template;
    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping(value = "/showValue", method = RequestMethod.GET)
    public String showName(@RequestParam("key") Integer key) {
        // query from cache
        ValueOperations valueOperations = redisTemplate.opsForValue();
        String val = (String) valueOperations.get(key);
        if (StringUtils.isBlank(val)) {
            logger.warn("缓存雪崩!!!");
            // query from mysql
            String sql = "SELECT user_name FROM cps_user_info WHERE id=?";
            val = template.queryForObject(
                    sql, new Object[]{key}, String.class);
            if (StringUtils.isNotBlank(val)) {
                valueOperations.set(key, val, 6000L, TimeUnit.SECONDS);
            }
        }
        return "Value:" + val;
    }

    @RequestMapping(value = "/insertRedis", method = RequestMethod.GET)
    public void insertRedis() {
        Integer key = 1;
        ValueOperations valueOperations = redisTemplate.opsForValue();
        valueOperations.set(key, "SEU", 10L, TimeUnit.SECONDS);
    }
}