redis解决缓存穿透/击穿/雪崩

文章目录

      • 1.缓存穿透
        • [1.1 概念](#1.1 概念)
        • [1.2 解决方案](#1.2 解决方案)
          • [1.2.1 缓存空对象](#1.2.1 缓存空对象)
          • [1.2.2 布隆过滤](#1.2.2 布隆过滤)
        • [1.2 店铺查询使用缓存穿透解决方案](#1.2 店铺查询使用缓存穿透解决方案)
          • [1.2.1 流程](#1.2.1 流程)
      • 2.缓存雪崩
        • [2.1 什么是缓存雪崩?](#2.1 什么是缓存雪崩?)
        • [2.2 雪崩解决方案](#2.2 雪崩解决方案)
      • 3.缓存击穿
        • [3.1 什么是缓存击穿?](#3.1 什么是缓存击穿?)
        • 3.2解决方案
          • [3.2.1 基于互斥锁解决缓存击穿问题(热点key问题)](#3.2.1 基于互斥锁解决缓存击穿问题(热点key问题))
          • [3.2.2 基于逻辑过期方式解决缓存击穿问题](#3.2.2 基于逻辑过期方式解决缓存击穿问题)

1.缓存穿透

1.1 概念

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

1.2 解决方案
1.2.1 缓存空对象

缺点:可能存在短期不一致。数据库新增了这个id对应的数据的时候,缓存中对应的是null

优点:简单

1.2.2 布隆过滤

布隆过滤存储的是一些二进制位,把数据库的数据通过哈希算法,计算出哈希值存储到布隆过滤器中

但是他说存在不一定存在,比如我数据库某条数据删除了,布隆过滤器没有更新

优点:内存占用少

缺点:实现复杂,存在误判

1.2 店铺查询使用缓存穿透解决方案
1.2.1 流程


代码实现

java 复制代码
public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY+id;
        // 1.从redis中查询店铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断缓存是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 判断命中的是否是空值
        if (shopJson != null) {
            // 返回错误信息
            return Result.fail("店铺不存在");
        }
        // 3.不存在,根据id查询数据库
        Shop shop = getById(id); // mybatisplus功能
        // 4.不存在,返回错误
        if (shop == null) {
            // 将空值写到redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return Result.fail("店铺数据不存在");
        }
        // 存在,写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

1.缓存穿透产生的原因?

用户请求的数据再数据库和缓存中都不存在,不断发起这样的请求对数据库带来了极大压力

2.缓存穿透的解决方案是什么?

缓存null值,布隆过滤,增加id复杂度,避免被猜测规律,做好基础数据检测校验,加强用户权限

2.缓存雪崩

2.1 什么是缓存雪崩?

缓存雪崩指的是同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力

2.2 雪崩解决方案

1.给不同的KEY的TTL添加随机值

2.利用Redis集群提高服务可用性(集群,哨兵,主从)

3.给缓存业务添加降级限流策略(某些服务不让请求)

4.给业务添加多级缓存(nginx,jvm,redis都加缓存)

3.缓存击穿

3.1 什么是缓存击穿?

也称为热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会瞬间给数据库带来极大冲击。

3.2解决方案

1.互斥锁

2.逻辑过期


3.2.1 基于互斥锁解决缓存击穿问题(热点key问题)
java 复制代码
 /*
    * redis锁逻辑
    * */
    // 1.获取锁
    public boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag); // Boolean是一个包装类型,可能为null
    }
    // 2.释放锁
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
     /*
     * 缓存击穿封装函数
     * */
    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY+id;
        // 1.从redis中查询店铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断缓存是否存在
        if (StrUtil.isNotBlank(shopJson)) { // 不为空值或者空字符串
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        // 判断命中的是否是空值
        if (shopJson != null) { // 这样判断他只能是空字符串了
            // 返回错误信息
            return null;
        }

        // 4.实现缓存重建
        // 4.1 获取互斥锁
        String lockKey = "lock:shop:" + id;
        Shop shop = null; // mybatisplus功能
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2 判断是否获取锁成功
            if (!isLock) {
                // 失败,休眠
                Thread.sleep(50);
                return queryWithMutex(id);
            }

            // 3.获取锁成功,根据id查询数据库
            // 模拟重建的延时
            Thread.sleep(200);
            shop = getById(id);
            // 4.不存在,返回错误
            if (shop == null) {
                // 将空值写到redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            // 存在,写入redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            unlock(lockKey);
        }

        return shop;
    }
3.2.2 基于逻辑过期方式解决缓存击穿问题

商品中原始没有逻辑过期的字段,需要自己自定义一个

java 复制代码
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data; // data存储shop数据
}

代码逻辑

java 复制代码
 /**
     * 存储店铺封装逻辑过期时间
     */
    public void saveShop2Redis(Long id, Long expireSeconds) {
        // 查询店铺数据
        Shop shop = getById(id);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        // 封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

    // 开启线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public Shop queryWithLogicalExpire(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1.从redis中查询店铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断缓存是否存在
        if (StrUtil.isBlank(shopJson)) {
            return null;
        }
        // 4.命中,需要把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //5 判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 5.1.未过期,直接返回店铺信息
            return shop;
        }
        // 5.2已过期,需要缓存重建
        String localKey = LOCK_SHOP_KEY+id;
        boolean isLock = tryLock(localKey);
        // 5.3 判断是否获取锁成功
        if (isLock) {
            // 6.3 开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                // 重建缓存
                try {
                    this.saveShop2Redis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unlock(localKey);
                }

            });
        }
        // 6.4 返回过期的店铺信息
        return shop;
    }
相关推荐
程序员皮皮林11 分钟前
Java 25 正式发布:更简洁、更高效、更现代!
java·开发语言·python
好家伙VCC14 分钟前
**发散创新:AI绘画编程探索与实践**随着人工智能技术的飞速发展,AI绘
java·人工智能·python·ai作画
勇者无畏40415 分钟前
基于 Spring AI Alibaba 搭建 Text-To-SQL 智能系统(前置介绍)
java·后端·spring·prompt·embedding
练习时长一年15 分钟前
IDEA开发常用快捷键总结
java·ide·intellij-idea
温柔532920 分钟前
仓颉语言异常捕获机制深度解析
java·服务器·前端
运维李哥不背锅31 分钟前
Ansible 的变量与模板:实现更灵活的自动化配置
java·自动化·ansible
运维李哥不背锅33 分钟前
Ansible 的条件语句与循环详解
数据库·ansible
信码由缰35 分钟前
Java 21 虚拟线程 vs 缓存线程池与固定线程池
java
踩坑小念40 分钟前
进程 线程 协程基本概念和区别 还有内在联系
java·linux·jvm·操作系统
yyongsheng43 分钟前
SpringBoot项目集成easy-es框架
java·服务器·前端