Redis项目:缓存

devtools/2025/3/19 8:20:54/

黑马点评缓存部分:

缓存的标准操作方式就是查询数据库之前先查询缓存,如果缓存数据存在,则直接从缓存中返回,如果缓存数据不存在,再查询数据库,然后将数据存入redis

最初缓存版本

java">//最初源代码public Shop queryWithPassThrough(Long id) {//从redis中查商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(SHOP_ID + id);//判断是否存在if (StrUtil.isNotBlank(shopJson)) {Shop shop = JSONUtil.toBean(shopJson, Shop.class);//存在 直接返回return null;}//判断是不是空if (shopJson != null) {//返回错误信息return null;}//不存在  查数据库 MybatisPlusShop shop = getById(id);//数据库不存在 返回错误if (shop == null) {//将空值写入redisstringRedisTemplate.opsForValue().set(SHOP_ID + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//数据库中存在  写入redisstringRedisTemplate.opsForValue().set(SHOP_ID + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);//返回return null;}

解决数据不一致的办法:双写

由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在, 几种方案

Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案

Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理

Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致 

如果采用第一个方案,那么假设我们每次操作数据库后,都操作缓存,但是中间如果没有人查询,那么这个更新动作实际上只有最后一次生效,中间的更新动作意义并不大,我们可以把缓存删除,等待再次查询时,将缓存中的数据加载出来

我们应当是先操作数据库,再删除缓存,原因在于,如果你选择先删除缓存再操作数据库,在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来,他查询缓存数据并不存在,此时他写入缓存,当他写入缓存后,线程1再执行更新动作时,实际上写入的就是旧的数据,新的数据被旧数据覆盖了。

缓存穿透问题:缓存空对象

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

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

  • 缓存null值

  • 布隆过滤

  • 增强id的复杂度,避免被猜测id规律

  • 做好数据的基础格式校验

  • 加强用户权限校验

  • 做好热点参数的限流 

这里使用缓存null值来完成缓存穿透

java">public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){String key = keyPrefix + id;// 1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(json)) {// 3.存在,直接返回return JSONUtil.toBean(json, type);}// 判断命中的是否是空值if (json != null) {// 返回一个错误信息return null;}// 4.不存在,根据id查询数据库R r = dbFallback.apply(id);// 5.不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);return r;}

缓存雪崩

热点Key问题

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

解决方案:

  • 给不同的Key的TTL添加随机值

  • 利用Redis集群提高服务的可用性

  • 缓存业务添加降级限流策略

  • 给业务添加多级缓存

缓存雪崩的解决方案:互斥锁

代码:

java">//互斥锁public Shop queryWithMutex(Long id) {//1.从redis中查商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(SHOP_ID + id);//判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在 直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判断是不是空if (shopJson != null) {//返回错误信息return null;}//实现缓存重建// 获取互斥锁String lockKey = SHOP_LOCK_KEY + id;Shop shop = null;try {boolean isLock = tryLock(lockKey);// 判断是否成功if (!isLock) {//失败->休眠并重试Thread.sleep(50);queryWithMutex(id);}//成功->查询数据库信息shop = getById(id);//模拟延迟//Thread.sleep(200);//数据库不存在 返回错误if (shop == null) {//将空值写入redisstringRedisTemplate.opsForValue().set(SHOP_ID + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//数据库中存在  写入redisstringRedisTemplate.opsForValue().set(SHOP_ID + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);//释放互斥锁} catch (InterruptedException e) {throw new RuntimeException(e);} finally {unLock(lockKey);}//返回return shop;}// 获取锁private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);//返回的啥时候拆箱 可能出现空指针return BooleanUtil.isTrue(flag);}//释放锁private void unLock(String key) {stringRedisTemplate.delete(key);}

互斥锁的大致逻辑:

缓存查询+缓存重建

先从 Redis 中查询缓存数据。

如果缓存命中(数据存在),直接返回缓存数据。

如果缓存未命中(数据不存在),尝试获取分布式锁,防止多个线程同时重建缓存

如果没有得到锁,休眠 重试

如果得到了锁 查询数据库信息 

数据库信息不存在 将空值写入redis 

数据库中存在 写入redis

最后无论如何释放锁 返回数据

 DoubleCheck: 为了避免多个线程同时重建缓存,从而减少不必要的数据库查询和缓存写入操作

在这里需要加入这个doubleCheck

在获取锁之前,先检查一次缓存(第一次检查)。如果缓存已经命中,直接返回数据,无需获取锁。这样可以避免大量线程同时竞争锁,减少锁的开销。

在获取锁之后、查询数据库之前,再次检查缓存(第二次检查)。如果缓存已经命中,直接返回数据,无需查询数据库。这样可以确保在等待锁的过程中,其他线程没有已经重建缓存,从而避免重复操作。(上述代码没写,逻辑过期的写了)

第二次检查防止数据更新,即使锁了,其他线程或进程仍然可以 通过其他途径更新缓存,而不受当前锁的限制

缓存雪崩的解决方案:逻辑过期

java"> private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);//热点key 逻辑public Shop queryWithLogicalExpire(Long id) {String key = CACHE_SHOP_KEY + id;String json = stringRedisTemplate.opsForValue().get(key);//从redis中查商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(SHOP_ID + id);//判断是否存在if (StrUtil.isBlank(shopJson)) {//存在 直接返回return null;}//命中 需要把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);//redisData.getData()是一个Object对象 需要,但实际上是一个JSONObjectShop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();//判断过期if (expireTime.isAfter(LocalDateTime.now())) {//没过期return shop;}//过期了需要缓存重建String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);//判断是否获取锁成功if (isLock) {//!!!做DoubleCheckString jsonAgain = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(jsonAgain)) {RedisData redisDataAgain = JSONUtil.toBean(jsonAgain, RedisData.class);LocalDateTime expireTimeAgain = redisDataAgain.getExpireTime();if (expireTimeAgain.isAfter(LocalDateTime.now())) {return JSONUtil.toBean((JSONObject) redisDataAgain.getData(), Shop.class);}}// 缓存仍需重建,异步执行CACHE_REBUILD_EXECUTOR.submit(() -> {try {this.saveShop2Redis(id, 20L);} catch (Exception e) {throw new RuntimeException(e);} finally {unLock(lockKey);}});}return shop;}
//缓存重建
public void saveShop2Redis(Long id, Long expireSeconds) {//查询店铺信息Shop shop = getById(id);//封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//写入RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));}

实现逻辑过期,需要创建一个逻辑过期数据类 记录过期时间的一个类

java">@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}

逻辑:

和前面一样,先从redis中查缓存 存在就返回数据

在返回数据的时候 先将json反序列化成RedisData对象 再将RedisData 中的data转换成shop,真正需要的对象

然后先判断数据是否过期 没过期返回

过期了需要进行缓存重建

重建还是先拿到锁,再做DoubleCheck 如果不需要继续重建,返回对象

如果确实需要重建 这里采用的是异步执行 

        1、使用线程池 CACHE_REBUILD_EXECUTOR 提交一个异步任务。

        2、在异步任务中,调用 saveShop2Redis 方法重建缓存

        3、无论缓存重建是否成功,最终都会释放锁。

最后返回数据

值得注意的是,在执行之前需要对缓存数据进行预热 将数据加载到缓存中。


http://www.ppmy.cn/devtools/168286.html

相关文章

Java面试八股—Redis篇

一、Redis的使用场景 &#xff08;一&#xff09;缓存 1.Redis使用场景缓存 场景&#xff1a;缓存热点数据&#xff08;如用户信息、商品详情&#xff09;&#xff0c;减少数据库访问压力&#xff0c;提升响应速度。 2.缓存穿透 正常的访问是&#xff1a;根据ID查询文章&…

4.3--入门知识扫盲,IPv4的头部报文解析,数据报分片,地址分类(包你看一遍全部记住)

IPv4协议&#xff1a;网络世界的快递包裹指南&#xff08;附拆箱说明书&#xff09; “IPv4就像一张明信片&#xff0c;既要写清楚地址&#xff0c;又要控制大小别超重” —— 某网络工程师的桌面铭牌 一、IPv4报头&#xff1a;快递面单的终极艺术 1.1 报头结构图&#xff08;…

MyBatis (三)关联查询

目录 一 学习自定义结果集 1 驼峰命名规则 2 自定义映射规则 二 指定一对一的关联封装进行查询 三 指定一对多的关联封装进行查询 一 学习自定义结果集 解决和数据库对不上的数据被封装为空&#xff1a; 1 驼峰命名规则 在xml文件中&#xff1a; # 启用驼峰命名自动转换…

饮食巧搭配,助力老人对抗进行性核上性麻痹

进行性核上性麻痹是一种较为罕见且复杂的神经系统退行性疾病&#xff0c;严重影响老人的生活自理能力与生活质量。在治疗的同时&#xff0c;合理的饮食搭配对缓解症状、维持身体机能至关重要。 由于疾病导致老人身体机能衰退&#xff0c;日常活动能力下降&#xff0c;却仍需充足…

【Spring】第三弹:基于 XML 获取 Bean 对象

一、获取 Bean 对象 1.1 根据名称获取 Bean 对象 由于 id 属性指定了 bean 的唯一标识&#xff0c;所以根据 bean 标签的 id 属性可以精确获取到一个组件对象。 1.确保存在一个测试类&#xff1a; public class HelloWorld {public void sayHello(){System.out.println(&quo…

linux 基础网络配置文件

使用“ifconfig”命令查看网络接口地址 直接执行“iconfg”命令后可以看到ens33、10、virbr0这3个网络接口的信息&#xff0c;具体命令如下 ifconfig ##查看网络接口地址 ens33:第一块以太网卡的名称 lo:“回环”网络接口 virbr0:虚拟网桥的连接接口 查看指…

理解Akamai EdgeGrid认证在REST API中的应用

在我们高度互联的世界中&#xff0c;快速且安全地将内容传递给用户是重中之重。Akamai 就是应运而生的佼佼者。作为内容分发和云服务的领导者&#xff0c;他们提供了一个名为 EdgeGrid 的平台&#xff0c;帮助使您的Web应用更加快速、可靠和安全。 但是&#xff0c;强大的功能伴…

两款软件助力图片视频去水印及图像编辑

今天给大家分享两款呼声很高的软件&#xff0c;它们都能处理图片和视频去水印相关的问题。其中一款软件在去水印的同时&#xff0c;图像编辑功能也十分出色&#xff1b;另一款软件专注于图片和视频去水印&#xff0c;去除效果好且支持批量处理。下面就来详细了解一下。 Remover…