Redis缓存穿透和缓存雪崩的面试题解析

前段时间去摩拜面试,然后,做笔试的时候,遇到了几道Redis面试题目,今天来做个总结。捋一下思路,顺便温习一下之前的知识,如果对您有帮助,左上角点下关注 ! 谢谢

文章目录


在这里插入图片描述
大家都知道Redis是一个缓存中间件, 类似的 还有

  • Ehcache(纯Java的进程内缓存框架,也叫二级缓存)
  • memcache是一套分布式的高速缓存系统

用的最多的还是Redis,而且我个人也觉得Redis比较好用,既然使用Redis
就会必然会考虑使用Redis出现的各种紧急情况 比如、高并发 、优化、穿透、雪崩、

我们先来讲一下几个概念

缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存查不到,接着查询数据库也无法查询出结果,因此也不会写入到缓存中,这将会导致每个查询都会去请求数据库,造成缓存穿透;简单点说就是穿过了缓存,就像把玻璃打破,穿过玻璃一样

解决缓存穿透也有两种方案:

1、由于请求的参数是不合法的(每次都请求不存在的参数),于是我们可以使用布隆过滤器(BloomFilter) 或者压缩filter提前拦截,不合法就不让这个请求到数据库层!
在这里插入图片描述

  1. 缓存空对象
    存储层不命中后,即使在这里插入图片描述返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;在这里插入图片描述
    但是这种方法会存在两个问题:

如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;

在这里插入图片描述

即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

解决缓存穿透 使用场景 维护成本
缓存空对象 1.数据命中不高 2.数据吞吐量大 代码维护简单,需要更多的缓存空间 数据不一致
布隆过滤器 1.数据命中不高 数据相对固定实时性低 代码维护复杂、缓存空间大

如果是电商类的项目, 这里面试官还会问,假设 有10000个请求,想达到第一次请求从数据库中获取,其他9999个请求从redis中获取这种效果。

高并发情况下,可能都要访问数据库,因为同时访问的方法,这时需要加入同步锁,当其中一个缓存获取后,其它的就要通过缓存获取数据.

+ 方法一: 在方法上加上同步锁 synchronized

//加同步锁,解决高并发下缓存穿透
    @Test
    public synchronized void getMyUser(){
        //字符串的序列化器 redis
        RedisSerializer redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);
        //查询缓存
        MyUser myUser = (MyUser) redisTemplate.opsForValue().get("myUser");
        if(null == myUser){
            System.out.println("当缓存没有myUser时显示.");
            //缓存为空,查询数据库
            myUser = myUserMapper.selectByPrimaryKey(1l);
            //把数据库查询出来的数据放入redis
            redisTemplate.opsForValue().set("myUser",myUser);
        }
        System.out.println(myUser);
    }
  • 方法二: 使用双层检测锁, 效率高于方法一.
@Test
    public void getMyUser(){
        //字符串的序列化器 redis
        RedisSerializer redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);
        //查询缓存
        MyUser myUser = (MyUser) redisTemplate.opsForValue().get("myUser");

        //双层检测锁
        if(null == myUser){
            synchronized(this){
                myUser = (MyUser) redisTemplate.opsForValue().get("myUser");
                if(null == myUser){
                    System.out.println("当缓存没有myUser时显示.");
                    //缓存为空,查询数据库
                    myUser = myUserMapper.selectByPrimaryKey(1l);
                    //把数据库查询出来的数据放入redis
                    redisTemplate.opsForValue().set("myUser",myUser);
                }
            }
        }


        System.out.println(myUser);
    }

进行高并发测试:

package com.bdqn.spiritmark.controller;

import  com.bdqn.spiritmark.bean.MyUser;
import  com.bdqn.spiritmark.mapper.MyUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
public class MyUserController {

    @Autowired
    private RedisTemplate<Object,Object> redisTemplate;
    @Resource
    private MyUserMapper myUserMapper;

    public Integer insertNew(){

        return 0;
    }

    @RequestMapping("/getMyUserTest")
    public void getMyUserTest(){
        //线程,该线程调用底层查询MyUser的方法
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                getMyUser();
            }
        };

        //多线程测试一下缓存穿透的问题
        ExecutorService executorService = Executors.newFixedThreadPool(25);
        for(int i=0;i<10000;i++){
            executorService.submit(runnable);
        }

    }

    public void getMyUser(){
        //字符串的序列化器 redis
        RedisSerializer redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);
        //查询缓存
        MyUser myUser = (MyUser) redisTemplate.opsForValue().get("myUser");

        //双层检测锁
        if(null == myUser){
            System.out.println("当缓存没有myUser时显示.没有进入同步锁");
            synchronized(this){
                myUser = (MyUser) redisTemplate.opsForValue().get("myUser");
                if(null == myUser){
                    System.out.println("当缓存没有myUser时显示.已经进入同步锁");
                    //缓存为空,查询数据库
                    myUser = myUserMapper.selectByPrimaryKey(1l);
                    //把数据库查询出来的数据放入redis
                    redisTemplate.opsForValue().set("myUser",myUser);
                }
            }
        }

        System.out.println(myUser);
    }


}

线程池中不要特别大的线程,

随后看打印输出:

当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.没有进入同步锁
当缓存没有myUser时显示.已经进入同步锁
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@b49b283] was not registered for synchronization because synchronization is not active
2019-01-01 17:10:51.616  INFO 19540 --- [ool-1-thread-14] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2019-01-01 17:10:51.747  INFO 19540 --- [ool-1-thread-14] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1935473697 wrapping com.mysql.cj.jdbc.ConnectionImpl@4bc0a38] will not be managed by Spring
==>  Preparing: select id, user_id, open_id, user_name, user_phone, location from my_user where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, user_id, open_id, user_name, user_phone, location
<==        Row: 1, 111, 111, 123, 111, t
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@b49b283]
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
MyUser(id=1, userId=111, openId=111, userName=123, userPhone=111, location=t)
...
...

可以看到多个并发同时访问方法时,只有一个进入同步锁查询了数据库,其它还是通过缓存获取数据.

缓存雪崩

缓存雪崩通俗简单的理解就是:由于原有缓存失效(或者数据未加载到缓存中),新缓存未到期间(缓存正常从Redis中获取,如下图)所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机,造成系统的崩溃。

基本解决思路如下:

  • 第一,大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,避免缓存失效时对数据库造成太大的压力,虽然能够在一定的程度上缓解了数据库的压力但是与此同时又降低了系统的吞吐量。

  • 第二,分析用户的行为,尽量让缓存失效的时间均匀分布。

  • 第三,如果是因为某台缓存服务器宕机,可以考虑做主备,比如:redis主备,但是双缓存涉及到更新事务的问题,update可能读到脏数据,需要好好解决。

Redis雪崩效应的解决方案:

1、可以使用分布式锁,单机版的话本地锁

2、消息中间件方式

3、一级和二级缓存Redis+Ehchache

4、均摊分配Redis的key的失效时间

缓存并发

缓存并发是指,高并发场景下同时大量查询过期的key值、最后查询数据库将缓存结果回写到缓存、造成数据库压力过大

解决

在缓存更新或者过期的情况下,先获取锁,在进行更新或者从数据库中获取数据后,再释放锁,需要一定的时间等待,就可以从缓存中继续获取数据。

参考文章地址 :

发布了40 篇原创文章 · 获赞 112 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_42897427/article/details/103902097