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

对黑马点评中Redis缓存穿透与击穿解决方案的小理解

一.首先是封装后的代码

@Component @Slf4j public class CacheClient { private final StringRedisTemplate stringRedisTemplate; public CacheClient(RedisTemplate redisTemplate, StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } public void set(String key, Object value, long time, TimeUnit timeUnit) { stringRedisTemplate.opsForValue().set(key, value.toString(), time, timeUnit); } public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit timeUnit) { //先从reids看看有没有 String key = keyPrefix + id; String json = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(json)) { //有,直接返回 return BeanUtil.toBean(json, type); } //判断是不是空值 //要知道知道空值与空字符串不是一个,这里说的如果是后面再reddis中存的空字符串就直接报错,不可数据库上压力的可能 if (json != null) { return null; } R r = dbFallback.apply(id); if (r == null) { //还要将空值写入redis,为了防止缓存穿透 stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); //正常返回 return null; } this.set(key, r, time, timeUnit); return r; } private static final ExecutorService CACHE_THREAD_POOL = Executors.newFixedThreadPool(10); public void setWithLogicalExpire(String key, Object value, long time, TimeUnit unit) { RedisData redisData = new RedisData(); redisData.setData(value); redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time))); stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); } public <R,ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type,Function<ID,R> dbFallback,Long time, TimeUnit timeUnit) { //先从reids看看有没有 String key = keyPrefix + id; String shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isBlank(shopJson)) { //有,直接返回 return null; } //由于下面设置的是属性的过期时间,所以要麻烦的取一下 //在Java中不方便操作JSON格式语句,先转换为通过toBean方法转换为RedisData对象,后面的字节码文件就是告诉toBean方法,按照RedisData类的属性模板来转换 RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class); //随后又通过get方法得到一个object的对象,但是object没有get,set方法,所以还要继续转成JSONObject类型的 JSONObject data = (JSONObject) redisData.getData(); //随后又将JSONObject类型的对象,转换为Shop类型的对象,方便传回 R r = JSONUtil.toBean(data, type); //时间的类型是LocalDateTime,直接与当前时间比较,判断是否过期即可 LocalDateTime expirTime = redisData.getExpireTime(); //如果过期时间在当前时间之后,说明没有过期 if (expirTime.isAfter(LocalDateTime.now())) { //没有过期,直接返回 return r; } //只有过期了,才需要重建缓存 String lockKey = LOCK_SHOP_KEY + id; boolean islock = tryLock(lockKey); if (islock) { CACHE_THREAD_POOL.submit(() -> { //saveShop2Redis是当前类的方法,所以要使用this调用 try { R r1 = dbFallback.apply(id); this.set(key, r1, time, timeUnit); } catch (Exception e) { throw new RuntimeException(e); } finally { unLock(lockKey); } }); } return r; } private boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); //防止空指针,如果是空指针的话会直接返回 false而不是抛异常 return BooleanUtil.isTrue(flag); } private void unLock(String key) { stringRedisTemplate.delete(key); } }
1.关于开头的构造注入
  • 首先要明确StringRedisTemplate stringRedisTemplate是我通过Java程序与redis交流的工具类,但他光导包是没有用的,他的创建需要大量的配置,例如redis的密码等等,这些由spring boot来自动完成后创建完整可用的bean存在容器里,而我们要拿到spring管理的bean对象用import是没有用的,他只可以找到类而非最后的对象
  • 那我们要如何拿到这个bean对象呢?首先类上面的@Component注解会让spring启动时吧当前类实例化成对象,存入容器中,在创建bean的时候spring也读构造函数,随后识别到StringRedisTemplate后将它对应的bean对象传入,为什么非要这个对象?一是需要其中已经配置好的连接设置,二是RedisTemplate的方法都是实例方法,只可以通过对象调用
2.关于set方法的设置
  • 就是为了省事
原来要写 stringRedisTemplate.opsForValue().set("user:1", userInfo.toString(), 30, TimeUnit.MINUTES);
但是现在只要写cacheClient.set("user:1", userInfo, 30, TimeUnit.MINUTES);
3.关于setWithLogicalExpire方法
  • 这个其实就是为了后面解做准备,缓存击穿就是同一个热点 key 过期的瞬间大量请求直接打到了数据库
public void setWithLogicalExpire(String key, Object value, long time, TimeUnit unit) { //创建RedisData,这个类只有两个属性:Object data真正要缓存的业务对象,LocalDateTime expireTime:自定义的逻辑过期时间 RedisData redisData = new RedisData(); //将你的数据放进包装对象 redisData.setData(value); //不直接设置TTL过期时间,而是直接设置一个逻辑过期时间点的属性 redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time))); //随后将整个RedisData转成JSON,存入redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); }
4.与3相连的queryWithLogicalExpire方法(解决热点key缓存击穿)
  • 首先为了提高复用性,需要对返回值做一些变动
//<R,ID>这个是声明了一个自定义的类型,R就是后面R的类型 public <R,ID> R queryWithLogicalExpire( String keyPrefix, ID id, // ID用作入参类型 Class<R> type, // R代表要转换的实体类 Function<ID,R> dbFallback, // 函数入参ID,返回R Long time, TimeUnit timeUnit )
  • 解析双层 JSON 结构
//第一层 JSON 转RedisData对象,分出两块内容:业务数据、逻辑过期时间 RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class); //`redisData.getData()`返回 Object,但是object无法解析内部字段,所以接着转JSONObject JSONObject data = (JSONObject) redisData.getData(); //随后就是用方法toBean就是反序列化成需要的对象 R r = JSONUtil.toBean(data, type); //同时取出预先存入的逻辑过期时间`expirTime`,用于判断缓存是否失效 LocalDateTime expirTime = redisData.getExpireTime();
  • 在判断缓存已经过期后就要进入异步更新的逻辑,首先是获取锁
String lockKey = LOCK_SHOP_KEY + id; boolean islock = tryLock(lockKey);
  • 随后开始异步更新
//只有当前线程拿到锁才有下一步 if (islock) { //submit把括号里的任务交给新的子线程去跑,主线程不会等待这段代码执行完毕,会直接跳出if,执行最后的return,返回旧数据,下面的子线程只进行大括号里面的 CACHE_THREAD_POOL.submit(() -> { try{ //apply就是传入参数t后执行传入的逻辑 R r1 = dbFallback.apply(id); //this就是当前对象实例,将最新的数据存进去 this.set(key, r1, time, timeUnit); } catch (Exception e) { throw new RuntimeException(e); //不论try里面的代码是不是对的,一定会执行内部代码 } finally { unLock(lockKey); } }); } //最后return r,但检测到过期的这一次还是旧数据
5.关于queryWithMutex(同步互斥锁解决穿透)
public Shop queryWithMutex(Long id) { //1. 查询缓存 String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); if (StrUtil.isNotBlank(shopJson)) { //缓存命中,直接返回 return BeanUtil.toBean(shopJson, Shop.class); } //2. 缓存是空字符串(之前存的无效id标记,防穿透) if (shopJson != null) { return null; } //走到这里:shopJson == null,缓存彻底过期/无缓存,需要重建 String lockKey = LOCK_SHOP_KEY + id; Shop shop = null; try { //3. 尝试获取分布式锁 boolean flag = tryLock(lockKey); if (!flag) { //没抢到锁:休眠50ms,递归重新执行本方法,再次查缓存 Thread.sleep(50); return queryWithMutex(id); } //4. 抢到锁,查询数据库 shop = getById(id); if (shop == null) { //数据库无数据,存入空字符串防穿透 stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } //5. 数据库有数据,同步写入Redis缓存 stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { //6. 无论正常/异常,强制释放锁,防止死锁 unLock(lockKey); } return shop; }
  • 与queryWithLogicalExpire的区别
互斥锁 缓存没了才上锁,抢到锁才查库写缓存;没抢到就等着重试;能拦截不存在 id(防穿透),用户会卡顿 逻辑过期 缓存一直都在,过期直接返回旧数据,后台悄悄更新;不用等用户无延迟,没法拦截无效 id
6.关于queryWithPassThrough(解决缓存穿透)
  • 还是用自定义泛型,方便复用
  • 主要注意空字符串与null
http://www.jsqmd.com/news/1078862/

相关文章:

  • 2026年国内口碑较好的工艺品设计平台有哪些值得关注
  • BLE Legacy 广播【广播使能】
  • Aeroblade空气动力学设计:从原理到工程实践
  • 代码质量工具静态分析与动态检测
  • 文件包含漏洞之原理、探测、利用、绕过、防御
  • VMware虚拟机导出OVF:绕过ovftool命令行的3种GUI替代方案,小白也能10分钟完成合规打包
  • Spring Cloud 服务注册与发现原理
  • 嵌入式无线通信自动化测试与协议分析实战指南
  • GEO服务商与SEO服务商有什么区别?2026年企业必须搞清的五个关键差异
  • Gemma 4多模态轻量模型实战指南:边缘部署与跨语言推理
  • 多维空间索引结构R树与KD树性能对比研究的技术8
  • 太吾绘卷:天幕心帷下载2026最新带破解
  • 数字取证实战:从系统日志到内存分析,还原安全事件真相
  • 如何快速掌握LangFlow:3步搞定AI应用可视化开发
  • MoE模型推理优化:动态调度与缓存管理实践
  • Gemini 3.1 Pro三层推理与Veo+Lyria多模态协同实战指南
  • CLIP实战避坑指南:图文对齐、零样本迁移与生产部署关键断点
  • 3分钟开启记忆守护:微信聊天记录永久保存的智能方案
  • 智码 AICoder · 桌面桌宠(Desktop Pet)功能完全介绍
  • Product Hunt 每日热榜 | 2026-06-25
  • Streamlit+Heroku部署GAN模型:零运维Web应用实战
  • 机器学习模型评估实战:从accuracy陷阱到AUC-ROC与PR曲线深度解析
  • 3个核心技术突破:Windows系统下LG Ultrafine显示器亮度控制终极方案
  • SpringSecurity 静态资源放行深度详解(解决401认证失败、文件无法访问、URL拦截问题)
  • 分布式算力容器与连续张量拓扑:基于 Gunicorn 多进程套接字复用与 NumPy 共享内存的 IPC 通信架构
  • 从脱靶量最小化到杀伤概率最大化:导弹制导新范式解析
  • MWC26上海 | 移远首款MediaTek平台旗舰级AI算力模组震撼登场
  • 如何在10分钟内搭建AI驱动的无代码测试平台:Testsigma完整实战指南
  • 文本转换(Transforming)
  • 存个对象到localStorage,结果[object Object]?