Redis—缓存雪崩、缓存穿透、缓存击穿详解

缓存雪崩、缓存穿透、缓存击穿

文章目录

    • 缓存雪崩、缓存穿透、缓存击穿
      • 1. 缓存雪崩
        • 1.1 什么情况下会发生
        • 1.2 解决办法
      • 2. 缓存穿透
        • 2.1 危害
        • 2.2 解决
        • 解决方案1:空对象缓存或者缺省值
        • 解决方案2:Google布隆过滤器Guava解决缓存穿透
        • 解决方案3:Redis布隆过滤器解决缓存穿透
        • 在centos7下布隆过滤器2种安装方式
      • 3. 缓存击穿
        • 3.1 危害
        • 3.2 解决
        • 方案1:对于访问频繁的热点key,干脆就不设置过期时间
        • 方案2:互斥独占锁防止击穿
        • 方案3:定时轮询,互斥更新,差异失效时间
        • 案例:淘宝聚划算功能实现+防止缓存击穿
    • 总结


1. 缓存雪崩

1.1 什么情况下会发生

  1. Redis主机挂了,Redis全盘崩溃
  2. 缓存中有大量数据同时过期

1.2 解决办法

  1. redis缓存集群实现高可用(事前)
  2. ehcache本地缓存 + Hystrix或者阿里sentinel限流&降级(事中)
  3. 开启Redis持久化机制aof/rdb,尽快恢复缓存集群(事后)
    Redis—缓存雪崩、缓存穿透、缓存击穿详解_第1张图片

2. 缓存穿透

  1. 请求去查询一条记录,先redis后mysql发现都查询不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库压力暴增,这种现象我们称为缓存穿透,这个redis变成了一个摆设。。。
  2. 简单说就是本来无一物,既不在Redis缓存中,也不在数据库中

2.1 危害

  1. 第一次来查询后,一般我们有回写redis机制
  2. 第二次来查的时候redis就有了,偶尔出现穿透现象一般情况无关紧要

2.2 解决

在这里插入图片描述

解决方案1:空对象缓存或者缺省值

一般OK
在这里插入图片描述
But
黑客或者恶意攻击

  1. 黑客会对你的系统进行攻击,拿一个不存在的id 去查询数据,会产生大量的请求到数据库去查询。可能会导致你的数据库由于压力过大而宕掉
  2. id相同打你系统:第一次打到mysql,空对象缓存后第二次就返回null了,避免mysql被攻击,不用再到数据库中去走一圈了
  3. id不同打你系统(防不住):由于存在空对象缓存和缓存回写(看自己业务不限死),redis中的无关紧要的key也会越写越多(记得设置redis过期时间)

解决方案2:Google布隆过滤器Guava解决缓存穿透

布隆过滤器特点:误判只会发生在过滤器没有添加过的元素,对于添加过的元素不会发生误判

Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器
Redis—缓存雪崩、缓存穿透、缓存击穿详解_第2张图片

  • 代码实战
public class GuavaBloomfilterDemo {
    public static final int _1W = 10000;
    //布隆过滤器里预计要插入多少数据
    public static int size = 100 * _1W;
    //误判率,它越小误判的个数也就越少(思考,是不是可以设置的无限小,没有误判岂不更好)
    public static double fpp = 0.01; //误判率特别小时,程序执行效率急剧下降

    /**
     * helloworld入门
     */
    public void bloomFilter() {
        // 创建布隆过滤器对象
        BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 100);
        // 判断指定元素是否存在
        System.out.println(filter.mightContain(1));//false
        System.out.println(filter.mightContain(2));//false
        // 将元素添加进布隆过滤器
        filter.put(1);
        filter.put(2);
        System.out.println(filter.mightContain(1));//true
        System.out.println(filter.mightContain(2));//true

    }

    /**
     * 误判率演示+源码分析
     */
    public void bloomFilter2() {
        // 构建布隆过滤器
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);

        //1 先往布隆过滤器里面插入100万的样本数据
        for (int i = 0; i < size; i++) {
            bloomFilter.put(i);
        }
   /*     List listSample = new ArrayList<>(size);
        //2 这100万的样本数据,是否都在布隆过滤器里面存在?
        for (int i = 0; i < size; i++){
            if (bloomFilter.mightContain(i)) {
                listSample.add(i);
                continue;
            }
        }
        System.out.println("存在的数量:" + listSample.size());//存在的数量:1000000 */

        //3 故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里,误判率演示
        List<Integer> list = new ArrayList<>(10 * _1W);

        for (int i = size + 1; i < size + 100000; i++) {
            if (bloomFilter.mightContain(i)) {
                System.out.println(i + "\t" + "被误判了.");
                list.add(i);
            }
        }
        System.out.println("误判的数量:" + list.size()); //误判的数量:947
    }

    public static void main(String[] args) {
        new GuavaBloomfilterDemo().bloomFilter2();
    }
}

解决方案3:Redis布隆过滤器解决缓存穿透

Guava缺点说明:只能单机使用

案例:白名单过滤器

  1. 架构图
    Redis—缓存雪崩、缓存穿透、缓存击穿详解_第3张图片
  2. 代码
public class RedissonBloomFilterDemo {
    public static final int _1W = 10000;

    //布隆过滤器里预计要插入多少数据
    public static int size = 100 * _1W;
    //误判率,它越小误判的个数也就越少
    public static double fpp = 0.03;

    static RedissonClient redissonClient = null;//jedis
    static RBloomFilter rBloomFilter = null;//redis版内置的布隆过滤器

    @Resource
    RedisTemplate redisTemplate;


    static {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.111.147:6379").setDatabase(0);
        //构造redisson
        redissonClient = Redisson.create(config);
        //通过redisson构造rBloomFilter
        rBloomFilter = redissonClient.getBloomFilter("phoneListBloomFilter", new StringCodec());

        rBloomFilter.tryInit(size, fpp);

        // 1测试  布隆过滤器有+redis有
        //rBloomFilter.add("10086");
        //redissonClient.getBucket("10086",new StringCodec()).set("chinamobile10086");

        // 2测试  布隆过滤器有+redis无
        //rBloomFilter.add("10087");

        //3 测试 ,布隆过滤器无+redis无

    }

    private static String getPhoneListById(String IDNumber) {
        String result = null;

        if (IDNumber == null) {
            return null;
        }
        //1 先去布隆过滤器里面查询
        if (rBloomFilter.contains(IDNumber)) {
            //2 布隆过滤器里有,再去redis里面查询
            RBucket<String> rBucket = redissonClient.getBucket(IDNumber, new StringCodec());
            result = rBucket.get();
            if (result != null) {
                return "i come from redis: " + result;
            } else {
                result = getPhoneListByMySQL(IDNumber);
                if (result == null) {
                    return null;
                }
                // 重新将数据更新回redis
                redissonClient.getBucket(IDNumber, new StringCodec()).set(result);
            }
            return "i come from mysql: " + result;
        }
        return result;
    }

    private static String getPhoneListByMySQL(String IDNumber) {
        return "chinamobile" + IDNumber;
    }


    public static void main(String[] args) {
        //String phoneListById = getPhoneListById("10086");
        //String phoneListById = getPhoneListById("10087"); //请测试执行2次
        String phoneListById = getPhoneListById("10088");
        System.out.println("------查询出来的结果: " + phoneListById);

        //暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        redissonClient.shutdown();
    }

}

在centos7下布隆过滤器2种安装方式

  1. 采用docker安装RedisBloom,推荐
    Redis 在 4.0 之后有了插件功能(Module),可以使用外部的扩展功能,可以使用 RedisBloom 作为 Redis 布隆过滤器插件。
    docker run -p 6379:6379 --name=redis6379bloom -d redislabs/rebloom  
	docker exec -it redis6379bloom /bin/bash        
	redis-cli

布隆过滤器常用操作命令
Redis—缓存雪崩、缓存穿透、缓存击穿详解_第4张图片

3. 缓存击穿

大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去
简单说就是热点key突然失效了,暴打mysql

3.1 危害

会造成某一时刻数据库请求量过大,压力剧增。

3.2 解决

在这里插入图片描述

方案1:对于访问频繁的热点key,干脆就不设置过期时间

方案2:互斥独占锁防止击穿

多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
Redis—缓存雪崩、缓存穿透、缓存击穿详解_第5张图片

方案3:定时轮询,互斥更新,差异失效时间

案例:淘宝聚划算功能实现+防止缓存击穿

高并发的淘宝聚划算案例落地 (Redis的list)
Redis—缓存雪崩、缓存穿透、缓存击穿详解_第6张图片

@Service
@Slf4j
public class JHSABTaskService {
    @Autowired
    private RedisTemplate redisTemplate;

    //@PostConstruct ,平时注释,用时打开
    public void initJHSAB() {
        log.info("启动AB定时器计划任务淘宝聚划算功能模拟.........." + DateUtil.now());
        new Thread(() -> {
            //模拟定时器,定时把数据库的特价商品,刷新到redis中
            while (true) {
                //模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
                List<Product> list = this.products();
                //先更新B缓存
                this.redisTemplate.delete(Constants.JHS_KEY_B);
                this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY_B, list);
                this.redisTemplate.expire(Constants.JHS_KEY_B, 20L, TimeUnit.DAYS);
                //再更新A缓存
                this.redisTemplate.delete(Constants.JHS_KEY_A);
                this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY_A, list);
                this.redisTemplate.expire(Constants.JHS_KEY_A, 15L, TimeUnit.DAYS);
                //间隔一分钟 执行一遍
                try {
                    TimeUnit.MINUTES.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                log.info("runJhs定时刷新..............");
            }
        }, "t1").start();
    }

    /**
     * 模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
     */
    public List<Product> products() {
        List<Product> list = new ArrayList<>();
        for (int i = 1; i <= 20; i++) {
            Random rand = new Random();
            int id = rand.nextInt(10000);
            Product obj = new Product((long) id, "product" + i, i, "detail");
            list.add(obj);
        }
        return list;
    }
}

@RestController
@Slf4j
@Api(description = "聚划算商品列表接口AB")
public class JHSABProductController {
    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping(value = "/pruduct/findab", method = RequestMethod.GET)
    @ApiOperation("按照分页和每页显示容量,点击查看AB")
    public List<Product> findAB(int page, int size) {
        List<Product> list = null;
        long start = (page - 1) * size;
        long end = start + size - 1;
        try {
            //采用redis list数据结构的lrange命令实现分页查询
            list = this.redisTemplate.opsForList().range(Constants.JHS_KEY_A, start, end);
            if (CollectionUtils.isEmpty(list)) {
                log.info("=========A缓存已经失效了,记得人工修补,B缓存自动延续5天");
                //用户先查询缓存A(上面的代码),如果缓存A查询不到(例如,更新缓存的时候删除了),再查询缓存B
                this.redisTemplate.opsForList().range(Constants.JHS_KEY_B, start, end);
            }
            log.info("查询结果:{}", list);
        } catch (Exception ex) {
            //这里的异常,一般是redis瘫痪 ,或 redis网络timeout
            log.error("exception:", ex);
            //TODO 走DB查询
        }
        return list;
    }
}

总结

Redis—缓存雪崩、缓存穿透、缓存击穿详解_第7张图片

你可能感兴趣的:(Redis,缓存,redis,数据库)