Redis缓存更新策略以及常见缓存问题

news2025/2/2 18:53:47

在这里插入图片描述

文章目录

  • 一、什么是缓存?
  • 二、添加Redis缓存
  • 三、缓存更新策略
  • 四、缓存穿透
  • 五、缓存雪崩
  • 六、缓存击穿
  • 七、缓存工具封装


一、什么是缓存?

缓存就是数据交换的缓冲区(Cache),是存储数据的临时地方,一般读写性能较好,常见缓存:
在这里插入图片描述
Web应用中缓存有什么作用呢?

  • 降低后端负载
  • 提高读写效率,降低响应时间

缓存的成本:

  • 数据的一致性成本
  • 代码维护成本
  • 运维成本

二、添加Redis缓存

缓存作用模型:
在这里插入图片描述
给一段Redis作为缓存的具体案例代码:

public Shop queryWithPassThrough(Long id) {
        // 1. 从Redis 查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        // 2. 判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 存在返回
            Shop shop = JSONUtil.toBean(shopJson,Shop.class);
            return shop;
        }
        // 不存在,根据id查询数据库
        Shop shop = getById(id);
        // 数据库中不存在 直接返回
        if(shop == null) {
            return null;
        }
        // 存在 写入redis
        stringRedisTemplate.opsForValue().set("cache:shop:" + id,JSONUtil.toJsonStr(shop));
        return shop;
    }

三、缓存更新策略

我们在使用Redis作为缓存,可以大大降低数据库的负载压力,但是也会带来缓存一致性问题(Redis数据与数据库数据不一样),以下为三种常见的缓存更新策略:

内存淘汰超时剔除主动更新
说明不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存给缓存添加TTL时间,到期后自动删除缓存,下次查询时更新缓存编写业务逻辑,再修改数据库时,更新缓存
一致性一般
维护成本

业务场景:

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

主动更新策略:

  1. Cache Aside Pattern:由缓存的调用者,在更新数据库时同时更新缓存
  2. Read/Write Through Pattern:缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题
  3. Write Behind Caching Pattern:调用者只操作缓存,用其他线程异步将缓存持久化到数据库,保证最终一致

综合比较一般使用的还是一种Cache Aside Pattern策略,但是在操作缓存和数据库时有三个问题需要考虑:
1.删除缓存还是更新缓存?
更新缓存:每次更新数据库都更新缓存,无效写操作较多
删除缓存:更新数据库时让缓存失效,查询时再更新缓存(选择的方案)

2.如何保证缓存与数据库的操作的同时成功或失败?
单体系统,将缓存与数据库操作放在一个事务
分布式系统,利用TTC等分布式事务方案

3.先操作缓存还是先操作数据库?
其实两种方案都可以,哪一个更优呢?我们来对比一下:
1.先删除缓存在操作数据库:
在这里插入图片描述
这种情况,缓存和数据库都是20,一致性没有问题
在这里插入图片描述
但是上述情况就产生了数据不一致情况,上述场景的情况发生的概率还是比较高的,因为更新数据库是相对较慢的,而查询缓存,写缓存速度是相对较快的就会出现上述情况

2.先操作数据库,再删除缓存
在这里插入图片描述
上述这种情况是不存在一致性问题的
在这里插入图片描述
上述这种情况是存在一致性问题,但需要几个条件,线程一恰好缓存失效,并且在查询完数据库与写入缓存之间完成更慢的更新数据库与删除缓存操作,这种可能性是更低的

缓存更新策略的最佳实践方案:
低一致性需求:使用Redis自带的内存淘汰机制

高一致性需求:主动更新,并以超时剔除作为兜底方案
读操作:

  • 缓存命中之间返回
  • 缓存未命中则查询数据库,并写入缓存,并设定超时时间

写操作:

  • 先写数据库,然后再删除缓存
  • 要确保数据库与缓存操作的原子性

写操作:

	@Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if(id == null) {
            return Result.fail("店铺id不能为空");
        }
        // 1. 更新数据库
        updateById(shop);
        // 2. 删除缓存
        stringRedisTemplate.delete("cache:shop:"+id);
        return Result.ok();
    }

我们需要加上@Transactional事务注解来保证数据库与缓存操作的原子性

四、缓存穿透

缓存穿透:指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远都不生效,这样大量的请求都会打到数据库,给数据库造成巨大压力
在这里插入图片描述
常见的两种解决方案:
缓存空对象
这是一种简单暴力的方法,当请求缓存与数据库中都不存在的数据时往缓存中写一份空数据,防止给数据库造成巨大压力
优点:实现简单,维护方便
缺点:额外的内存消耗,可能造成短期的不一致(可以设置一个TTL)
在这里插入图片描述
布隆过滤
布隆过滤器通过为位数据和多个哈希函数来实现判断,当请求查询缓存前会先对请求关键字进行布隆过滤器的判断,如果不存在直接返回,不再查询缓存或数据库
优点:内存占用较少,没有多余key
缺点:实现复杂,存在误判可能
在这里插入图片描述
缓存穿透解决方案:
在这里插入图片描述

public Shop queryWithPassThrough(Long id) {
        // 1. 从Redis 查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        // 2. 判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 存在返回
            Shop shop = JSONUtil.toBean(shopJson,Shop.class);
            return shop;
        }
        // 判断命中的是否为空值
        if(shopJson != null) {
            return null;
        }
        // 不存在,根据id查询数据库
        Shop shop = getById(id);
        // 数据库中不存在 存放空值
        if(shop == null) {
            // 将空值写入Redis
            stringRedisTemplate.opsForValue().set("cache:shop:" + id,"",2,TimeUnit.MINUTES);
            return null;
        }
        // 存在 写入redis
        stringRedisTemplate.opsForValue().set("cache:shop:" + id,JSONUtil.toJsonStr(shop),30, TimeUnit.MINUTES);
        return shop;
    }

当访问缓存和数据库都不存在的数据时,往缓存中写入一个空数据,并设置TTL。

缓存穿透解决方案有哪些?

  • 缓存null值
  • 布隆过滤
  • 增强id复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

五、缓存雪崩

缓存雪崩:指在同一时刻大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力
在这里插入图片描述
面对这种同一时刻大量缓存失效或者Redis服务宕机的情况,我们有以下几种解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级

六、缓存击穿

缓存击穿:也叫热点Key问题,就是一个被高并发访问并且缓存重建业务比较复杂的Key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
在这里插入图片描述
常见的解决方案:

  1. 互斥锁,该方案最大的问题是线程都在相互等待

在这里插入图片描述

  1. 逻辑过期,设置一个expire字段,比较这个字段与当前时间判断是否过期
    在这里插入图片描述
解决方案优点缺点
互斥锁没有额外的内存消耗 保证一致性 实现简单县城需要等待,性能受到影响 可能有死锁风险
逻辑过期线程无需等待,性能较好不保证一致性 有额外内存消耗 实现复杂

利用互斥锁解决缓存击穿问题
这里我们使用什么互斥锁呢?其实Redis中的setnx命令就是一个不错的选择
在这里插入图片描述

// 加锁解锁
	private boolean tryLock(String key) {
        Boolean flg = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flg);
    }

    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }
// 缓存击穿
    public Shop queryWithMutex(Long id){
        // 1. 从Redis 查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        // 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;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2失败,休眠重试
            if (!isLock) {
               Thread.sleep(50);
               return queryWithMutex(id);
            }
            // 4.3 成功,根据id查询数据库
            shop = getById(id);
            if(shop == null) {
                // 将空值写入Redis
                stringRedisTemplate.opsForValue().set("cache:shop:" + id,"",2,TimeUnit.MINUTES);
                return null;
            }
            // 存在 写入redis
            stringRedisTemplate.opsForValue().set("cache:shop:" + id,JSONUtil.toJsonStr(shop),30, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            unLock(lockKey);
        }
        return shop;
    }

基于逻辑过期方式解决缓存击穿问题:
在这里插入图片描述
那既然我们要加一个逻辑字段,我们是怎么加,直接在实体类中增加吗?让实体类继承带有expire属性的类?这两种都不太好,因为都会都实体类进行修改。我们选择的是组合方式

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}
// 写入数据时 + 一个逻辑过期字段
public 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
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(redisData));
    }
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    public Shop queryWithLogicalExpire(Long id) {
        // 1. 从Redis 查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        // 2. 判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            // 不存在返回
            return null;
        }
        // 4. 命中,先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(),Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5. 判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())) {
            // 5.1 未过期 返回店铺信息
            return shop;
        }
        // 5.2 已过期, 缓存重建
        // 6 缓存重建
        String lockKey = "lock:shop:" + id;
        boolean isLock = tryLock(lockKey);
        // 6.1 获取互斥锁
        // 6.2 判断是否获取成功
        if (isLock) {
            // 6.3 成功,开启独立线程 实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                // 重建缓存
                try {
                    saveShop2Redis(id,30L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unLock(lockKey);
                }
            });
        }
        // 6.4 返回过期商铺信息
        return shop;
    }

七、缓存工具封装

基于StringRedisTemplate封装一个缓存工具类,满足下列要求:

@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
}

1.将任意的Java对象序列化为json并存储在String类型的key中,并且可以设置TTL超时时间

	public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

2.将任意的Java对象序列化为json并存储在String类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题

public void setWithLogicalExpire(String key,Object value,Long time,TimeUnit unit) {
        // 设置逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        redisData.setData(value);
        // 写入Redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }

3.根据指定的key进行查询缓存,并反序列化为指定类型,利用缓存的空值方式解决缓存穿透问题

public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit) {
        String key = keyPrefix + id;
        // 从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 判断是否存在
        if (StrUtil.isNotBlank(json)) {
            // 存在 直接返回
            return JSONUtil.toBean(json,type);
        }
        // 判断命中的是否为空值
        if (json != null) {
            return null;
        }
        // 不存在,根据id查询数据库
        R r = dbFallback.apply(id);
        // 不存在 将空值写入Redis
        if(r == null) {
            stringRedisTemplate.opsForValue().set(key,"",time,unit);
        }
        // 存在 写入redis
        set(key,r,time,unit);
        return r;
    }

4.根据指定的key查询缓存,并反序列化为指定类型,利用逻辑过期时间解决缓存击穿问题

private boolean tryLock(String key) {
        Boolean flg = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flg);
    }

    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    public <R,ID> R queryWithLogicalExpire(String keyPrefix,String lockPrefix,ID id,Class<R> type, Function<ID,R> dbFallback,
    Long time,TimeUnit unit) {
        String key = keyPrefix + 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);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(),type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5. 判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())) {
            // 5.1 未过期 返回店铺信息
            return r;
        }
        // 5.2 已过期, 缓存重建
        // 6 缓存重建
        String lockKey = lockPrefix + id;
        boolean isLock = tryLock(lockKey);
        // 6.1 获取互斥锁
        // 6.2 判断是否获取成功
        if (isLock) {
            // 6.3 成功,开启独立线程 实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                // 重建缓存
                try {
                    // 查数据库
                    R r1 = dbFallback.apply(id);
                    // 写 Redis
                    setWithLogicalExpire(key,r1,time,unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unLock(lockKey);
                }
            });
        }
        // 6.4 返回过期商铺信息
        return r;
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/762414.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mac环境配置(Java)----使用bash_profile进行配置

1、打开软件--终端 2、首先查看本机Java的安装地址&#xff08;系统默认的&#xff09; /usr/libexec/java_home -V 查看到Java8安装的路径如下&#xff1a; 3、如果是第一次配置环境变量&#xff0c;使用命令&#xff1a;【touch .bash_profile】创建一个.bash_profile隐藏…

多表查询例题

目录 创建学生表 创建分数表 查看表结构 向两张表插入数据 1.查间student表的昕有记录 ​2.查间student表的第2条到4条记录 ​3.从student表查间所有学生的学号(id)、姓名(name)和院系(department)的信息 ​4.从student表中查间计算机系和英语系的学生的信息 ​5.从stu…

免费nas diy

安装Debian11 * 安装时选择ssh-server服务 * 安装时选中non-free contrib 还有bullseye-backports non-free 可以安装闭源驱动 bullseye-backports 可以更新内核 安装完成后,编辑/etc/apt/sources.list文件,关闭光驱源 并 切换到国内源 安装新内核6.1,重启后,可以移除老内…

监控指标111

P95、P99.9百分位数值——服务响应时间的重要衡量指标 开发技术 开发技术 2021-02-01 388次浏览 前段时间&#xff0c;在对系统进行改版后&#xff0c;经常会有用户投诉说页面响应较慢&#xff0c;我们看了看监控数据&#xff0c;发现从接口响应时间的平均值来看在500ms左右&…

数据结构--线性表以及其顺序存储结构

这里写目录标题 线性表的定义和特征定义特征 案例引入稀疏多项式链表实现多项式相加小结 线性表的类型定义&#xff08;抽象数据类型&#xff09;定义格式基本操作小结 线性表的顺序表示和实现实现1顺序存储表示顺序表中元素存储位置的计算 实现2顺序表的优点问题出现结构体表示…

你需要跟踪分析产品知识库的哪些关键数据

当你拥有一个产品知识库时&#xff0c;了解如何跟踪和分析关键数据指标是非常重要的。这些数据可以提供有关知识库使用情况、用户行为和满意度的宝贵见解。 在本文中&#xff0c;我们将探讨你需要跟踪的几个重要数据指标&#xff0c;以帮助你优化和改进产品知识库的效果和价值。…

大语言模型评估全解:评估流程、评估方法及常见问题

编者按&#xff1a;随着对大语言模型&#xff08;LLM&#xff09;评估领域的深入研究&#xff0c;我们更加清楚地认识到全面理解评估过程中的问题对于有效评估LLM至关重要。 本文探讨了机器学习模型评估中出现的常见问题&#xff0c;并深入研究了LLM对模型评估领域带来的重大挑…

Blazor 自定义可重用基础组件之 Dialog (Bootstrap Modal)

对话框是常用的组件之一&#xff0c;可以提供信息提示&#xff0c;也可以设置表单录入数据等。但是&#xff0c;Bootstrap Modal需要JS互操作&#xff0c;这个不太懂&#xff0c;只能绕过。这里没有一句JS代码&#xff0c;非常好用。 以下是具体代码&#xff1a; &#xfeff…

Drools用户手册翻译——第三章 构建,部署,应用和运行(六)剩余部分

终于是把这一章给看完了&#xff0c;看完也有点懵&#xff0c;需要重新梳理实践一下&#xff0c;最主要是概念有些多&#xff0c;不过还好&#xff0c;多用一用就明白了。 甩锅声明&#xff1a;本人英语一般&#xff0c;翻译只是为了做个笔记&#xff0c;所以有翻译错误的地方…

SciencePub学术 | 物联网类重点SCIEEI征稿中

SciencePub学术 刊源推荐: 物联网类重点SCIE&EI征稿中&#xff01;CCF推荐&#xff0c;对国人友好&#xff01;信息如下&#xff0c;录满为止&#xff1a; 一、期刊概况&#xff1a; 物联网类重点SCIE&EI 【期刊简介】IF&#xff1a;7.0-7.5&#xff0c;JCR1区&#…

算法训练营第四十一天||● 343. 整数拆分 96.不同的二叉搜索树

● 343. 整数拆分 这道有难度&#xff0c;不看题解肯定 想不到用动态规划&#xff0c;看了题解后能大概明白&#xff0c;但还不是很清晰&#xff0c;不太明白递推公式中强调的与dp[i]还要比较一次&#xff0c;也不明白第一次去最大最的那个比较 需要后面继续看 动规五部曲&a…

深度强化学习落地方法论训练篇:PPO、DQN、DDPG、学习率、折扣因子等

为了保证 DRL 算法能够顺利收敛,policy 性能达标并具有实用价值,结果有说服力且能复现,需要算法工作者在训练前、训练中和训练后提供全方位一条龙服务。我记得 GANs 刚火起来的时候,因为训练难度高,有人在 GitHub 上专门开了 repository,总结来自学术界和工业界的最新训练…

麦穗检测Y8S

采用YOLOV8训练&#xff0c;得到PT模型&#xff0c;然后直接转ONNX&#xff0c;使用OPENCV的DNN&#xff0c;不需要其他依赖&#xff0c;支持C/PYTHON 麦穗检测Y8S

CodeGeex论文阅读

《CodeGeeX: A Pre-Trained Model for Code Generation with Multilingual Evaluations on HumanEval-X》 论文地址&#xff1a;https://arxiv.org/pdf/2303.17568.pdf 代码地址&#xff1a;https://github.com/THUDM/CodeGe 一、简介 CodeGeeX&#xff0c;是一个具有130亿…

<数据结构>NO10.快速排序|递归|非递归|优化

文章目录 快速排序递归实现快速排序hoare版本DigHole版本前后指针版本 非递归实现快速排序算法优化1. 针对有序数组进行优化2. 针对全相等数组进行优化 算法分析时间复杂度空间复杂度 快速排序 快速排序&#xff08;英语&#xff1a;Quicksort&#xff09;&#xff0c;又称分区…

0基础学习VR全景平台篇 第64篇:高级功能-自定义LOGO和密码访问

一、功能说明 VR视频的高级功能目前有两项&#xff0c;分别是自定义LOGO和密码访问。 二、后台编辑界面 1、自定义LOGO&#xff1a;支持JPG、PNG、GIF格式的图片&#xff0c;大小不超过5M&#xff0c;建议高度不超过500px&#xff0c;设置后显示在VR视频的左上角位置。 2、密…

Vue学习随堂记录

计算属性和监听器 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </he…

MobPush:Android SDK 集成指南

开发工具&#xff1a;Android Studio 集成方式&#xff1a;Gradle在线集成 安卓版本支持&#xff1a;minSdkVersion 19 集成准备 注册账号 使用PushSDK之前&#xff0c;需要先在MobTech官网注册开发者账号&#xff0c;并获取MobTech提供的AppKey和AppSecret&#xff0c;详情可…

《程序是怎样跑起来的》简介

目录 1. 前言2. 主要内容3. 总结 1. 前言 闲暇之余&#xff0c;读了一遍《程序是怎样跑起来的》这本书。颇感欣喜。借此机会分享一下。 本书可以这样定位&#xff1a; 对学生&#xff1a;作为专业课之前的开胃菜&#xff0c;非常合适&#xff0c;尤其是作为《计算机组成原理…

华为OD机试真题 Java 实现【最少数量线段覆盖】【2023Q1 200分】,附详细解题思路

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路四、Java算法源码五、效果展示1、输入2、输出3、说明4、复杂一点5、理性分析一下 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#xff09;》。 刷的越多&#xff…