Redis缓存穿透,雪崩,击穿

什么是缓存

缓存就是数据交换的缓冲区(Cache),是存储数据的临时地方,读写性能较高

缓存的优点:

1.降低数据库的负载(优点片面,这里以传统来解释)

2.提高读写效率,降低响应时间

缓存的成本:(也不能说缺点,成本比较合适)

1.需要解决数据一致性问题

2.代码的维护成本提高

查询时候的缓存模型

例如原先查询商品信息,通过Mybatis-plus直接从数据库查询,那么现在需要改写这个逻辑

开始改写逻辑

这里我们使用RedisTemplate<String,Object> 需要配置一下 如果使用的是StringRedisTemplate则不需要配置 注意如果使用RedisTemplate不加泛型(默认就是Object) 或者RedisTemplate<Object,Object> 也可以不配置,只不过key value看不懂

配置RedisTemplate<String,Object>的序列化方式

/**
 * @author hrui
 * @date 2025/1/28 18:41
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 创建 RedisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        // 设置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 设置序列化工具
        Jackson2JsonRedisSerializer<Object> j2jrs = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 解决jackson无法反序列化LocalDateTime的问题
        om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        om.registerModule(new JavaTimeModule());
        om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        j2jrs.setObjectMapper(om);


        // key 和 hashKey 采用 string 序列化
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());

        // value 和 hashValue 采用 JSON 序列化
        redisTemplate.setValueSerializer(j2jrs);
        redisTemplate.setHashValueSerializer(j2jrs);

        return redisTemplate;
    }
}

为商店添加缓存

第一次查询,缓存里没有,会查数据库,后面都是查缓存

解决时间格式问题问题

复制代码
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")

记得先删除Redis中原先的缓存

缓存更新策略

下面介绍下主动更新策略的三种模式

根据缓存更新策略修改

那么我们在修改和和删除的时候

缓存穿透及解决思路

什么是缓存穿透:

指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会请求数据库

布隆过滤器的简单理解:内部是个byte数组 会提前将数据库中的例如(id)存起来,通过某种算法实现,当布隆过滤器中说存在的时候,有可能不存在(并非100%准确),但是当布隆过滤器说不存在的时候,那么就真实的不存在 ---->也就是说有一定的穿透风险

修改原先的缓存逻辑

缓存雪崩及解决方案

什么是缓存雪崩:

是指在同一时间段内大量的key同时失效或者Redis服务宕机,导致大量请求到达数据库,给数据库带来巨大压力.

最简单常用的:错开TTL过期时间

耍酷的可以用些其他手段

缓存击穿及解决方案

什么是缓存击穿:

就是一个被高并发访问(并且缓存重建业务较为复杂)的key突然失效(到期)了,无数请求瞬间给数据库带来了巨大冲击

所谓的缓存重建业务比较复杂:例如需要多表查询,运算 可能建立起这个缓存业务需要几秒甚至更久

互斥锁和逻辑过期图解:都是为了解决缓存过期之后的高并发问题

互斥锁的不足是当缓存重建过程非常耗时的情况下,等待时间交久(当然有人会说,为什么不直接访问数据库???首先为什么需要缓存,使用缓存往往针对并发量非常高的场景的一种解决方案,当然如果你只是为了解决复杂业务查询而使用缓存,单纯为了加快响应速度,那么完全可以不需要等待,直接查询数据库)

优缺点

所谓出现死锁现象例如一个业务中有多个缓存查询需求,而另外一个业务里也有相应的缓存查询需求,你得到一把锁,后续缓存中其他缓存业务的锁在其他业务中,互相等待(你等着我解锁,我等着你的锁解决,好了,都等着吧,谁也解不了了),出现死锁现象(死锁的出现是有条件的,一般不会)

1.基于互斥锁解决缓存击穿问题

自定义互斥锁:可以参考Redis中的一种方式

setnx name hahaha 这条命令的意思是:当key为name不存在的时候,才会在redis中设置值,如果为name的key已经存在,则不会执行

也就是说只有第一个线程可以去执行这个操作,加锁就是设置这个key,释放锁,就是删除这个key

那么就可以利用这种机制,来自定义一个互斥锁(这也是分布式锁的一个基本原理,当然真正的分布式锁比这个复杂)

但是这里要考虑一点,例如设置这把锁之后,中间程序出了问题,没有来得及删除这把锁,那么会导致这把锁永远存在,那么我们可以考虑设置过期时间(根据具体业务复杂度设置过期时间,一般业务1秒内肯定就可以完成,那么我们可以考虑设置过期时间5秒这样,看具体的),另外保险点,在finally中删除锁

代码

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    @Override
    public Result queryById(Long id) {
        //缓存穿透解决方案
        //Shop shop = queryWithPassThrough(id);
        //使用互斥锁 解决缓存击穿
        Shop shop = queryWithMutex(id);
        if (shop == null){
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }
    //缓存击穿解决方案
    public Shop queryWithMutex(Long id) {
        String cacheKey = "cache:shop:" + id;
        // 1. 从缓存中查询
        Object cacheData = redisTemplate.opsForValue().get(cacheKey);
        // 2. 判断缓存是否存在
        if (cacheData != null) {
            if ("null".equals(cacheData)) {
                return null; // 缓存中存的是空值
            }
            return (Shop) cacheData; // 直接返回缓存中的 Shop 对象
        }
        Shop shop=null;
        try {
            //这里进行缓存击穿后的逻辑(重建缓存逻辑) 步骤是 拿锁 拿锁成功查询数据库 释放锁   拿锁不成功说明已经有线程在重建缓存 等待(并重新调用)
            boolean b = tryLock("lock" + id);
            if(!b){
                //休眠
                Thread.sleep(50);
                //递归 重试
                queryWithMutex(id);
            }
            //如果拿锁成功就会往下走
            // 3. 不存在则查询数据库
            shop = getById(id);
            // 4. 如果数据库中也没有这个数据,防止缓存穿透,存入 "null" 并设置短期过期时间
            if (shop == null) {
                redisTemplate.opsForValue().set(cacheKey, "null", 10L, TimeUnit.MINUTES);
                return null;
            }
            // 5. 存在则写入缓存,设置正常过期时间
            redisTemplate.opsForValue().set(cacheKey, shop, 30L, TimeUnit.MINUTES);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            unLock("lock" + id);
        }
        //释放锁
        return shop;
    }
    //缓存穿透解决方案
    public Shop queryWithPassThrough(Long id) {
        String cacheKey = "cache:shop:" + id;
        // 1. 从缓存中查询
        Object cacheData = redisTemplate.opsForValue().get(cacheKey);
        // 2. 判断缓存是否存在
        if (cacheData != null) {
            if ("null".equals(cacheData)) {
                return null; // 缓存中存的是空值
            }
            return (Shop) cacheData; // 直接返回缓存中的 Shop 对象
        }
        // 3. 不存在则查询数据库
        Shop shop = getById(id);

        // 4. 如果数据库中也没有这个数据,防止缓存穿透,存入 "null" 并设置短期过期时间
        if (shop == null) {
            redisTemplate.opsForValue().set(cacheKey, "null", 10L, TimeUnit.MINUTES);
            return null;
        }
        // 5. 存在则写入缓存,设置正常过期时间
        redisTemplate.opsForValue().set(cacheKey, shop, 30L, TimeUnit.MINUTES);
        return shop;
    }
    //获取锁
    private boolean tryLock(String key) {
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(flag);
    }
    //释放锁
    private void unLock(String key) {
        redisTemplate.delete(key);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result update(Shop shop) {
        //1.先修改数据库
        updateById(shop);
        //2.直接删除缓存
        redisTemplate.delete("cache:shop:" + shop.getId());
        return Result.ok();
    }

    public static void main(String[] args) {

    }
}
2.基于逻辑过期解决缓存击穿问题

要使用逻辑过期,那么首先需要加个字段来标识逻辑过期字段

可以设计一个实体类

复制代码
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

代码

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    //重建缓存的线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    @Override
    public Result queryById(Long id) {
        //缓存穿透解决方案
        //Shop shop = queryWithPassThrough(id);
        //使用互斥锁 解决缓存击穿
        //Shop shop = queryWithMutex(id);
        //使用逻辑过期解决缓存击穿
        Shop shop = queryWithLogicalExpire(id);
        if (shop == null){
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }
    //缓存击穿解决方案 使用逻辑过期
    //主线上找不到就返回null 不需要再判断空值
    public Shop queryWithLogicalExpire(Long id) {
        String cacheKey = "cache:shop:" + id;
        // 1. 从缓存中查询
        Object cacheData = redisTemplate.opsForValue().get(cacheKey);
        // 2. 判断缓存是否存在
        if (cacheData == null) {
            return null;
        }
        //命中,需要判断过期时间
        RedisData redisData = (RedisData) cacheData;
        Shop shop = (Shop) redisData.getData();
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {//没有过期
            return shop;
        }
        //过期 重建缓存 获取互斥锁
        if (tryLock("lock" + id)) {
            //如果成功 开启线程 重建缓存  获取锁成功理论上应该再次检查redis缓存是否过期 做DoubleCheck 如果存在则无需重建缓存
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //重建缓存
                    saveShop2Redis(id, 30L);
                }catch (Exception e){
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock("lock" + id);
                }
            });
        }
        return shop;
    }
    //缓存击穿解决方案 使用互斥锁
    public Shop queryWithMutex(Long id) {
        String cacheKey = "cache:shop:" + id;
        // 1. 从缓存中查询
        Object cacheData = redisTemplate.opsForValue().get(cacheKey);
        // 2. 判断缓存是否存在
        if (cacheData != null) {
            if ("null".equals(cacheData)) {
                return null; // 缓存中存的是空值
            }
            return (Shop) cacheData; // 直接返回缓存中的 Shop 对象
        }
        Shop shop=null;
        try {
            //这里进行缓存击穿后的逻辑(重建缓存逻辑) 步骤是 拿锁 拿锁成功查询数据库 释放锁   拿锁不成功说明已经有线程在重建缓存 等待(并重新调用)
            boolean b = tryLock("lock" + id);
            if(!b){
                //休眠
                Thread.sleep(50);
                //递归 重试
                queryWithMutex(id);
            }
            //如果拿锁成功就会往下走
            // 3. 不存在则查询数据库
            shop = getById(id);
            // 4. 如果数据库中也没有这个数据,防止缓存穿透,存入 "null" 并设置短期过期时间
            if (shop == null) {
                redisTemplate.opsForValue().set(cacheKey, "null", 10L, TimeUnit.MINUTES);
                return null;
            }
            // 5. 存在则写入缓存,设置正常过期时间
            redisTemplate.opsForValue().set(cacheKey, shop, 30L, TimeUnit.MINUTES);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            unLock("lock" + id);
        }
        //释放锁
        return shop;
    }
    //缓存穿透解决方案
    public Shop queryWithPassThrough(Long id) {
        String cacheKey = "cache:shop:" + id;
        // 1. 从缓存中查询
        Object cacheData = redisTemplate.opsForValue().get(cacheKey);
        // 2. 判断缓存是否存在
        if (cacheData != null) {
            if ("null".equals(cacheData)) {
                return null; // 缓存中存的是空值
            }
            return (Shop) cacheData; // 直接返回缓存中的 Shop 对象
        }
        // 3. 不存在则查询数据库
        Shop shop = getById(id);

        // 4. 如果数据库中也没有这个数据,防止缓存穿透,存入 "null" 并设置短期过期时间
        if (shop == null) {
            redisTemplate.opsForValue().set(cacheKey, "null", 10L, TimeUnit.MINUTES);
            return null;
        }
        // 5. 存在则写入缓存,设置正常过期时间
        redisTemplate.opsForValue().set(cacheKey, shop, 30L, TimeUnit.MINUTES);
        return shop;
    }
    //获取锁
    private boolean tryLock(String key) {
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(flag);
    }
    //释放锁
    private void unLock(String key) {
        redisTemplate.delete(key);
    }

    //逻辑过期解决缓存击穿 这个key是永久有效的用逻辑过期判断  好比缓存预热(启动时候就存进去了)
    private void saveShop2Redis(Long id,Long expireSeconds) {
        //1.查询店铺数据
        Shop shop = getById(id);
        //2.封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //3.写入redis
        redisTemplate.opsForValue().set("cache:shop:" + id, shop);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result update(Shop shop) {
        //1.先修改数据库
        updateById(shop);
        //2.直接删除缓存
        redisTemplate.delete("cache:shop:" + shop.getId());
        return Result.ok();
    }

    public static void main(String[] args) {

    }
}
相关推荐
希忘auto11 分钟前
详解Redis之事务
redis
yours_Gabriel14 分钟前
【Redis_1】初识Redis
数据库·redis·缓存
萝卜青今天也要开心1 小时前
读书笔记-《Redis设计与实现》(二)单机数据库实现(上)
java·数据库·redis·学习·缓存
liuhaikang2 小时前
鸿蒙HarmonyOS Next 视频边播放边缓存- OhosVideoCache
缓存·音视频·harmonyos
@Java小牛马2 小时前
Redis真的是单线程的吗?
数据库·redis·缓存·reactor·单线程·多线程
lingllllove7 小时前
ubuntu22.04防火墙策略
数据库·postgresql
程序猿小D10 小时前
第三百五十八节 JavaFX教程 - JavaFX滑块
java·前端·数据库
memorycx10 小时前
MySQL(3)
数据库·sql
My LQS10 小时前
使用 EXISTS 解决 SQL 中 IN 查询数量过多的问题
数据库·sql