当前位置: 首页 > news >正文

二刷 黑马点评 商户查询缓存

缓存

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

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.dtcms.com/a/273586.html

相关文章:

  • <script>标签对HTML文件解析过程的影响以及async和defer属性的应用
  • 在 React Three Fiber 中实现 3D 模型点击扩散波效果
  • 车企战略投资项目管理的实践与思考︱中国第一汽车集团进出口有限公司战略部投资管理专家庞博
  • 台球 PCOL:极致物理还原的网页斯诺克引擎(附源码深度解析)
  • 软件设计师中级逻辑公式题
  • Ubuntu 24.04上安装 Intelligent Pinyin 中文输入法
  • Java算法 -蓝桥云课 -卖货
  • 【联合国国家指标 2025:HDI、GDP、POP、面积】数据集countries_metric - Sheet1.csv
  • C++迭代器失效
  • 深入剖析Spring Bean生命周期:从诞生到消亡的全过程
  • 羲和:一款诗词风格的摆件App
  • GitHub Copilot:产品经理提升工作效率的AI助手
  • 销售数据可视化分析项目
  • AI基建还能投多久?高盛:2-3年不是问题,回报窗口才刚开启
  • Lookahead:Trie 树(前缀树)
  • TCP详解——流量控制、滑动窗口
  • 【接口测试】07 Fiddler使用教程(图文详解)
  • Flutter、Vue 3 和 React 在 UI 布局比较
  • 20.缓存问题与解决方案详解教程
  • 【Java】【力扣】102.二叉树层序遍历
  • 前端抓包(不启动前端项目就能进行后端调试)--whistle
  • 什么是DOM存储
  • 05 rk3568 debian11 root用户 声音服务PulseAudio不正常
  • Typecho架构深度剖析:轻量级博客系统的设计哲学与实现原理
  • 前端性能与可靠性工程:我们度量什么?核心 Web 指标与工具入门
  • 【氮化镓】不同偏压应力下电荷俘获效应导致的P-GaN HEMT阈值电压不稳定性
  • Chromium 136 编译指南 - Android 篇:从Linux版切换到Android版(六)
  • 电子基石:硬件工程师的器件手册 (四) - 二极管:电流的单向阀与电路的守护神
  • SSL与HTTP概述
  • 神经网络的参数初始化