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

【遇见青山】项目难点:缓存击穿问题解决方案

【遇见青山】项目难点:缓存击穿问题解决方案

  • 1.缓存击穿
    • 互斥锁🔒方案
    • 逻辑过期方案
  • 2.基于互斥锁方案的具体实现
  • 3.基于逻辑过期方案的具体实现

1.缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

在这里插入图片描述

常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期

互斥锁🔒方案

给线程添加互斥锁🔒

在这里插入图片描述

优点:

  • 没有额外的内存消耗
  • 保证一致性
  • 实现简单

缺点:

  • 线程需要等待,性能受影响
  • 可能有死锁风险

逻辑过期方案

缓存中维护一个expire字段,代表逻辑过期时间(并非TTL值)

例如:

在这里插入图片描述
在这里插入图片描述

优点:

  • 线程无需等待,性能较好

缺点:

  • 不保证一致性
  • 有额外内存消耗
  • 实现复杂

2.基于互斥锁方案的具体实现

需求:修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题

架构流程图:

在这里插入图片描述

首先自定义获取锁和释放锁的方法:

/*** 尝试获取锁,解决缓存击穿问题方案** @param key key*/
private boolean tryLock(String key) {Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(aBoolean);
}/*** 删除锁,解决缓存击穿问题方案** @param key key*/
private void unlock(String key) {stringRedisTemplate.delete(key);
}

核心代码:

/*** 缓存击穿解决方案** @param id 商户id* @return 商户对象*/
public Shop queryWithMutex(Long id) {// 从redis查询商户缓存String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);// 判断商户缓存是否存在if (StringUtils.isNotBlank(shopJson)) {// 此商户缓存存在,直接返回结果return JSONUtil.toBean(shopJson, Shop.class);}// 判断命中的是否为空值 "",防止缓存穿透if ("".equals(shopJson)) {return null;}Shop shop = null;try {// 实现缓存重建// 获取互斥锁boolean isLock = tryLock(LOCK_SHOP_KEY + id);// 判断是否取锁成功if (!isLock) {// 失败,则进入休眠并重试Thread.sleep(50);return queryWithMutex(id);}// 缓存中商户信息不存在,查询数据库shop = getById(id);// 模拟重建的延时Thread.sleep(200);// 查询数据库不存在,返回错误if (shop == null) {// 将null值写入Redis,防止缓存穿透问题stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}// 查询数据库存在,写入数据到Redis中stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {// 释放互斥锁unlock(LOCK_SHOP_KEY + id);}// 返回数据给前端return shop;
}

使用JMeter做压力测试:

这里开1000个线程延时5秒:

在这里插入图片描述
在这里插入图片描述

结果:

全部请求成功!

在这里插入图片描述

平均QPS 200:

在这里插入图片描述


3.基于逻辑过期方案的具体实现

需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题

设计架构图:

在这里插入图片描述

这里,我们如何将过期时间字段加入Redis中呢,为了不对原有的代码进行修改,最好的解决办法是封装一个带有过期时间的实体类:

/*** 逻辑过期时间的实体支持*/
@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}

创建一个将商户预热的方法:

/*** 将热点商户加入到缓存中,进行预热** @param id            商户id* @param expireSeconds 逻辑过期时间*/
public void saveShopToRedis(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));
}

进行测试:

@SpringBootTest
@RunWith(SpringRunner.class)
public class QingShanApplicationTests {@Resourceprivate ShopServiceImpl shopService;/*** 添加热点商户到Redis缓存的测试类*/@Testpublic void testSaveShop() {shopService.saveShopToRedis(1L, 10L);}
}

添加热点缓存成功!

在这里插入图片描述

核心实现:

// 生成线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);/*** 逻辑过期解决缓存击穿问题* @param id 商户id* @return 商户对象*/
public Shop queryWithLogicalExpire(Long id){// 组装keyString key = RedisConstants.CACHE_SHOP_KEY + id;// 去redis中读取数据String value = stringRedisTemplate.opsForValue().get(key);// 如果缓存未命中,说明访问的不是热点数据,直接返回空if(StrUtil.isBlank(value)){return null;}// 缓存命中后,还需要先判断数据有没有过期RedisData redisData = JSONUtil.toBean(value, RedisData.class);// 获取数据信息,由于数据存进去时是object类型,这里需要做一下处理Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);// 获取数据的逻辑过期时间LocalDateTime expireTime = redisData.getExpireTime();// 如果数据没有过期,就直接返回if(expireTime.isAfter(LocalDateTime.now())){return shop;}// 如果数据已过期,则尝试获取互斥锁String lockKey = RedisConstants.LOCK_SHOP_KEY+id;boolean tryLock = tryLock(lockKey);if(tryLock){// 获取锁成功则开辟一条独立线程执行缓存重建CACHE_REBUILD_EXECUTOR.submit(()->{try {// 再次检查缓存有没有过期,防止在高并发环境下缓存多次重建LocalDateTime time = JSONUtil.toBean(stringRedisTemplate.opsForValue().get(key), RedisData.class).getExpireTime();if(time.isAfter(LocalDateTime.now())){// 数据没过期则直接结束return;}// 调用重建缓存的方法saveShopToRedis(id,10l);} catch (Exception e) {throw new RuntimeException(e);} finally {//释放锁操作unlock(lockKey);}});}// 返回过期数据return shop;
}
http://www.lryc.cn/news/2779.html

相关文章:

  • 2023Flag具体实施计划(短期)
  • 研一寒假C++复习笔记--左值和右值的理解和使用
  • Android 11.0 动态修改SystemProperties中ro开头系统属性的值
  • 为什么分库分表
  • 1625_MIT 6.828 stabs文档信息整理_下
  • 论文阅读 | Rethinking Coarse-to-Fine Approach in Single Image Deblurring
  • Mysql 增删改查(二)—— 增(insert)、删(delete)、改(update)
  • JSD2212复习串讲
  • sphinx 升级到6.x后的Jquery问题
  • NSSCTF Round#8 Basic
  • 多传感器融合定位十二-基于图优化的建图方法其一
  • RockChip MPP编码
  • 【学习笔记】NOIP暴零赛2
  • linux基本功系列之hostname实战
  • Easy-Es框架实践测试整理 基于ElasticSearch的ORM框架
  • 【数据结构】双向链表的模拟实现(无头)
  • vue自定义指令---处理加载图片失败时出现的碎图,onerror事件
  • 加盟管理系统挑选法则,看完不怕被坑!
  • alertmanager笔记
  • Android Jetpack组件之WorkManager后台任务管理的介绍与使用(二)
  • 【MySQL】第十七部分 约束
  • java ssm集装箱码头TOS系统调度模块的设计与实现
  • MS14-064(OLE远程代码执行漏洞复现)
  • 【C++深陷】之shared_ptr
  • SpringMVC中遇到的错误
  • 姿态估计端到端新方案 | DirectMHP:用于全范围角度2D多人头部姿势估计
  • jvm学习的核心(五)---垃圾回收算法和常见垃圾回收器
  • 亿级高并发电商项目-- 实战篇 --万达商城项目 二(Zookeeper、Docker、Dubbo-Admin等搭建工作
  • 【C#基础】 C# 数据类型总结
  • 格子玻尔兹曼法介绍