详解Redis缓存、穿透、雪崩

目录

1.redis的使用场景

2.什么是缓存穿透?怎么解决?

2.1 什么是缓存穿透

2.2 解决方案

2.2.1 方案一:缓存空数据

原理:

优缺点:

2.2.2 方案二:布隆过滤器

2.2.2.1 什么是布隆过滤器

2.2.2.2 布隆过滤器中是怎么利用位数组表示一个集合的(理解)

传统集合 vs. Bloom Filter 中的位数组

位数组表示集合的本质

2.2.2.3 布隆过滤器解决缓存击穿原理

2.2.2.4 优缺点:  

2.2.2.5 特点:

2.2.2.6 实现方案

2.2.2.7 Redisson实现布隆过滤器结合自己项目说明(todo待更详细的结合自己项目业务说明):

2.3 面试回答:

3.什么是缓存击穿?怎么解决?

3.1 什么是缓存击穿?

3.2 怎么解决?

3.2.1 互斥锁

互斥锁特点:

代码实现

单机

集群

3.2.2 逻辑过期

逻辑过期特点:

代码实现:

原理:

为什么设置逻辑过期时间?

逻辑过期 vs Redis 过期

saveShopRedis 方法中的过期时间设置

逻辑过期时间的意义:

单机代码:

集群代码

3.2.3 热点key永不过期

3.3 面试回答:

4. 什么是缓存雪崩?怎么解决?

4.1 什么是缓存雪崩?

4.2 怎么解决?

4.3面试回答

4.4 打油诗记忆


1.redis的使用场景

  • 结合项目业务回答哪里用到redis,用来解决什么问题
  • 缓存      穿透、击穿、雪崩、双写一致、持久化、数据过期、淘汰策略
  • 分布式锁   setnx  redisson

2.什么是缓存穿透?怎么解决?

2.1 什么是缓存穿透

  • 缓存穿透:缓存穿透是指当查询的数据在缓存中不存在时,大量的请求会直接访问数据库,导致数据库压力增大。
  •  网友回答:

简述:查询一个一定不存在的数据,由于缓存不命中,每次都会去查询数据库,导致缓存失去意义,一般是被攻击时

详解:缓存穿透指在高并发场景下,如果某一个 key 被高并发访问,没有被命中,出于对容错性考虑,会尝试去从后端数据库中获取,从而导致了大量请求到达数据库,而当该 key 对应的数据库本身value就是空的情况下,这就导致数据库中并发的去执行了很多不必要的查询操作,从而导致巨大冲击和压力。

2.2 解决方案

  • 解决方案一:缓存空数据
  • 解决方案二:布隆过滤器

2.2.1 方案一:缓存空数据

原理:

缓存空数据,查询返回的数据为空,仍把这个空结果 {key:1,value:null} 进行缓存 

优缺点:

优点:简单

缺点:消耗内存,可能会发生不一致的问题

2.2.2 方案二:布隆过滤器

2.2.2.1 什么是布隆过滤器

Bloom Filter是一种空间效率很高的概率型数据结构,利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。

Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。

2.2.2.2 布隆过滤器中是怎么利用位数组表示一个集合的(理解)

传统集合 vs. Bloom Filter 中的位数组
  1. 传统集合

    • 一个传统的集合会直接存储所有元素,比如 {x1, x2, x3}。你可以很容易地检查一个元素是否在集合中,因为集合直接存储了这些元素。
  2. Bloom Filter 中的位数组

    • 在 Bloom Filter 中,位数组并不直接存储元素,而是用来标记元素在集合中的“存在性”。
    • 当你将元素 x1 加入到集合时,通过哈希函数将 x1 映射到位数组中的某些位置,并将这些位置的值设为 1。这些被设为 1 的位置就是 x1 在集合中的代表
    • 同样地,当你加入 x2 时,哈希函数也会将 x2 映射到位数组中的某些位置,并将这些位置设为 1。

    通过这种方式,Bloom Filter 并没有存储具体的元素,而是通过位数组的某些位被置为 1 来表示集合中可能包含哪些元素。

位数组表示集合的本质
  • 位数组本质上是一个标记器,用来记录哪些元素通过哈希函数映射到哪些位置。
  • 集合在 Bloom Filter 中是一种概率性的表示,不存储实际的元素,只记录哪些位置已经被使用(设为 1),从而间接地表达集合中可能包含的元素。

        因此,位数组并没有直接代替集合存储,而是通过一组哈希函数将集合中元素的位置信息记录在位数组中。位数组的每一位(bit)都表示集合中的某种状态,而不是具体的元素值。

2.2.2.3 布隆过滤器解决缓存击穿原理
  • 简述:

用布隆过滤器在访问缓存之前进行一次快速判断,过滤掉不可能存在的数据请求。

优点:内存占用较少,没有多余key

缺点:实现复杂,存在误判(假阳)

  • 详解:

BF是由一个长度为m比特的位数组(bit array)k个哈希函数(hash function)组成的数据结构。位数组均初始化为0,所有哈希函数都可以分别把输入数据尽量均匀地散列。

  • 当要插入一个元素时,将其数据分别输入k个哈希函数,产生k个哈希值。以哈希值作为位数组中的下标,将所有k个对应的比特置为1。
  • 当要查询(即判断是否存在)一个元素时,同样将其数据输入哈希函数,然后检查对应的k个比特。如果有任意一个比特为0,表明该元素一定不在集合中。如果所有比特均为1,表明该集合有(较大的)可能性在集合中。
  •   为什么不是一定在集合中呢?因为一个比特被置为1有可能会受到其他元素的影响,这就是所谓“假阳性”(false positive)。相对地,“假阴性”(false negative)在BF中是绝不会出现的。

  下图示出一个m=18, k=3的BF示例。集合中的x、y、z三个元素通过3个不同的哈希函数散列到位数组中。当查询元素w时,因为有一个比特为0,因此w不在该集合中。

            

2.2.2.4 优缺点:  

BF的优点是显而易见的:

  • 不需要存储数据本身,只用比特表示,因此空间占用相对于传统方式有巨大的优势,并且能够保密数据;
  • 时间效率也较高,插入和查询的时间复杂度均为O(k);
  • 哈希函数之间相互独立,可以在硬件指令层面并行计算。

  但是,它的缺点也同样明显:

  • 存在假阳性的概率,不适用于任何要求100%准确率的情境;
  • 只能插入和查询元素,不能删除元素,这与产生假阳性的原因是相同的。我们可以简单地想到通过计数(即将一个比特扩展为计数值)来记录元素数,但仍然无法保证删除的元素一定在集合中。

2.2.2.5 特点:

布隆过滤器(Bloom Filter)的假阳性问题是无法彻底解决的,只能通过以下特点降低概率

特点:

  • 哈希函数个数k越多,假阳性概率越低;
  • 位数组长度m越大,假阳性概率越低;
  • 已插入元素的个数n越大,假阳性概率越高。
2.2.2.6 实现方案

Redisson:

Guava:

实现示例:

https://www.cnblogs.com/jing99/p/12882259.html

2.2.2.7 Redisson实现布隆过滤器结合自己项目说明(todo待更详细的结合自己项目业务说明):

布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是 redisson实现的布隆过滤器。 它的底层主要是先去初始化一个比较大数组,里面存放的二进制0或1。在一 开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据 的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一 个key的存在。查找的过程也是一样的。 当然是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置 这个误判率,大概不会超过5%,其实这个误判是必然存在的,要不就得增 加数组的长度,其实已经算是很划算了

todo待更详细的结合业务说明

2.3 面试回答:

        缓存穿透是指查询一个一定不存在的数据,如果从存储层查不到数据则不写 入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。

         解决方案的话,我们通常都会用布隆过滤器来解决它。 布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是 redisson实现的布隆过滤器。 它的底层主要是先去初始化一个比较大数组,里面存放的二进制0或1。在一 开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据 的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一 个key的存在。查找的过程也是一样的。 当然是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置 这个误判率,大概不会超过5%,其实这个误判是必然存在的,要不就得增 加数组的长度,其实已经算是很划分了,5%以内的误判率一般的项目也能 接受,不至于高并发下压倒数据库。

3.什么是缓存击穿?怎么解决?

3.1 什么是缓存击穿?

缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这些并发的请求可能会瞬间把DB(DataBase)压垮

缓存击穿指的是某个热门的缓存键在过期后,同时有大量并发请求到达,导致所有请求都穿透缓存直接访问数据库

3.2 怎么解决?

解决方案一:互斥锁

解决方案二:逻辑过期

解决方案三:热点key永不过期(需谨慎)

3.2.1 互斥锁

上图中 3休眠一会儿,再重试  具体是哪些操作

  • 休眠一会儿(sleep)以等待锁的释放。
  • 重新查询缓存,看看缓存是否已经由线程1重建完成。
    • 如果缓存已经重建,线程2直接返回数据。
    • 如果缓存仍未命中,则再尝试获取锁。
    • 发现第一个线程已获取锁,继续休眠....直到锁的释放
互斥锁特点:
  • 强一致:缓存为建立时别的线程不会返回过期数据,而是被阻塞
  • 性能差:当大量线程同时访问失效的缓存时,只有一个线程能够获取锁并进行缓存重建,其他线程会被阻塞,直到锁释放。这种阻塞会导致这些线程无法立即得到响应,从而拖累整个系统的响应时间,尤其在高并发场景下,性能瓶颈更为明显。

代码实现
单机

封装两个方法,一个写key来尝试获取锁另一个删key来释放锁

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

/**
 * 释放锁
 *
 * @param key
 */
private void unlock(String key) {
    stringRedisTemplate.delete(key);
}

在并行情况下每当其他线程想要获取锁,来访问缓存都要通过将自己的key写到tryLock()方法里,setIfAbsent()返回false则说明有线程在在更新缓存数据,锁未释放。若返回true则说明当前线程拿到锁了可以访问缓存甚至操作缓存。
我们在下面一个热门的查询场景中用代码用代码来实现互斥锁解决缓存击穿
                       

    /**
     * 解决缓存击穿的互斥锁
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1.从Redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);  //JSON格式
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) { //不为空就返回 此工具类API会判断" "为false
            //存在则直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            //return Result.ok(shop);
            return shop;
        }
        //3.判断是否为空值 这里过滤 " "的情况,不用担心会一直触发这个条件因为他有TTL
        if (shopJson != null) {
            //返回一个空值
            return null;
        }
        //4.缓存重建 Redis中值为null的情况
        //4.1获得互斥锁
        String lockKey = "lock:shop"+id;
        Shop shopById=null;
        try {
            boolean isLock = tryLock(lockKey);
            //4.2判断是否获取成功
            if (!isLock){
                //4.3失败,则休眠并重试
                Thread.sleep(50);
               return queryWithMutex(id);
            }
            //4.4成功,根据id查询数据库
            shopById = getById(id);
            //5.不存在则返回错误
            if (shopById == null) {
                //将空值写入Redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                //为什么这里要存一个" "这是因为如果后续DB中有数据补充的话还可以去重建缓存
                //return Result.fail("暂无该商铺信息");
                return null;
            }
            //6.存在,写入Redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopById), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //7.释放互斥锁
            unlock(lockKey);
        }

        return shopById;
    }

例子参考原文链接:https://blog.csdn.net/weixin_57535055/article/details/128572301

集群

分布式锁:这里使用redission客户端实现,获取锁 trylock() 释放锁unlock()的方法不用我们编写,可直接使用redission中的API方法

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;

public class ShopService {

    @Autowired
    private RedissonClient redissonClient;  // 注入 Redisson 客户端

    /**
     * 解决缓存击穿的互斥锁,使用 Redisson 分布式锁
     * @param id 商铺ID
     * @return Shop 对象
     */
    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;
        
        // 1. 从Redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);  // 获取JSON格式的商铺信息
        
        // 2. 判断是否存在缓存数据
        if (StrUtil.isNotBlank(shopJson)) {  // 不为空直接返回商铺信息
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        
        // 3. 如果缓存中存在空值,直接返回空
        if (shopJson != null) {
            return null;
        }

        // 4. 如果缓存未命中,则尝试加锁进行缓存重建
        String lockKey = "lock:shop" + id;
        RLock lock = redissonClient.getLock(lockKey);  // 获取Redisson锁对象

        Shop shopById = null;
        try {
            // 4.1 尝试获取锁,最多等待100毫秒,锁定时间10秒
            boolean isLock = lock.tryLock(100, 10, TimeUnit.SECONDS);

            // 4.2 如果获取失败,稍等一会儿重试
            if (!isLock) {
                Thread.sleep(50);
                return queryWithMutex(id);  // 递归调用
            }

            // 4.3 成功获取锁后,根据id查询数据库
            shopById = getById(id);
            
            // 5. 如果数据库中也没有对应数据,写入空值到缓存并返回
            if (shopById == null) {
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            
            // 6. 数据库中存在数据,将其写入Redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopById), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 7. 无论如何都要释放锁
            lock.unlock();
        }

        return shopById;
    }
}

3.2.2 逻辑过期

工作原理:逻辑过期是在缓存中保存数据的同时,也保存一个过期时间戳。在客户端或应用程序访问缓存时,即使数据已经“过期”,缓存仍然会返回过期的数据,同时异步触发后台线程或任务去更新缓存。这样,即使缓存的数据过期,用户仍然能够立即获取到旧数据,从而避免了缓存失效引发的大量数据库查询,保证系统的高可用性。

逻辑过期特点:
  • 高可用:即使缓存中的数据已经过期,系统仍然能够继续返回过期数据,避免了高并发场景下大量请求同时穿透缓存、直达数据库的问题。这减少了对数据库的压力,从而保持系统的稳定性和高可用性。

  • 性能优:
    • 异步更新:逻辑过期允许缓存中的数据在过期后仍然被使用,这意味着在数据更新的同时,应用程序不必等待新数据的生成或数据库查询的完成。更新操作可以在后台异步执行,这样前台请求不受影响,提升了系统的响应速度。

    • 减少锁争用:与互斥锁相比,逻辑过期避免了多个线程在缓存失效时争抢锁的情况,从而减少了锁争用和线程阻塞问题。这种机制有效提高了系统的并发处理能力,尤其是在高并发场景下,能够显著提升性能。

  • 不能保证数据绝对一致
代码实现:
原理:

逻辑过期不是真正的缓存时间过期,而是对于对应的Key我们并不需要去设置TTL(Time-To-Live,存活时间),而是通过业务逻辑来达到一个类似于“过期”的效果。其本质还是限制落到数据库的请求数量!但前提是牺牲一致性保证可用性,

还是上一个业务的接口,通过使用逻辑过期来解决缓存击穿:

这样一来,缓存基本是会被命中的,因为我没有给缓存设置任何过期时间,并且对于Key的set都是事先选择好的,如果出现未命中的情况基本可以判断他不在选择之内,这样我就可以直接返回错误信息。那么对于命中的情况,就需要先判断逻辑时间是否过期,根据结果再来决定是否进行缓存重建。而这里的逻辑时间就是减少大量请求落到数据库的一个“关口”

看完上面这一段,相信大家还很迷惑。既然没有设置过期时间,那你为什么还要判断逻辑过期时间,怎么还存在过不过期的问题?

逻辑过期的意思不是不设置过期时间吗?上面代码中的saveShopRedis(Long id, Long expireTime)方法中的redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));为什么设置过期时间呢?


其实,这里所谓的逻辑过期时间只是一个类的属性字段,根本没有上升到Redis,上升到缓存的层面,是用来辅助判断查询对象的,也就是说,所谓的过期时间与缓存数据是剥离开的,所以根本不存在缓存过期的问题,自然数据库也不会有压力。

此例子参考原文链接:https://blog.csdn.net/weixin_57535055/article/details/128572301

自己的理解:

为什么设置逻辑过期时间?

在逻辑过期方案中,逻辑过期时间的作用是用于标记缓存数据是否已经过期。虽然数据本身仍然保留在 Redis 中,但是如果逻辑过期时间已经过去,我们就认为这条缓存是过期的,需要进行缓存重建。

逻辑过期 vs Redis 过期
  • 逻辑过期:缓存数据不会被 Redis 自动删除,应用程序通过检查 expireTime 来判断是否需要重建缓存。即使数据过期,后续请求也可以获取到旧的过期数据,避免阻塞。
  • Redis 过期:如果使用 Redis 的 TTL 机制,过期后数据会被 Redis 自动删除,后续请求可能会导致缓存穿透或击穿。
saveShopRedis 方法中的过期时间设置

saveShopRedis(Long id, Long expireTime) 方法中的 redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime)); 是为了设置一个逻辑上的过期时间,这个过期时间仅用于标识数据什么时候需要进行缓存重建。

LocalDateTime.now() 是当前的时间,而 plusSeconds(expireTime) 是在当前时间的基础上加上一个延迟时间expireTime(即未来某个时间点),这个时间点表示数据在这个时间之后会被逻辑上视为过期。

逻辑过期时间的意义

每次保存缓存数据时,我们不是直接把当前时间作为过期时间,而是基于当前时间计算一个未来的时间(LocalDateTime.now().plusSeconds(expireTime))。在未来这个时间点之后,系统会认为缓存过期,但数据仍然保留在 Redis 中,可以继续返回,直到缓存重建。

这样做的好处是:

  1. 即使缓存已经逻辑过期,数据仍然保留在 Redis 中,后续的请求可以立即返回过期数据,避免频繁访问数据库。
  2. 当有请求发现缓存已经逻辑过期时,系统会启动一个独立线程来异步更新缓存,而不阻塞其他请求。
单机代码:

为了尽可能地贴合开闭原则,不采用继承的方式来扩展原实体的属性而是通过组合的形式。

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;  //这里用Object是因为以后可能还要缓存别的数据
}

封装一个方法用来模拟更新逻辑过期时间与缓存的数据在测试类里运行起来达到数据与热的效果

/**
 * 添加逻辑过期时间
 *
 * @param id
 * @param expireTime
 */
public void saveShopRedis(Long id, Long expireTime) {
    //查询店铺信息
    Shop shop = getById(id);
    //封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
    //将封装过期时间和商铺数据的对象写入Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

查询接口:

/**
 * 逻辑过期解决缓存击穿
 *
 * @param id
 * @return
 */
public Shop queryWithLogicalExpire(Long id) throws InterruptedException {
    String key = CACHE_SHOP_KEY + id;
    Thread.sleep(200);
    //1.从Redis查询缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);  //JSON格式
    //2.判断是否存在
    if (StrUtil.isBlank(shopJson)) {
        //不存在则直接返回
        return null;
    }
    //3.判断是否为空值
    if (shopJson != null) {
        //返回一个空值
        //return Result.fail("店铺不存在!");
        return null;
    }
    //4.命中
    //4.1将JSON反序列化为对象
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    //4.2判断是否过期
    if (expireTime.isAfter(LocalDateTime.now())) {
        //5.未过期则返回店铺信息
        return shop;
    }
    //6.过期则缓存重建
    //6.1获取互斥锁
    String LockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(LockKey);
    //6.2判断是否成功获得锁
    if (isLock) {
        //6.3成功,开启独立线程,实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                //重建缓存
                this.saveShop2Redis(id, 20L);

            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                //释放锁
                unlock(LockKey);
            }
        });
    }
    //6.4返回商铺信息
    return shop;
}
集群代码
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;

public class ShopService {

    @Autowired
    private RedissonClient redissonClient;  // 注入 Redisson 客户端

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 解决缓存击穿的逻辑过期方案,使用 Redisson 分布式锁
     * @param id 商铺ID
     * @return Shop 对象
     */
    public Shop queryWithLogicalExpire(Long id) {
        String key = CACHE_SHOP_KEY + id;
        
        // 1. 从Redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);  // 获取JSON格式的商铺信息
        
        // 2. 判断是否存在缓存数据
        if (StrUtil.isBlank(shopJson)) {  // 如果缓存不存在,直接返回null
            return null;
        }

        // 3. 解析缓存中的数据
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();

        // 4. 判断缓存是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 4.1 缓存未过期,直接返回数据
            return shop;
        }

        // 5. 缓存已过期,尝试获取锁进行缓存重建
        String lockKey = "lock:shop" + id;
        RLock lock = redissonClient.getLock(lockKey);  // 获取Redisson锁对象

        boolean isLock = false;
        try {
            // 5.1 尝试获取锁
            isLock = lock.tryLock();

            // 5.2 如果获取锁失败,直接返回过期的数据(不阻塞其他请求)
            if (!isLock) {
                return shop;  // 返回过期数据
            }

            // 5.3 获取锁成功,查询数据库并重建缓存
            shop = getById(id);

            // 6. 如果数据库中没有数据,将空值写入Redis
            if (shop == null) {
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }

            // 7. 构建新的缓存数据,并更新Redis
            redisData.setData(shop);
            redisData.setExpireTime(LocalDateTime.now().plusMinutes(CACHE_SHOP_TTL));  // 更新过期时间
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));

        } finally {
            // 8. 释放锁
            if (isLock) {
                lock.unlock();
            }
        }
        //别的请求获取锁失败,直接返回过期数据
        return shop;
    }
}

3.2.3 热点key永不过期

        设置热点 Key 永不过期:对于确定为热点 Key 的数据,可以设置其永不过期。但这种方法需要谨慎使用,因为它可能会导致数据的更新不及时。例如,对于一些很少更新但被频繁访问的系统配置信息 Key,可以采用这种方式。

3.3 面试回答:

        第一种方案可以使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓 存,否则重试get缓存的方法

        第二种方案可以设置当前key逻辑过期,大概是思路如下: ①:在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前 key设置过期时间 ②:当查询的时候,从redis取出数据后判断时间是否过期 ③:如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新

        当然两种方案各有利弊: 如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么 高,锁需要等,也有可能产生死锁的问题 如果选择key的逻辑删除,则优先考虑的高可用性,性能比较高,但是数据 同步这块做不到强一致。

4. 什么是缓存雪崩?怎么解决?

4.1 什么是缓存雪崩?

缓存雪崩 是指在某一时刻,大量缓存数据共同过期,导致瞬时大流量涌向数据库。或者由于分布式缓存节点故障导致缓存失效。

4.2 怎么解决?

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性        哨兵模式、集群模式
  • 给缓存业务添加降级限流策略              ngxin或spring cloud gateway
  • 给业务添加多级缓存                            Guava或Caffeine
  • 缓存预热 预加载:在系统启动时预先加载常用数据到缓存中,避免高并发时缓存未命中。

降级可做为系统的保底策略,适用于穿透、击穿、雪崩

4.3面试回答

缓存雪崩意思是设置缓存时采用了相同的过期时间,导致缓存在某一时刻同 时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。

与缓存击穿的区别: 雪崩是很多key,击穿是某一个key缓存。

解决方案主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基 础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重 复率就会降低,就很难引发集体失效的事件

4.4 打油诗记忆

todo注意面试时要结合项目 业务回答 哪里用到了,为了解决什么问题,带来了什么影响?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值