缓存穿透、缓存击穿、缓存雪崩【入门】

高并发、缓存穿透、缓存击穿、缓存雪崩、互斥锁、逻辑过期


缓存穿透

  • 客户端请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,那么所有请求都会直接打到数据库上,增大数据库压力
  • 解决:
    缓存空对象、布隆过滤、增加 id 复杂度,避免被猜出规律、做好基础数据格式校验、加强用户权限校验、做热点参数限流

缓存击穿(热点 key)

  • 一个被高并发访问并且缓存重建业务复杂的 key 突然失效,大量请求瞬间打到数据库
  • 解决:
    互斥锁、逻辑过期

缓存雪崩

  • 同一时段的大量 key 同时失效或 redis 宕机,导致大量请求直接打到数据库
  • 解决:
    给不同的 key 的 TTL 添加随机值、提高 redis 集群的高可用、缓存业务添加限流策略、添加多级缓存

缓存穿透

解决案例—缓存空对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//缓存穿透
    public Shop queryWithPassThrough(Long id){
        //1.从redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
        //2.判断shopJson是否有数据,有直接返回,没有判空
        if (StrUtil.isNotBlank(shopJson)){
            //判断某字符串是否不为空且长度不为0且不由空白符构成
            //判断参数:是否不为空,长度是否不为0,值是否不包含空白字符
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }

        //3.是否命中缓存的空对象
        //shopJson不

缓存击穿

解决方法

互斥锁案例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    public Shop queryWithMutex(Long id){

        //1.从redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);

        if (StrUtil.isNotBlank(shopJson)){
            //判断某字符串是否不为空且长度不为0且不由空白符构成
            //判断参数:是否不为空,长度是否不为0,值是否不包含空白字符
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }

        //shopJson不为空说明长度为0或者由空白符构成
        //没有命中空值
        if (shopJson!=null){
            return null;
        }

        //shopJson为空则查数据库加缓存
        //缓存重建
        //1.获取互斥锁
        String lockKey="lock:shop:"+id;
        Shop shop = null;
        try {
            boolean triedLock = tryLock(lockKey);
            //2.判断是否获取锁成功
            if (!triedLock){
                //3.获取锁失败则休眠50ms,醒来重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //4.获取锁成功则查询数据库,存在则返回并缓存重建,不存在则缓存空值并返回
            shop = getById(id);
            //模拟重建延迟
            //Thread.sleep(200);
            if (shop==null){
                //5.防止redis穿透,查询数据库为空,就缓存一个空值,并设置过期时间
stringRedisTemplate.opsForValue().set("cache:shop:","",30L,TimeUnit.MINUTES);
                //数据库没有,返回
                return null;
            }
            //6.数据库查询到信息,添加缓存,设置超时剔除
         stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //7.释放互斥锁
            unLock(lockKey);
        }
        return shop;
    }


    //缓存击穿问题,获取互斥锁
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    //释放互斥锁
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

这里模拟高并发,使用 apifox

逻辑过期案例

逻辑过期并不是设置 TTL,而是为缓存添加一个逻辑上的时间字段,判断该字段是否过期,过期则缓存重建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
    //缓存重建,也可以用于缓存预热,提前把热点数据缓存到redis
    private void saveShop2Redis(Long id,Long expireSeconds) throws InterruptedException {
        //1.查询店铺数据
        Shop shop = getById(id);
        //模拟缓存重建延迟
        Thread.sleep(200);
        //2.封装逻辑过期时间
        RedisData data = new RedisData();
        data.setData(shop);

        //设置逻辑过期时间
        data.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //3.写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(data));
    }

    //线程池,10个线程
    private static final ExecutorService CACHE_REBUILD= Executors.newFixedThreadPool(10);
    //逻辑过期解决缓存击穿
    //热点key一般提前添加到缓存,也就是缓存预热。
    //如果没有命中,说明业务上要求查询的该数据不展示,也就不需要再去查数据库了,这点看实际的业务需求
    //命中但是过期了就需要缓存重建,此时要去查数据库
    public Shop queryWithLogicExpire(Long id){

        //1.从redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
        if (StrUtil.isBlank(shopJson)){
            //2.没有命中直接返回null
            return null;
        }

        //3.命中redis,把json反序列化成对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject)redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //4.判断逻辑时间是否过期
        if (expireTime.isAfter(LocalDateTime.now())){
            //5.未过期,直接返回
            return shop;
        }

        //6.过期,缓存重建,获取互斥锁,判断是否获取锁成功
        String key="lock:shop:"+id;
        boolean triedLock = tryLock(key);
        if (triedLock){
            // 7.获取锁成功,使用线程池开启独立线程缓存重建
            CACHE_REBUILD.submit(
                    ()->{
                        try {
                            this.saveShop2Redis(id,1800L);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        } finally {
                            unLock(key);
                        }
                    }
            )
        }

        //8.获取锁失败,先返回过期的商铺信息
        return shop;
    }


    //获取互斥锁
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    //释放互斥锁
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

缓存雪崩

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计