Redis缓存双写一致性、穿透(布隆)、雪崩、击穿的案例总结

目录

1、缓存的作用与成本

2、关于缓存双写一致性

2.1 四种更新策略

2.2 最佳实践方案

3、关于缓存穿透       

3.1 使用 缓存空对象 方案

3.2 使用 布隆过滤器 方案

4、关于缓存雪崩

5、关于缓存击穿

5.1采用双检加锁策略

5.1.1 使用 互斥锁 方案

5.1.2 使用 逻辑过期 方案

5.2 设置差异失效时间

6、封装 Redis 工具类

6.1 封装 缓存穿透中的缓存空对象 方案

6.2 封装 缓存击穿中的逻辑过期 方案

7、总结

 三种淘汰策略


1、缓存的作用与成本

作用: 暂存数据处理结果,并提供下次访问使用。在很多场合,数据的处理或者数据获取可能非常费时,当对这个数据的请求量很大时,频繁的数据处理会消耗大量资源。缓存的作用就是将这些来之不易的数据存储起来,当再次请求此数据时,直接从缓存中获取而省略数据处理流程,从而降低资源的消耗提高响应速度

成本:数据不一致问题,缓存层和数据层有时窗口不一致,和更新策略有关;代码维护成本,原本只需读写MySQL就能实现功能,但加入了缓存之后就需要去维护缓存的数据,增加了代码复杂度


2、关于缓存双写一致性

相关图解:

                

相关代码实现:

  从 redis 中进行查询,若缓存命中,则直接返回对应数据

//1.从 redis 中查询缓存
        String shopJSON = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //1.1 若缓存中有店铺的相关信息则进行返回,即缓存命中
        if(StrUtil.isNotBlank(shopJSON)){
            //1.2 这里需要将 JSON 类型的数据,转换为 java 类型的数据进行返回
            Shop shop = JSONUtil.toBean(shopJSON, Shop.class);
            return Result.ok(shop);
        }

 若 redis 中命中缓存失败,则需要从 mysql 中进行查询数据返回,并将查询到的数据写回 redis 中作为缓存

//2.缓存未命中,则从 mysql 中查询相关信息
        Shop shop = shopService.getById(id);
        //2.1 mysql 中存在相关信息,写入 redis 缓存,并则进行返回
        if(!Objects.isNull(shop)){

            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop));

        }else {
            //2.2 若不存在,则给出提示信息
            Result.fail("店铺ID 为"+id+"的信息不存在!");
        }

        return Result.ok(shop);

        这时需要考虑到一个问题,在数据库中的数据进行更新时,这时 redis 缓存那边保存的还是旧数据,需要将数据库中的数据更新到 redis 中,以保证双写一致性


 2.1 四种更新策略

  • 先更新数据库,再更新缓存

其中,可能出现的问题:

  1.  先更新mysql的某商品的库存,当前商品的库存是100,更新为99个。
  2.  先更新mysql修改为99成功,然后更新redis。
  3.  此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面的还是100 。
  4. 上述发生,会让数据库里面和缓存redis里面数据不一致,读到redis脏数据
  • 先更新缓存,再更新数据库(不太推荐,业务上一般把mysql作为底单数据库,以保证最后解释)

其中,可能出现的问题:

【异常逻辑】多线程环境下,A、B两个线程有快有慢有并行

A update redis  100

B update redis  80

B update mysql 80

A update mysql 100

最终结果:mysql100,redis80

  • 先删除缓存,再更新数据库

其中,可能出现的问题:

  1. 请求A进行写操作,删除redis缓存后,工作正在进行中,更新mysql......A还么有彻底更新完mysql,还没commit
  2. 请求B开工查询,查询redis发现缓存不存在(被A从redis中删除了)
  3. 请求B继续,去数据库查询得到了mysql中的旧值(A还没有更新完)
  4. 请求B将旧值写回redis缓存
  5. 请求A将新值写入mysql数据库

解决方法:使用延时双删策略

  • 先更新数据库,再删除缓存(一般推荐使用这种方法)

其中,可能出现的问题:

假如缓存删除失败或者来不及,导致请求再次访问redis时缓存命中,读取到的是缓存旧值。

解决方法:使用中间件,如MQ消息队列

  1. 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbitMQ等)。
  2. 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
  3. 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试
  4. 如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。

2.2 最佳实践方案

  1. 低一致性需求:使用 redis 自带的内存淘汰机制
  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案

读操作:

  • 缓存命中则直接返回
  • 缓存未命中则查询数据库,并且写入缓存,加上超时时间

写操作:

  • 先写数据库,再删除缓存
  • 确保数据库与 redis 缓存的原子性

代码案例:

@Transactional  //这里进行添加事务
    public Result updateShop(Shop shop) {

        if(!Objects.isNull(shop.getId())) {
            //1.先更新数据库
            shopService.updateById(shop);

            //2.再删除 redis 缓存
            stringRedisTemplate.delete(CACHE_SHOP_KEY + shop.getId());
        }else {
            Result.fail("店铺的 ID 不能为空!");
        }

        return Result.ok();
    }


3、关于缓存穿透       

定义:当去查询一条记录,先查 redis 中,查询不到,然后再查询 mysql 中,也查询不到;即使这样,请求最终每次都会打到 mysql 数据库中,从而导致后台数据库的压力暴增,redis  形同虚设 ,接下来,介绍对应的解决方案🤔             

3.1 使用 缓存空对象 方案

        可以增强回写机制。mysql也查不到的话也让redis存入刚刚查不到的key并保护mysql。第一次来查询uid:abcdxxx,redis和mysql都没有,返回null给调用者,但是增强回写后第二次来查uid:abcdxxx,此时redis就有值了。可以直接从Redis中读取default缺省值(null)返回给业务应用程序,避免了把大量请求发送给mysql处理,打爆mysql。

但是,这样存在缺点,只能解决 key 值相同的问题,同时会带来额外的内存消耗与短期的不一致性(由于TTL过期时间)

代码案例:

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

迷迷的k

感谢长的这么帅还来打赏我

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值