redis 使用互斥锁或逻辑过期两种方案解决缓存击穿,和缓存穿透(用缓存空值 或布隆过滤器)的解决方案

news/2024/5/19 5:29:47

缓存穿透
        缓存穿透是指在缓存中查找一个不存在的值,由于缓存一般不会存储这种无效的数据,所以每次查询都会落到数据库上,导致数据库压力增大,严重时可能会导致数据库宕机。
解决方案:
缓存空值 (本文此方案)
2 布隆过滤器
3 增强id的复杂度
4 做好数据的基础格式校验
5 做好热点参数的限流


缓存击穿
        缓存击穿是指一个被频繁访问(高并发访问并且缓存重建业务较复杂)的缓存键因为过期失效,同时又有大量并发请求访问此键,导致请求直接落到数据库或后端服务上,增加了系统的负载并可能导致系统崩溃 
解决方案
互斥锁
逻辑过期


1 前提先好做redis与springboot的集成,redisson的集成【用于加锁解锁】【本文用的redisson】
   另外用到了hutool的依赖


2 缓存对象封装的类,这里只是逻辑过期方案可以用上,你也可以自己改

/*** 决缓存击穿--(设置逻辑过期时间)*/
@Data
public class RedisData {//逻辑过期时间private LocalDateTime expireTime;//缓存实际的内容private Object data;
}

3 相关的常量

public class Constant {//缓存空值的ttl时间public static final Long CACHE_NULL_TTL = 2L;//缓存时间,单位程序里参数传public static final Long CACHE_NEWS_TTL = 10L;//缓存前缀,根据模块来public static final String CACHE_NEWS_KEY = "cache:news:";//锁-前缀,根据模块来public static final String LOCK_NEWS_KEY = "lock:news:";//持有锁的时间public static final Long LOCK_TTL = 10L;
}

4 缓存核心类

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;import static org.example.service_a.cache.Constant.CACHE_NULL_TTL;
import static org.example.service_a.cache.Constant.LOCK_NEWS_KEY;@Slf4j
@Component
//封装的将Java对象存进redis 的工具类
public class CacheClient {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedissonClient redissonClient;// 定义线程池private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);AtomicInteger atomicInteger = new AtomicInteger();/*** 设置TTL过期时间set** @param key* @param value* @param time* @param unit*/public void set(String key, Object value, Long time, TimeUnit unit) {// 需要把value序列化为string类型String jsonStr = JSONUtil.toJsonStr(value);stringRedisTemplate.opsForValue().set(key, jsonStr, time, unit);}/*** 缓存穿透功能封装** @param id* @return*/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);}// 这里要先判断命中的是否是null,因为是null的话也是被上面逻辑判断为不存在// 这里要做缓存穿透处理,所以要对null多做一次判断,如果命中的是null则shopJson为""if ("".equals(Json)) {return null;}//4. 不存在,根据id查询数据库R r = dbFallback.apply(id);log.error("查询数据库次数 {}",atomicInteger.incrementAndGet());if (r == null) {//5. 不存在,将null写入redis,以便下次继续查询缓存时,如果还是查询空值可以直接返回false信息stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//6. 存在,写入Redisthis.set(key, r, time, unit);//7. 返回return r;}/*** 解决缓存击穿--(互斥锁)* @param keyPrefix* @param id* @param type* @param dbFallback* @param time* @param unit* @return* @param <R>* @param <ID>*/public <R, ID> R queryWithMutex(String keyPrefix, 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.isNotBlank(shopJson)) {// 3.存在,直接返回return JSONUtil.toBean(shopJson, type);}// 判断命中的是否是空值if (shopJson != null) {// 返回一个错误信息return null;}log.error("缓存重建----");// 4.实现缓存重建// 4.1.获取互斥锁String lockKey = LOCK_NEWS_KEY + id;R r = null;try {boolean isLock = tryLock(lockKey);// 4.2.判断是否获取成功if (!isLock) {// 4.3.获取锁失败,休眠并重试Thread.sleep(10);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}// 4.4.获取锁成功,根据id查询数据库r = dbFallback.apply(id);log.info("查询数据库次数 {}",atomicInteger.incrementAndGet());// 5.不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 7.释放锁unLock(lockKey);}// 8.返回return r;}/*** --------------注意 key 没有加过期时间,会一直存在,只是 缓存的内容里有个字段,标识了过期的时间----------------* 设置逻辑过期set** @param key* @param value* @param time* @param chronoUnit*/public void setWithLogicExpire(String key, Object value, Long time, ChronoUnit chronoUnit) {// 设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plus(time, chronoUnit));// 需要把value序列化为string类型String jsonStr = JSONUtil.toJsonStr(redisData);stringRedisTemplate.opsForValue().set(key, jsonStr);}/*** 解决缓存击穿--(设置逻辑过期时间)方式* 1. 组合键名,从Redis查询缓存。* 2. 缓存不存在,直接返回(预设热点数据已预热)。* 3. 解析缓存内容,获取过期时间。* 4. 若未过期,直接返回数据。* 5. 已过期,执行缓存重建流程:* a. 尝试获取互斥锁。* b. 二次检查缓存是否已重建且未过期,若是则返回数据。* c. 成功获取锁,异步执行:* i. 查询数据库获取最新数据。* ii. 重新写入Redis缓存,附带新的逻辑过期时间。* iii. 最终释放锁。* 6. 未能获取锁,直接返回旧数据。** @param id* @return*/public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, ChronoUnit chronoUnit) throws InterruptedException {String key = keyPrefix + id;//1. 从Redis中查询缓存String Json = stringRedisTemplate.opsForValue().get(key);//2. 判断是否存在if (StrUtil.isBlank(Json)) {//3. 不存在,直接返回(这里做的是热点key,先要预热,所以已经假定热点key已经在缓存中)return null;}//4. 存在,需要判断过期时间,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(Json, RedisData.class);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();//5. 判断是否过期if (expireTime.isAfter(LocalDateTime.now())) {//5.1 未过期,直接返回店铺信息return r;}log.error("缓存内容已逻辑过期-----------{}",LocalDateTime.now());//5.2 已过期,需要缓存重建//6. 缓存重建//6.1 获取互斥锁String lockKey = LOCK_NEWS_KEY + id;//6.2 判断是否获取锁成功boolean isLock = tryLock(lockKey);if (isLock) {// 二次验证是否过期,防止多线程下出现缓存重建多次String Json2 = stringRedisTemplate.opsForValue().get(key);// 这里假定key存在,所以不做存在校验// 存在,需要判断过期时间,需要先把json反序列化为对象RedisData redisData2 = JSONUtil.toBean(Json2, RedisData.class);R r2 = JSONUtil.toBean((JSONObject) redisData2.getData(), type);LocalDateTime expireTime2 = redisData2.getExpireTime();if (expireTime2.isAfter(LocalDateTime.now())) {// 未过期,直接返回店铺信息return r2;}//6.3 成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 重建缓存,这里设置的值小一点,方便观察程序执行效果,实际开发应该设为30min// 查询数据库R apply = dbFallback.apply(id);log.info("查询数据库次数 {}",atomicInteger.incrementAndGet());// 写入redisthis.setWithLogicExpire(key, apply, time, chronoUnit);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放锁unLock(lockKey);}});}//7. 返回,如果没有获得互斥锁,会直接返回旧数据return r;}/*** 加锁* @param lockKey* @return*/private boolean tryLock(String lockKey) {RLock lock = redissonClient.getLock(lockKey);try {// 尝试获取锁,最多等待10秒,获取到锁后自动 LOCK_SHOP_TTL 0秒后解锁return lock.tryLock(10, Constant.LOCK_TTL, TimeUnit.SECONDS);} catch (Exception e) {Thread.currentThread().interrupt();// 重新抛出中断异常log.error("获取锁时发生中断异常", e);return false;}}/*** 解锁* @param lockKey*/private void unLock(String lockKey) {RLock lock = redissonClient.getLock(lockKey);lock.unlock(); // 解锁操作}}

5 缓存预热和测试

import cn.hutool.json.JSONUtil;
import org.example.common.AppResult;
import org.example.common.AppResultBuilder;
import org.example.service_a.cache.CacheClient;
import org.example.service_a.cache.RedisData;
import org.example.service_a.domain.News;
import org.example.service_a.service.NewsService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;import static org.example.service_a.cache.Constant.CACHE_NEWS_KEY;
import static org.example.service_a.cache.Constant.CACHE_NEWS_TTL;@RestController
@Validated()
@RequestMapping("/article")
public class News_Controller {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate CacheClient cacheClient;@Autowiredprivate NewsService newsService;@Autowiredprivate RedissonClient redissonClient;/*** @param id 编号*/@RequestMapping("/get/{id}")public AppResult<News> getGirl(@PathVariable("id") Long id) throws InterruptedException {//解决缓存穿透-------->News news = cacheClient.queryWithPassThrough(CACHE_NEWS_KEY, id, News.class,newsService::getById,CACHE_NEWS_TTL, TimeUnit.MINUTES);//(互斥锁)解决缓存击穿---------->
//        News news = cacheClient.queryWithMutex(CACHE_NEWS_KEY, id, News.class,
//                (x) -> {
//                    return newsService.getById(id);
//                }
//                , CACHE_NEWS_TTL, TimeUnit.MINUTES);//(设置逻辑过期时间)解决缓存击穿---------->
//        News news = cacheClient.queryWithLogicalExpire(
//                CACHE_NEWS_KEY,
//                id,
//                News.class,
//                (x)->{
//                    return newsService.getById(id);
//                },
//                CACHE_NEWS_TTL,
//                ChronoUnit.SECONDS);System.out.println("news = " + news);//判断返回值是否为空
//        if (news == null) {
//            return Result.fail("信息不存在");
//        }
//        //返回
//        return Result.ok(news);return AppResultBuilder.success(news);}/***缓存预热*/@PostConstruct()public void cache_init() {RLock lock = redissonClient.getLock("lock:cacheInit");lock.lock();try {List<News> list = newsService.list();redisTemplate.executePipelined(new SessionCallback<Object>() {HashMap<String, Object> objectObjectHashMap = new HashMap<>();@Overridepublic <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {list.forEach(news -> {//演示缓存击穿--逻辑过期 用这种方式
//                        RedisData redisData = new RedisData();
//                        redisData.setData(news);
//                        redisData.setExpireTime(LocalDateTime.now().plusSeconds(30));
//                        objectObjectHashMap.put(CACHE_NEWS_KEY +news.getId(),JSONUtil.toJsonStr(redisData));//演示缓存击穿--互斥锁 用这种方式objectObjectHashMap.put(CACHE_NEWS_KEY + news.getId(), JSONUtil.toJsonStr(news));});operations.opsForValue().multiSet((Map<? extends K, ? extends V>) objectObjectHashMap);return null;}});} catch (Exception e) {throw new RuntimeException(e);} finally {lock.unlock();}}
}


http://www.mrgr.cn/p/51288348

相关文章

谷歌上架,为什么会触发填表单,可以避免吗?怎么填表单可以提高通过率?

在谷歌上架过程中&#xff0c;相信大部分开发者都有收到过谷歌发来表单填写的邮件通知&#xff0c;要求开发者们在14天内根据表单要求回复关于应用部分情况。邮件如图&#xff1a; 根据触发填表单的开发者分享的经验来看&#xff0c;填完表之后出现的情况不尽相同&#xff0c;且…

WDS+MDT网络启动自动部署windows(十五)使用it天空万能驱动

简介: 虽然我们可以使用dism这样的工具来备份驱动,并通过适当的厂家、型号来区分并自动注入驱动,它没万能驱动用着方便呀,还得去备份。 本文目标:在MDT部署时使用it天空的万能驱动。 下载 或许是我脑子坏掉了,印象中不是这个域名。 IT天空 - 新的十年,新的天空 (itsk.co…

农作物害虫检测数据集VOC+YOLO格式18975张97类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;18975 标注数量(xml文件个数)&#xff1a;18975 标注数量(txt文件个数)&#xff1a;18975 标…

一篇文章 学会Qt 样式表(qss)

QML 中风格和主题的设计可以通过配置文件选择现有几种中的一种&#xff0c;或者直接在控件定义时&#xff0c;指定其属性&#xff0c;如背景颜色或者字体大小。在QWidget框架中&#xff0c;则通过了一种叫做qss样式表的东西来进行描述&#xff0c;跟CSS逻辑上类似。 这个qss抽…

Flask表单详解

Flask表单详解 概述跨站请求伪造保护表单类把表单渲染成HTML在视图函数中处理表单重定向和用户会话Flash消息 概述 尽管 Flask 的请求对象提供的信息足够用于处理 Web 表单&#xff0c;但有些任务很单调&#xff0c;而且要重复操作。比如&#xff0c;生成表单的 HTML 代码和验…

使用docker-compose编排Lnmp(dockerfile) 完成Wordpress

目录 一、 Docker-Compose 1.1Docker-Compose介绍 1.2环境准备 1.2.1准备容器目录及相关文件 1.2.2关闭防火墙关闭防护 1.2.3下载centos:7镜像 1.3Docker-Compose 编排nginx 1.3.1切换工作目录 1.3.2编写 Dockerfile 文件 1.3.3修改nginx.conf配置文件 1.4Docker-Co…

UI-Diffuser——使用生成式扩散模型的UI原型设计算法解析

概述。 移动UI是影响参与度的一个重要因素&#xff0c;例如用户对应用的熟悉程度和使用的便利性。如果你有一个类似的应用程序&#xff0c;你可能会选择一个具有现代、好看的设计的应用程序&#xff0c;而不是一个旧的设计。然而&#xff0c;要从头开始研究什么样的UI最适合应…

5月4(信息差)

&#x1f384; HDMI ARC国产双精度浮点dsp杜比数码7.1声道解码AC3/dts/AAC环绕声光纤、同轴、USB输入解码板KC33C &#x1f30d; 国铁集团回应高铁票价将上涨 https://finance.eastmoney.com/a/202405043066422773.html ✨ 源代码管理平台GitLab发布人工智能编程助手DuoCha…

10G MAC层设计系列-(4)MAC TX模块

一、前言 MAC TX模块就是要将IP层传输过来的数据封装前导码、MAC地址、帧类型以及进行CRC校验&#xff0c;并与CRC值一块组成以太网帧。 二、模块设计 首先对输入的数据进行缓存&#xff0c;原因是在之后要进行封装MAC帧头&#xff0c;所以需要控制数据流的流动 FIFO_DATA_6…

论文辅助笔记:TimeLLM

1 __init__ 2 forward 3 FlattenHead 4 ReprogrammingLayer

Spring Cloud学习笔记(Hystrix):基本知识和代码示例

这是本人学习的总结&#xff0c;主要学习资料如下 - 马士兵教育 1、Hystrix简介2、Hystrix架构2.1、Hytrix的入口2.2、toObservable()流程 3、Hsytrix的简单样例3.1、dependency3.2、代码样例 1、Hystrix简介 Hytrix是用于处理处理延迟和容错的开源库&#xff0c;包含服务隔离…

W801学习笔记十九:古诗学习应用——下

经过前两章的内容&#xff0c;背唐诗的功能基本可以使用了。然而&#xff0c;仅有一种模式未免显得过于单一。因此&#xff0c;在本章中对其进行扩展&#xff0c;增加几种不同的玩法&#xff0c;并且这几种玩法将采用完全不同的判断方式。 玩法一&#xff1a;三分钟限时挑战—…

Stability AI 推出稳定音频 2.0:为创作者提供先进的 AI 生成音频

概述 Stability AI 的发布再次突破了创新的界限。这一尖端模型以其前身的成功为基础&#xff0c;引入了一系列突破性的功能&#xff0c;有望彻底改变艺术家和音乐家创建和操作音频内容的方式。 Stable Audio 2.0 代表了人工智能生成音频发展的一个重要里程碑&#xff0c;为质量…

[1678]旅游景点信息Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 旅游景点信息管理系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql…

芯片与扫地机器人

芯片与扫地机器人石头科技在V20扫地机器人上采用了石头系列产品中首创的“PreciSense创新石头星阵领航系统”避障方案,即3D ToF+RGB的动态避障模式,这也是石头首款搭载“双光源固态激光雷达导航避障”的扫地机器人产品。石头V20的避障系统由两颗可以实现38400Hz的超精准建图采…

[软件工具]批量根据文件名查找PDF文件复制到指定的地方,如何批量查找文件复制,多个文件一起查找复制

多个文件目录下有多个PDF, 如何根据文件名一个清单&#xff0c;一次性查找多个PDF复制保存 如图所示下面有7个文件夹&#xff0c;每个文件夹里面有几百上千PDF文件 如何从上千个PDF文件中一次性快速找到我们要的文件呢 &#xff1f; 我们需要找到文件名是这样的PDF&#xff0…

阿里面试:事务ACID,底层是如何实现的?

文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 : 免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备 免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,…

1. 深度学习笔记--神经网络中常见的激活函数

1. 介绍 每个激活函数的输入都是一个数字&#xff0c;然后对其进行某种固定的数学操作。激活函数给神经元引入了非线性因素&#xff0c;如果不用激活函数的话&#xff0c;无论神经网络有多少层&#xff0c;输出都是输入的线性组合。激活函数的意义在于它能够引入非线性特性&am…

14_Scala面向对象编程_属性

文章目录 属性1.类中属性声明2.系统默认赋值3.BeanProperty4.整体代码如下 属性 1.类中属性声明 // 1.给Scala声明属性&#xff1b;var name :String "zhangsan"val age :Int 302.系统默认赋值 scala由于初始化变量必须赋值&#xff0c;为了解决此问题可以采…

二、VLAN原理和配置

vlan不是协议&#xff0c;是一个技术&#xff0c;虚拟局域网技术&#xff0c;基于802.1q协议。 vlan&#xff08;虚拟局域网&#xff09;&#xff0c;将一个物理的局域网在逻辑上划分成多个广播域的技术。 目录 1.冲突域和广播域 概念 范围 2.以太网帧格式 3.以太网帧封装…