缓存穿透、缓存雪崩、缓存击穿解决方案
什么是缓存
缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。
添加 redis 缓存
给店铺类型查询业务添加缓存
需求:添加ShopTypeController中的queryTypeList方法,添加查询缓存
缓存更新策略
业务场景:
低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
操作缓存和数据库时有三个问题需要考虑:
1.删除缓存还是更新缓存
-
更新缓存:每次更新数据库都更新缓存,无效写操作较多(×)
-
删除缓存:更新数据库时让缓存失效,查询时再更新缓存(✓)
2.如何保证缓存与数据库的操作同时成功或失败
-
单体系统:将缓存与数据库操作放在一个事务
-
分布式系统:利用TCC等分布式事务方案
3.先操作缓存还是先操作数据库
-
先删除缓存,在操作数据库
-
先操作数据库,再删除缓存
读操作:
-
缓存命中则直接返回
-
缓存未命中则查询数据库,并写入缓存,设定超时时间
写操作:
-
先写数据库,然后再删除缓存
-
要确保数据库与缓存操作的原子性
给查询商铺的缓存添加超时剔除和主动更新的策略
修改ShopController的业务逻辑,满足下面的需求
① 根据id查询商铺时,如果缓存未命中,则查询数据库,将数据库数据写入缓存,并设置超时时间
//6.存在,将商铺数据写入redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
② 根据id修改店铺时,先修改数据库,再修改缓存
@Override@Transactionalpublic Result update(Shop shop) {Long id = shop.getId();if (id==null) {return Result.fail("店铺id不为空");}//1更新数据库updateById(shop);//2.删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY + id);return Result.ok();}
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库
常见的解决方案有两种:
1.缓存空对象
优点:实现简单,维护方便
缺点:额外的内存消耗,可能造成短期的数据不一致
2.布隆过滤
优点:内存占用少,没有多余的key
缺点:实现复杂,存在误判
其他的解决方案
1.增加id的复杂度,避免被猜测id规律
2.做好数据的基础格式校验
3.加强用户权限校验
4.做好热点参数的限流
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力
解决方案:
1.给不同的key的TTL添加随机值
2.利用redis集群提高服务的可用性
3.给缓存业务添加降级限流策略
4.给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的
key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
常见的解决方案有两种:
1.互斥锁
2.逻辑过期
优缺点:
基于互斥锁方式解决缓存击穿问题
需求:修改根据id查询商铺的业务,基于互斥锁来解决缓存击穿问题
基于逻辑过期解决缓存击穿问题
需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题
缓存工具封装
基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
方法:
1.将任意Java对象序列化为json并存储在String类型的key中,并且设置TTL过期时间
2.将任意Java对象序列化为json并存储在String类型的key中,并且设置逻辑过期时间,用于处理缓存击穿问题
3.根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
4.根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
CacheClient
@Slf4j
@Component
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}public void setWithLogicExpire(String key, Object value, Long time, TimeUnit unit) {//设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));//写入redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}//解决缓存穿透public <R, ID> R queryWithPassThrough(String kerPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {//1.查询商铺缓存String key = kerPrefix + id;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) {//将空值写入数据库stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//6.存在,将商铺数据写入redisthis.set(key, r, time, unit);//7.返回return r;}//解决缓存击穿public <R, ID> R queryWithLogicExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallBack, Long time, TimeUnit unit) {//1.查询商铺缓存String key = keyPrefix + id;String json = stringRedisTemplate.opsForValue().get(key);//2.判断是否存在if (StrUtil.isBlank(json)) {//3.缓存存在,返回return null;}//4.命中,需要把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();if (expireTime.isAfter(LocalDateTime.now())) {//5.1.未过期,返回商铺信息return r;}//5.2 已过期,需要缓存重建//6.缓存重建//6.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);//6.2判断是否获取锁成功if (isLock) {//6.3成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {//查询数据库R r1 = dbFallBack.apply(id);//写入redisthis.setWithLogicExpire(key, r1, time, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {//释放锁unLock(lockKey);}});}//6.4返回过期的商铺信息return r;}private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.MINUTES);return BooleanUtil.isTrue(flag);}private void unLock(String key) {stringRedisTemplate.delete(key);}
}
ShopServiceImpl
@Resourceprivate CacheClient cacheClient;@Overridepublic Result queryById(Long id) {//缓存穿透//Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);//Shop shop = queryWithPassThrough(id);//互斥锁解决缓存击穿//Shop shop = queryWithMutex(id);//逻辑过期解决缓存击穿Shop shop = cacheClient.queryWithLogicExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);//Shop shop = queryWithLogicExpire(id);if (shop == null) {return Result.fail("店铺不存在");}//7.返回return Result.ok(shop);}
视频地址
相关文章:

缓存穿透、缓存雪崩、缓存击穿解决方案
什么是缓存 缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。 添加 redis 缓存 给店铺类型查询业务添加缓存 需求:添加ShopTypeController中的queryTypeList方法,添加查询缓存 缓存更新…...

web + servlet + jdbc mysql 实现简单的表单管理界面
目录数据库创建数据库连接servlet创建,这里注意一下我的数据库我自己改了一下名字lhx网页html运行文件目录展示首先我们准备好开发使用的工具以及配置 idea2020 tomcat8.5 创建javaweb参考idea编译Tomcat详细步骤 IDEA通过JDBC连接数据库请参考jdbc连接数据库 需要登陆注册界面…...
Maven 国内镜像仓库
镜像仓库目标 当我们未定义任何远程仓库时,使用 Maven 更新依赖时,其会去默认远程仓库中拉取,默认远程仓库 是国外地址,所以在国内访问特别慢,想提升访问速度,需要将国外地址换成国内地址 更换仓库地址的…...
day21 ● 530.二叉搜索树的最小绝对差 ● 501.二叉搜索树中的众数 ● 236. 二叉树的最近公共祖先
二叉搜索树的最小绝对差 二叉搜索树(Binary Search Tree,简称 BST)是一种特殊的二叉树,它的每个节点都满足以下条件: 左子树上所有节点的值均小于该节点的值;右子树上所有节点的值均大于该节点的值&#…...
大学计算机(软件类)专业推荐竞赛 / 证书 官网及赛事相关信息整理
大学计算机专业(软件)推荐竞赛 / 证书 官网及赛事相关信息 一、算法类(丰富简历): 1、ACM国际大学生程序设计竞赛: 官网:https://icpc.global/ 国内:http://icpc.pku.edu.cn/index.htm 报名方式:区域预赛一般每年9-1…...
Metasploit入门到高级【第九章】
预计更新第一章:Metasploit 简介 Metasploit 是什么Metasploit 的历史和发展Metasploit 的组成部分 第二章:Kali Linux 入门 Kali Linux 简介Kali Linux 安装和配置常用命令和工具介绍 第三章:Metasploit 基础 Metasploit 的基本概念Met…...
JDK之8后: 协程? 虚拟线程!!!
特性官方文档: https://openjdk.org/jeps/436 Java协程 近三十年来,Java 开发人员一直依赖线程作为并发服务器应用程序的构建块。每个方法中的每个语句都在线程内执行,并且由于 Java 是多线程的,因此多个执行线程同时发生。线程是Java的并发…...

体验 jeecg
体验 jeecg官网地址事前准备安装升级 node 和 npm 版本验证安装安装 pnpm clidocker 启动 MySQLdocker 启动 redisgit clone 项目启动JAVA项目 jeecg-boot启动Vue3项目 jeecgboot-vue3官网地址 http://www.jeecg.com/ 事前准备 (1) 为了回避Could not find artifact com.mic…...

投稿指南【NO.13】计算机学会CCF推荐期刊和会议分享(人工智能)
前 言国内高等院校研究生及博士毕业条件需要发表高水平期刊或者顶会(清北上交等重点学校毕业要求为至少发一篇顶会),很多同学私信问到一级学会的会议论文怎么找、是什么,比如前段时间放榜的CVPR论文就是人工智能领域的顶会国际会议…...

一份sql笔试
1、 select substr(time,1,10),count(order_id),count(distinct passenger_id) from order where substr(time,1,7)2023-08 group by substr(time,1,10) order by substr(time,1,10);2、 select city_id from (select * from order where substr(time,1,7) 2022-08) t1 left j…...
交换瓶子
交换瓶子 贡献者:programmer_ada 有N个瓶子,编号 1 ~ N,放在架子上。 比如有5个瓶子: 2 1 3 5 4 要求每次拿起2个瓶子,交换它们的位置。 经过若干次后,使得瓶子的序号为: 1 2 3 4 5 对于这么…...
二、Docker安装、启动、卸载、示例
Docker 支持 CentOS 6 及以后的版本,可以直接通过yum进行安装: 使用流程:启动主机 – 启动Docker服务 – 下载容器镜像 – 启动镜像得一个到容器 – 进入容器使用我们想要的程序 主机一般是Linux、Utuban 以下主机系统以CentOS7为例子&#…...

开心档之C++ STL 教程
C STL 教程 目录 C STL 教程 实例 在前面的章节中,我们已经学习了 C 模板的概念。C STL(标准模板库)是一套功能强大的 C 模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构…...
Thread 类的基本用法
文章目录一、线程创建1.1 Thread的常见构造方法2.1 创建线程二、线程中断2.1 Thread的几个常见属性2.2 中断线程三、线程等待四、线程休眠五、获取线程实例一、线程创建 1.1 Thread的常见构造方法 方法说明Thread()创建线程对象Thread(Runnable target)使用Runnable对象创建线…...
2023.3.28 天梯赛训练赛补题(病毒溯源 , 龙龙送外卖 , 红色警报)
文章目录1.病毒溯源问题:求树的最长链长度和字典序最小的最长链思路:2.龙龙送外卖思路:3.红色警报:思路:1.病毒溯源 问题:求树的最长链长度和字典序最小的最长链 思路: 一开始用 bfs 做的 &a…...

917. 仅仅反转字母
917. 仅仅反转字母https://leetcode.cn/problems/reverse-only-letters/ 难度简单189 给你一个字符串 s ,根据下述规则反转字符串: 所有非英文字母保留在原有位置。所有英文字母(小写或大写)位置反转。 返回反转后的 s 。 示例…...

Linux-Git
一、总论 1.1 写在前面的话 这已经是我第三遍学Git相关操作了,可以说这个玩意是真的狗,因为确实用不到,不知道下个学期会不会用到,直到现在我刚刚学完,处于知识水平的巅峰,知道Git的具体功能ÿ…...

leetcode:2273. 移除字母异位词后的结果数组(python3解法)
难度:简单 给你一个下标从 0 开始的字符串 words ,其中 words[i] 由小写英文字符组成。 在一步操作中,需要选出任一下标 i ,从 words 中 删除 words[i] 。其中下标 i 需要同时满足下述两个条件: 0 < i < words.l…...

基于Python长时间序列遥感数据处理及在全球变化、物候提取、植被变绿与固碳分析、生物量估算与趋势分析等领域中的应用
植被是陆地生态系统中最重要的组分之一,也是对气候变化最敏感的组分,其在全球变化过程中起着重要作用,能够指示自然环境中的大气、水、土壤等成分的变化,其年际和季节性变化可以作为地球气候变化的重要指标。此外,由于…...
4.4---Spring框架之Spring事务(复习版本)
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。 Spring只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过 redo log 和 undo log实现的。 S…...
Java 8 Stream API 入门到实践详解
一、告别 for 循环! 传统痛点: Java 8 之前,集合操作离不开冗长的 for 循环和匿名类。例如,过滤列表中的偶数: List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...

HTML 列表、表格、表单
1 列表标签 作用:布局内容排列整齐的区域 列表分类:无序列表、有序列表、定义列表。 例如: 1.1 无序列表 标签:ul 嵌套 li,ul是无序列表,li是列表条目。 注意事项: ul 标签里面只能包裹 li…...

【2025年】解决Burpsuite抓不到https包的问题
环境:windows11 burpsuite:2025.5 在抓取https网站时,burpsuite抓取不到https数据包,只显示: 解决该问题只需如下三个步骤: 1、浏览器中访问 http://burp 2、下载 CA certificate 证书 3、在设置--隐私与安全--…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
unix/linux,sudo,其发展历程详细时间线、由来、历史背景
sudo 的诞生和演化,本身就是一部 Unix/Linux 系统管理哲学变迁的微缩史。来,让我们拨开时间的迷雾,一同探寻 sudo 那波澜壮阔(也颇为实用主义)的发展历程。 历史背景:su的时代与困境 ( 20 世纪 70 年代 - 80 年代初) 在 sudo 出现之前,Unix 系统管理员和需要特权操作的…...
【JavaSE】多线程基础学习笔记
多线程基础 -线程相关概念 程序(Program) 是为完成特定任务、用某种语言编写的一组指令的集合简单的说:就是我们写的代码 进程 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存…...
Git常用命令完全指南:从入门到精通
Git常用命令完全指南:从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...

Golang——9、反射和文件操作
反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一:使用Read()读取文件2.3、方式二:bufio读取文件2.4、方式三:os.ReadFile读取2.5、写…...

Ubuntu系统多网卡多相机IP设置方法
目录 1、硬件情况 2、如何设置网卡和相机IP 2.1 万兆网卡连接交换机,交换机再连相机 2.1.1 网卡设置 2.1.2 相机设置 2.3 万兆网卡直连相机 1、硬件情况 2个网卡n个相机 电脑系统信息,系统版本:Ubuntu22.04.5 LTS;内核版本…...
微服务通信安全:深入解析mTLS的原理与实践
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、引言:微服务时代的通信安全挑战 随着云原生和微服务架构的普及,服务间的通信安全成为系统设计的核心议题。传统的单体架构中&…...