java - redis 缓存击穿 -逻辑过期

发布时间:2026/11/6 1:36:23
java - redis 缓存击穿 -逻辑过期 逻辑过期 代码使用 逻辑过期 解决 缓存击穿逻辑过期时长这里设20秒privatestaticfinallongLOGIC_EXPIRE_SECONDS20L;创建独立线程池专门用来异步更新缓存只创建一次privatestaticfinalExecutorServiceCACHE_REBUILD_EXECUTORExecutors.newFixedThreadPool(10);OverridepublicResultqueryShopByLogicExpire(Longid){StringkeyCACHE_SHOP_KEYid;StringshopJsonstringRedisTemplate.opsForValue().get(key);// 逻辑过期方案需要提前预热缓存如果redis没有key直接返回不存在if(StrUtil.isBlank(shopJson)){returnResult.fail(商铺不存在);}// 反序列化RedisData对象RedisDataredisDataJSONUtil.toBean(shopJson,RedisData.class);Shopshop(Shop)redisData.getData();LocalDateTimeexpireTimeredisData.getExpireTime();判断 逻辑过期 是否过期过期时间在当前时间之后 未过期if(expireTime.isAfter(LocalDateTime.now())){// 未过期直接返回商铺信息returnResult.ok(shop);}走到这里缓存逻辑过期了StringlockKeyLOCK_SHOP_KEYid;booleanisLocktryLogicLock(lockKey);if(isLock){// 获取锁成功开启异步线程去更新缓存CACHE_REBUILD_EXECUTOR.submit(()-{try{saveShopRedis(id,LOGIC_EXPIRE_SECONDS);}finally{// 释放锁unlock(lockKey);}});}重点不管拿到锁还是没拿到锁主线程直接返回旧数据不等待returnResult.ok(shop);}// 逻辑过期时间 - 获取锁privatebooleantryLogicLock(StringlockKey){// NX锁10秒BooleanflagstringRedisTemplate.opsForValue().setIfAbsent(lockKey,1,10,TimeUnit.SECONDS);returnBooleanUtil.isTrue(flag);}缓存预热方法项目启动时手动调用提前把数据放入Redis。逻辑过期方案必须先预热不能等查询的时候才存入redis。publicvoidsaveShopRedis(Longid,LongexpireSeconds){// 1. 查询数据库ShopshopgetById(id);// 2. 封装RedisDataRedisDataredisDatanewRedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));// 3. 写入Redis不设置过期时间stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEYid,JSONUtil.toJsonStr(redisData));}privatevoidunlock(StringlockKey){stringRedisTemplate.delete(lockKey);}完整执行流程前提提前调用 saveShopRedis() 预热cache:shop:1已经存在Rediskey没有物理过期时间value里面包含shop数据 逻辑过期时间。用户请求接口传入商铺 id拼接 cache:shop:1查询 Redis 拿到 json 字符串。判断StrUtil.isBlankRedis有数据继续执行。JSON字符串反序列成RedisData对象。取出shop对象强制转换Object→Shop、取出逻辑过期时间expireTime。判断 expireTime 是否在当前时间之后✅未过期直接返回shop请求结束。❌已过期进入缓存更新逻辑。拼接锁 keylock:shop:1尝试获取分布式锁。✅抢到锁向线程池提交异步任务新开线程执行saveShopRedis。异步线程查DB封装RedisData写入Redis更新数据与新的逻辑过期时间最后释放锁。❌没抢到锁不做任何操作。主线程不等待异步任务直接返回旧的shop数据给前端后续再来查询这个商铺Redis已经是异步线程更新后的新数据逻辑过期时间也刷新直接返回新数据。核心特点缺陷✅特点Redis 的 key 物理上永不过期靠 value 内部的 expireTime 判断过期缓存过期后用户请求不会阻塞没有sleep高并发性能好分布式锁只用来控制同一时刻最多只有1个异步线程更新缓存不是阻塞用户弱一致性缓存过期之后异步更新完成前所有用户拿到旧数据。❌缺陷必须提前预热缓存不能访问时动态加载忘记预热直接返回不存在弱一致性存在旧数据窗口解锁直接delete没有锁持有者校验存在锁误删风险Redis key永久存在占用Redis内存不能解决缓存穿透只解决缓存击穿。和互斥锁方案一句话区分互斥锁Redis 真实删除 key请求阻塞等待读到最新数据强一致性。逻辑过期Redis 保留 key判断内部逻辑时间请求不阻塞短暂读到旧数据高并发。Executors.newFixedThreadPool(10)privatestaticfinalExecutorServiceCACHE_REBUILD_EXECUTORExecutors.newFixedThreadPool(10);ExecutorService是线程池接口定义提交任务、关闭线程池等方法。Executors.newFixedThreadPool(10)返回的对象实现了这个接口用接口接收面向接口编程CACHE_REBUILD_EXECUTOR变量名语义缓存重建线程池。专门用来跑「异步更新缓存」的任务不和Tomcat 的 HTTP工作线程 混用。Executors.newFixedThreadPool(10)Executors是 JDK 提供的 线程池工具类。newFixedThreadPool(10)→固定大小线程池核心线程数量10。特性线程数量固定为10个最多同时跑10个缓存重建任务任务多了多余任务进入无界队列LinkedBlockingQueue排队等待线程不会自动回收常驻内存适合短时间、高频的异步任务这里就是用来后台更新Redis缓存CACHE_REBUILD_EXECUTOR.submit(()-{try{saveShopRedis(id,LOGIC_EXPIRE_SECONDS);}finally{unlock(lockKey);}});.submit()提交任务给线程池从线程池拿出一条空闲线程去执行括号里面的代码而当前用户请求的主线程Tomcat线程不会卡住直接往下执行return返回旧数据核心目的把缓存重建任务交给独立线程池异步执行不占用Tomcat处理HTTP请求的工作线程。如果不单独开线程池用 Tomcat 本身的线程去做更新那这个请求就会被阻塞逻辑过期 “不阻塞” 的优势就没了。无界队列固定线程池newFixedThreadPool(10)最多同时10个线程干活。无界队列里排队的任务只要有一个线程空闲出来就从队列头部取出一个任务交给这个线程执行。例子线程池核心线程 10第110个任务直接分配给10条线程全部开始执行第11、12、13…任务线程全部忙进LinkedBlockingQueue无界队列排队假设任务1执行完毕线程1空闲了线程池检测到有空闲线程从队列队首取出排在最前面的任务任务11线程1执行任务11队列里剩下任务继续等待哪个线程干完活就从队列拿一个新任务队列是先进先出 FIFO先来的任务先被执行。⚠️ 重点LinkedBlockingQueue 是无界队列队列没有上限可以无限往里塞任务。极端场景大量缓存同时过期疯狂submit任务任务不断堆在队列里队列越来越大占用堆内存最终OOM。这就是阿里开发手册禁止直接用Executors.newFixedThreadPool的原因。补充线程池 和 Tomcat线程 区别Tomcat线程池专门接收、处理用户 http 请求请求来了就用这个线程执行 controller/service 代码。CACHE_REBUILD_EXECUTOR独立线程池只做后台缓存更新和http请求线程隔离。用户主线程Tomcat线程只是提交任务提交完就解放直接返回结果真正查DB、更新Redis的动作交给缓存重建线程池里的线程。面试小问题Q为什么要单独创建一个线程池不用Tomcat线程直接做更新A如果用 Tomcat 线程去同步更新缓存主线程就会阻塞等待更新完成逻辑过期方案“不阻塞请求”的特点就失效。单独线程池把重建任务异步后台执行Tomcat 请求线程提交任务后立刻返回提升并发能力。Qstatic在这里的意义Astatic 保证整个项目只有这一个线程池实例不会多次创建多个线程池避免创建大量多余线程消耗系统资源。Qfinal的作用A防止代码其他地方不小心把CACHE_REBUILD_EXECUTOR重新赋值为null或者另一个线程池保证引用不变。QExecutors.newFixedThreadPool()底层队列是无界队列。如果短时间大量缓存同时过期大量任务不断丢进队列队列无限膨胀会OOM内存溢出。面试标准答案开发演示写这个没问题生产环境不推荐 Executors 快捷创建线程池推荐手动ThreadPoolExecutor构造方法自己指定队列最大容量 拒绝策略防止任务堆积OOM。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询