缓存

数据交换的缓冲区,俗称的缓存是缓冲区内的数据,一般从数据库中获取,

1:Static final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>(); 本地用于高并发例2:static final Cache<K,V> USER_CACHE = CacheBuilder.newBuilder().build(); 用于redis等缓存例3:Static final Map<K,V> map =  new HashMap(); 本地缓存
  1. ConcurrentHashMap
    线程安全的哈希表,支持高并发读写。适用于本地内存缓存,无需序列化,直接操作 Java 对象。但无法持久化或分布式共享。

  2. CacheBuilder(Guava Cache):
    功能更丰富的本地缓存,支持过期策略、最大容量、弱引用等。通常用于本地二级缓存,配合 Redis 等远程缓存使用,减少远程访问压力。

  3. HashMap
    非线程安全的哈希表,直接用于缓存会有并发问题(如数据不一致、死循环)。不推荐在高并发场景使用,除非通过外部同步(如Collections.synchronizedMap)。

使用缓存的目的

速度快,提高读写效率,降低响应时间
缓存数据存储在内存中,而内存读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力(降低后端负载)
![[Pasted image 20250711171217.png]]

  1. 数据一致性成本
    若后端数据更新(如商品价格修改),缓存未及时同步,会出现 “缓存与源数据不一致” 的问题。需设计 缓存失效策略(如超时、主动更新),但这会增加代码复杂度和异常处理成本。
  2. 代码维护成本
    引入缓存后,代码需新增 “缓存读写、失效、回源(缓存未命中时查后端)” 等逻辑,还需处理缓存穿透、击穿、雪崩等异常场景,导致代码更复杂,维护难度提升。
  3. 运维成本
    缓存系统(如 Redis、Memcached)需独立部署、监控(内存、命中率、连接数)、扩容(集群化)、故障恢复,增加运维人力和资源投入。

如何使用缓存:
构建多级缓存,例如本地缓存和redis缓存并发使用

浏览器缓存:保存在浏览器端的缓存
应用层缓存:分为tomcat本地缓存,如使用map或redis
数据库缓存:数据库中有一个缓存池,增改查数据都会先加载到mysql缓存中
CPU缓存:CPU的L1、L2、L3级缓存
![[Pasted image 20250711171515.png]]

添加商户缓存

在查询商户信息时,先到缓存中查询
这里添加redis缓存
查询时先访问Redis,若没有命中再访问数据库,同时写缓存到Redis
![[Pasted image 20250711171743.png]]

String key = "cache:shop:" + id;  
String shopJson = stringRedisTemplate.opsForValue().get(key);  
if (StrUtil.isNotBlank(shopJson)) {  Shop shop = JSONUtil.toBean(shopJson, Shop.class);  return Result.ok(shop);  
}  
Shop shop = getById(id);  
if (shop == null) {  return Result.fail("店铺不存在哦");  
}  
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));  
return Result.ok(shop);

先从redis查询店铺数据
如果存在,将JSOn格式的字符串通过JSONUtil反序列化成Shop类对象的实例
如果不存在,去数据库中查找,将返回值写入redis,这里同样将Shop类对象的实例转化成String类型

缓存更新策略

若缓存中数据过多,redis会对部分数据进行更新或淘汰

内存淘汰

当内存达到设定的最大值时,自动淘汰一些不重要的数据

超时剔除

设置过期时间,redis会将超时的数据进行删除

主动更新

手动调用方法删除缓存,通常用于解决缓存和数据库不一致的问题

![[Pasted image 20250711172822.png]]

数据库缓存不一致解决方案

由于缓存数据来源于数据库,而数据库中的数据是会发生变化的,若数据库数据发生变化,而缓存未同步,就会出现一致性问题
后果是用户可能使用缓存中过时数据,从而产生类似多线程数据安全问题
有如下解决方案:
人工编码:内存调用者在更新完数据库后更新缓存
读写穿透模式:系统作为中间层,同时管理缓存与数据库的读写操作
写回缓存模式:应用层仅操作缓存,数据库更新由异步线程批量处理,调用者写入缓存后直接返回,由异步线程定期将缓存数据批量写入数据库

综合推荐使用方案一,
在操作数据库时,我们可以将缓存删除,待查询时再从缓存中加载数据
为保证数据库操作同时成功或失败:
采用单体系统的情况,则将缓存与数据库放在一个事务中
采用分布式系统,则利用TCC等分布式事务方案

具体操作缓存和数据库时,应该采用先操作数据库,后删除缓存的操作
若先删除缓存,再操作数据库:
当有两个线程并发查询的时候,假设线程1先查询,删除缓存后此时线程2发现没有缓存数据,从数据库中读取旧数据写入到缓存中,此时线程1再进行更新数据库的操作,那么缓存就是旧数据

![[Pasted image 20250711174001.png]]

@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_KEY + id);  return Result.ok();  
}

具体到代码在执行更新操作是,先更新数据库,再删除缓存

缓存穿透问题的解决思路

缓存穿透:客户端请求的数据在缓存中和数据库总都不存在,都有缓存永远不会生效,这些请求都会打到数据库。
解决方案:

将空对象缓存:哪怕数据在数据库中不存在也存入redis中,这样就不会访问数据库
实现简单,但会造成额外的内存消耗

布隆过滤:通过一个庞大的二进制数据,走哈希的思想判断这个数据是否存在,若存在才会放行
内存占用少,但实现复杂并有误判可能
![[Pasted image 20250711174637.png]]

缓存雪崩及解决思路

缓存雪崩是指同一时间大量缓存key同时失效导致Redis服务宕机,导致大量请求到达数据库,从而造成巨大的压力
解决方案:
给不同Key的TTL添加随机值
使用Redis集群
给缓存业务进行降级限流
给业务添加多级缓存
![[Pasted image 20250711174901.png]]

缓存击穿及解决思路

也叫热点key,就是一个高并发且缓存重建业务比较复杂(重建时间长)的key突然失效,无数请求的访问会瞬间给数据库带来巨大的冲击
解决方案:

互斥锁

将并行查询改为串行,一次只能一个线程访问数据库,使用tryLock和double check解决问题
![[Pasted image 20250711175126.png]]

逻辑过期

不设置过期时间,将过期时间设置在redis的value中,当线程1查询缓存时,发现数据已经过期了,他会开启一个新的线程去进行重构数据的逻辑,而线程1直接返回过期数据,假设线程3过来访问,由于线程2持有锁,线程3无法获得锁,它也直接返回过期数据
特点是在完成缓存重建之前,所有线程返回的都是脏数据
![[Pasted image 20250711175417.png]]

对比:

互斥锁:简单,保证数据一致,可能存在死锁风险且性能低
逻辑过期:读取不需要等待,性能好,在重构之前都是脏数据,实现复杂
![[Pasted image 20250711175513.png]]

使用互斥锁解决缓存击穿问题

 public Shop queryWithMutex(Long id)  {String key = CACHE_SHOP_KEY + id;// 1、从redis中查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get("key");// 2、判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判断命中的值是否是空值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){//4.3 失败,则休眠重试Thread.sleep(50);return queryWithMutex(id);}//4.4 成功,根据id查询数据库shop = getById(id);// 5.不存在,返回错误if(shop == null){//将空值写入redisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);//返回错误信息return null;}//6.写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL,TimeUnit.MINUTES);}catch (Exception e){throw new RuntimeException(e);}finally {//7.释放互斥锁unlock(lockKey);}return shop;}

先进行获取锁,未获取到迭代继续获取,知道拿到后查询数据库,如果数据库没有,将空对象写入redis并返回null
如果有就写入redis,然后释放锁,最后返回数据库的结果

使用逻辑过期解决缓存击穿问题

在查询redis时,先判断是否命中,如果没有命中直接返回空数据,不查询数据库,一旦命中将value取出,判断value的过期时间,如果没过期直接返回数据,过期则开启独立线程后返回之前的数据,独立线程单独重构数据,重构完成后释放互斥锁
![[Pasted image 20250711204829.png]]

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {String key = CACHE_SHOP_KEY + id;// 1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isBlank(json)) {// 3.存在,直接返回return null;}// 4.命中,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, 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.缓存重建// 6.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock){CACHE_REBUILD_EXECUTOR.submit( ()->{try{//重建缓存this.saveShop2Redis(id,20L);}catch (Exception e){throw new RuntimeException(e);}finally {unlock(lockKey);}});}// 6.4.返回过期的商铺信息return shop;
}
  1. 线程池的运用
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    
    这里创建了一个固定大小为 10 的线程池,目的是管控缓存重建任务。借助线程池,可以避免因大量创建线程而导致系统资源被过度占用。
  2. 异步任务的提交
    CACHE_REBUILD_EXECUTOR.submit( ()->{// 任务内容
    });
    
    当商铺缓存过期后,会向线程池提交一个重建缓存的任务,这样可以让主线程继续执行后续操作,不用等待缓存重建完成。
  3. 缓存重建的流程
    try{// 重建缓存this.saveShop2Redis(id, 20L);
    } catch (Exception e) {throw new RuntimeException(e);
    }
    
    • saveShop2Redis(id, 20L) 方法会从数据库获取最新的商铺数据,然后把这些数据存入 Redis,同时设置 20 秒的逻辑过期时间。
    • 对可能出现的异常进行捕获,将其封装成运行时异常后重新抛出。
  4. 锁的释放操作
    finally {unlock(lockKey);
    }
    
    不管缓存重建成功与否,最终都会执行 unlock(lockKey) 方法来释放锁,防止出现死锁的情况。

封装redis工具类

基于StringRedisTemplate封装一个缓存工具类
方法1:将任意Java对象序列化为Json并储存在string类型的key中,可设置TTL过期时间
方法2:将任意Java对象序列化为Json并储存在String类型的key中,可以设置逻辑过期时间,用于处理缓存击穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透的问题
根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

Shop shop = cacheClient.queryWithPassThrough( CACHE_SHOP_KEY, // 缓存键前缀 
id, // 商铺ID 
Shop.class, // 返回类型 
this::getById, // 数据库查询回调
CACHE_SHOP_TTL, // 缓存时间 
TimeUnit.MINUTES // 时间单位 );
public <R, ID> R queryWithPassThrough( String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) { // ... R r = dbFallback.apply(id); // 调用传入的函数 // ... }

这里的 Function<ID, R> 是一个函数式接口,表示接受一个 ID 类型的参数,返回一个 R 类型的结果。
等价于id->getById(id),表示传入一个id参数,调用当前对象的getById方法处理它,并返回结果

总结

  1. 缓存的核心作用是什么?
    缓存通过将高频访问数据存储在内存中,显著提升读写效率、降低响应时间,同时减少后端数据库的访问压力,缓解高并发场景下的服务器负载。

  2. 常见的本地缓存实现有哪些?核心区别是什么?
    常见实现包括ConcurrentHashMap(线程安全,适用于高并发本地缓存,无持久化)、Guava Cache(功能丰富,支持过期策略、容量控制等,适合本地二级缓存)、HashMap(非线程安全,高并发下易出问题,不推荐直接使用)。核心区别在于线程安全性、功能丰富度及适用场景。

  3. 如何解决缓存与数据库的数据一致性问题?
    推荐 “先更新数据库,后删除缓存” 的策略:更新操作时,先保证数据库数据正确,再删除对应缓存,避免旧数据残留。单体系统中可通过事务保证操作原子性,分布式系统需结合 TCC 等分布式事务方案。

  4. 什么是缓存穿透?如何解决?
    缓存穿透指请求数据在缓存和数据库中均不存在,导致请求直接穿透缓存冲击数据库。解决方式包括:①缓存空对象(将不存在的数据以空值存入缓存,避免重复穿透);②布隆过滤(通过哈希判断数据是否存在,提前拦截无效请求)。

  5. 缓存击穿的解决方式有哪些?各有什么特点?
    缓存击穿指高并发下热点 Key 突然失效,大量请求瞬间冲击数据库。解决方式包括:①互斥锁(串行化请求,保证缓存重建时仅一个线程访问数据库,数据一致但性能略低);②逻辑过期(不设置物理过期,通过 value 中的逻辑时间判断,过期时异步重建缓存,性能高但可能返回脏数据)。

  6. 缓存雪崩的成因及预防措施是什么?
    缓存雪崩指大量缓存 Key 同时失效,导致 Redis 压力骤降、请求集中冲击数据库。预防措施包括:①给 Key 的 TTL 添加随机值,避免集中过期;②使用 Redis 集群提高可用性;③对缓存业务降级限流;④引入多级缓存减少单一层级依赖。

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【前端】【组件库开发】【原理】【无框架开发】现代网页弹窗开发指南:从基础到优化

效果 现代网页弹窗开发指南&#xff1a;从基础到优化 弹窗&#xff08;Modal&#xff09;作为网页交互的重要组件&#xff0c;在用户通知、确认操作和表单输入等场景中广泛应用。本文将循序渐进地讲解弹窗的技术实现与最佳实践。 一、弹窗基础概念 弹窗是一种覆盖在主内容之…

【操作系统】线程

JavaEE—线程 一、进程与线程 1.包含管理 2.资源布局 2.1公共资源 2.2私有资源 二、并发编程 1.多线程优势 1.1创建 1.1.1多线程 1.1.2多进程 1.2通信 1.2.1多线程 1.2.2多进程 1.3调度 1.3.1多线程 1.3.2多进程 1.4销毁 1.4.1多线程 1.4.2多进程 2.多进程…

React 自定义Hook——页面或元素滚动到底部监听 Hook

功能简介 useReachBottom 是一个 React 自定义 Hook&#xff0c;支持监听页面&#xff08;body&#xff09;或任意可滚动元素&#xff08;如 div&#xff09;是否滚动到底部。它能帮助你在用户滑动到底部时触发加载更多、显示提示等操作&#xff0c;极大提升前端交互体验。 亮…

当Powerbi遇到quickbi,性能优化方式对比

powerbi性能优化对于powerbi&#xff0c;性能优化可以从15个方面考虑&#xff1a; 1.过滤源数据【quickbi数据集过滤或sql过滤】2.删除无关列 【quickbi不选字段或sql不查询】3.聚合分析粒度 【quickbi使用sql聚合或计算字段聚合】4.整理字段 【quickbi使用sql聚合或计算字段聚…

ValueConverter转换器WPF

属性搭桥 比如BoolToVisibility 创建两个属性 Bool Visibility 这样不好 混乱了viewmodels 降低了泛用性系统自带的convertor <Window.Resources><BooleanToVisibilityConverter x:Key"booltovis"></BooleanToVisibilityConverter><…

Qt数据库编程详解:SQLite实战指南

Qt数据库编程详解&#xff1a;SQLite实战指南 目录 SQLite数据库简介Qt数据库核心类数据库操作全流程CRUD操作实战运行效果展示 1. SQLite数据库简介 SQLite是Qt内置的轻量级嵌入式数据库&#xff1a; #mermaid-svg-OiZ2cgq9n1G69iH5 {font-family:"trebuchet ms",…

FastAPI 与 OpenIddict 的微服务鉴权整合方案

架构概述基于微服务的身份认证架构采用OAuth 2.0/OpenID Connect协议&#xff0c;OpenIddict作为认证服务器&#xff0c;FastAPI作为资源服务器。系统包含三个核心组件&#xff1a;认证服务、API网关和业务微服务。OpenIddict负责颁发令牌&#xff0c;FastAPI通过JWT验证访问权…

计算两个点的欧式距离

目录 一、概述 二、公式 1、二维空间 2、三维空间 3、n 维空间 三、python实现 一、概述 欧式距离&#xff08;Euclidean Distance&#xff09;是一种在欧几里得空间中度量两个点之间距离的常用方法&#xff0c;其公式根据空间维度的不同而不同 二、公式 1、二维空间 对于二…

八股训练--RabbitMQ

一、经典问题 1.为什么要用MQ&#xff1f; MQ的作用主要是3个&#xff0c; 第一个是流量削峰&#xff1a;当某个活动举行时&#xff0c;访问量可能是平时的几百倍&#xff0c;可能一下会把服务器弄崩溃&#xff0c;所以通过MQ的形式&#xff0c;引入中间者&#xff0c;客户端…

Elasticsearch 文档检索系统

学习笔记&#xff1a;Elasticsearch 文档检索系统 1. 技术栈与核心组件 Node.js&#xff1a;后端运行环境&#xff0c;适合构建高性能 Web 服务。Express&#xff1a;Node.js 的 Web 框架&#xff0c;简化 API 开发。Elasticsearch&#xff1a;分布式全文检索引擎&#xff0c;支…

如何准确查看服务器网络的利用率?

在服务器运维与性能调优过程中&#xff0c;网络利用率是一个不容忽视的关键指标。它反映了服务器带宽资源的实际使用情况&#xff0c;是判断系统瓶颈、规划资源扩展、排查连接问题的重要依据。很多人误以为网络是否正常只要“能上网”或“Ping得通”就可以了&#xff0c;实际上…

掌握Spring声明式事务传播机制:AOP与ThreadLocal的协同工作

声明式事务的传播机制是解决多个事务方法嵌套调用时&#xff0c;事务如何创建、复用、挂起或隔离的核心逻辑。它的实现依赖于事务管理器、事务状态管理、线程上下文绑定等组件的协同&#xff0c;本质是通过一套 “规则判断 状态维护” 的逻辑&#xff0c;在方法调用时动态决定…

@Transactional事务注解的批量回滚机制

关键机制说明&#xff1a;1.​​事务注解生效​​&#xff1a;Transactional(rollbackFor Exception.class)Override Transactional(rollbackFor Exception.class) public Boolean saveUser(UserDTO userDto) {SysUser sysUser new SysUser();BeanUtils.copyProperties(user…

飞算 JavaAI 深度体验:开启 Java 开发智能化新纪元

个人主页&#xff1a;♡喜欢做梦 欢迎 &#x1f44d;点赞 ➕关注 ❤️收藏 &#x1f4ac;评论 目录 一、引言 二、飞算 JavaAI 初印象与功能概览 &#xff08;一&#xff09;初识飞算 JavaAI &#xff08;二&#xff09;核心功能模块概览 三、智能代码生成功能深度体…

pandas销售数据分析

pandas销售数据分析 数据保存在data目录 消费者数据&#xff1a;customers.csv商品数据&#xff1a;products.csv交易数据&#xff1a;transactions.csv customers.csv数据结构&#xff1a;字段描述customer_id客户IDgender性别age年龄region地区membership_date会员日期produc…

访问Windows服务器备份SQL SERVER数据库

以前没有直接访问过Windows服务器,今天刚一看到的是时候有点懵,竟然下意识的使用SecureCRT远程工具去连了一下,然后领导说,看一下用户名,突然意识到,跟我们平时远程桌面是一样的。 一、 win + R 打开命令窗口 二、 输入 mstsc 三、 输入远程地址 四、点击连接,如果有弹…

C++ 面向对象 - 对象定义方法汇总

C对象定义方法汇总 1. 栈上定义方式 1.1 调用无参构造函数的定义方式 无参构造函数有两种&#xff1a; 默认无参构造函数Demo(){}默认值列表构造函数。Demo():a{1},b{2}{} // 使用初始化列表实现对象定义方式&#xff1a; Demo d; Demo d1{}; // 以下定义方式还调用了拷贝构造…

指尖上的魔法:优雅高效的Linux命令手册

一、Linux基础指令 1. ls ls&#xff1a;对于目录&#xff0c;列出该目录下的所有子目录与文件&#xff0c;对于文件&#xff0c;将列出文件名以及其他信息。 -a&#xff1a;列出目录下的所有文件&#xff0c;包含以.开头的隐藏文件 -l:列出文件的详细信息 -d&#xff1a;将目录…

《磁力下载工具实测:资源搜索+高速下载一站式解决方案》

嘿&#xff0c;朋友们&#xff01;我是阿灿&#xff0c;今天给大家带来一个超实用的看片神器&#xff0c;特别适合老司机们使用&#xff0c;保证让你眼前一亮&#xff01;推荐一款比某雷更好用的下载工具&#xff0c;搭配资源搜索神器&#xff0c;轻松获取资源不限速。超强磁力…

Go网络编程基础:网络模型与协议栈概述 - 从理论到实践的完整指南

1. 引言 在当今的互联网时代&#xff0c;网络编程已经成为后端开发的核心技能。Go语言以其出色的并发性能和简洁的语法&#xff0c;在网络编程领域展现出了强大的优势。从Docker、Kubernetes到众多微服务框架&#xff0c;Go已经成为构建高性能网络应用的首选语言之一。 你是否…