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

Redis集群在高频优惠券查询场景下的缓存穿透与雪崩治理

Redis集群在高频优惠券查询场景下的缓存穿透与雪崩治理

又见面了,我是高佣返利省赚客APP研发者微赚!

在省赚客APP的营销活动中,整点抢券、限时秒杀是常态。面对瞬间涌入的百万级QPS,Redis集群是我们抵御数据库压力的第一道防线。然而,高并发是一把双刃剑,若处理不当,缓存穿透(查询不存在的数据)和缓存雪崩(大量Key同时过期)极易导致后端MySQL崩溃,引发全站服务不可用。本文将深入剖析我们在生产环境中落地的一套组合拳方案,通过布隆过滤器、逻辑过期与随机TTL策略,构建高可用的缓存架构。

基于布隆过滤器的缓存穿透防御

缓存穿透是指用户查询一个根本不存在的数据(如恶意构造的无效优惠券ID),缓存层未命中,请求直接穿透到数据库。由于数据不存在,无法写入缓存,导致每次请求都打到DB,瞬间压垮数据库。传统的“查不到也存空值”方案会浪费大量内存存储无效Key。我们引入了Guava BloomFilter结合Redis Lua脚本的方案,在内存中维护一个极小的位图,快速拦截非法请求。

packagejuwatech.cn.provinceearn.coupon.filter;importcom.google.common.hash.BloomFilter;importcom.google.common.hash.Funnels;importjuwatech.cn.provinceearn.coupon.entity.CouponTemplate;importjuwatech.cn.provinceearn.coupon.repository.CouponRepository;importlombok.extern.slf4j.Slf4j;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Component;importjavax.annotation.PostConstruct;importjava.nio.charset.StandardCharsets;importjava.util.concurrent.TimeUnit;/** * 基于布隆过滤器的优惠券ID校验器 * 解决缓存穿透问题:拦截不存在的CouponID请求 */@Slf4j@ComponentpublicclassCouponBloomFilter{privatefinalStringRedisTemplateredisTemplate;privatefinalCouponRepositorycouponRepository;// 本地布隆过滤器,用于快速预检,减少网络IOprivateBloomFilter<CharSequence>localBloomFilter;privatestaticfinalStringBLOOM_KEY_PREFIX="bloom:coupon:";privatestaticfinallongEXPECTED_INSERTIONS=10_000_000L;// 预计一千万数据privatestaticfinaldoubleFPP=0.00001;// 误判率万分之一publicCouponBloomFilter(StringRedisTemplateredisTemplate,CouponRepositorycouponRepository){this.redisTemplate=redisTemplate;this.couponRepository=couponRepository;}@PostConstructpublicvoidinit(){// 初始化本地布隆过滤器this.localBloomFilter=BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8),EXPECTED_INSERTIONS,FPP);loadAllValidIds();}/** * 全量加载有效ID到布隆过滤器 * 实际生产中可通过监听Binlog或定时任务增量更新 */publicvoidloadAllValidIds(){log.info("Start loading valid coupon IDs to BloomFilter...");couponRepository.streamAllValidIds().forEach(id->{localBloomFilter.put(id);// 同步更新Redis中的布隆过滤器位图(此处简化为直接set,实际应使用Lua脚本操作bitset)redisTemplate.opsForValue().setBit(BLOOM_KEY_PREFIX+"bitmap",hashId(id),true);});log.info("BloomFilter loaded with {} IDs",EXPECTED_INSERTIONS);}/** * 核心校验方法 * @param couponId 优惠券ID * @return true表示可能存在,false表示一定不存在 */publicbooleanmightExist(StringcouponId){// 1. 先查本地布隆过滤器,速度最快if(!localBloomFilter.mightContain(couponId)){returnfalse;}// 2. 二次确认Redis集群中的位图,防止本地数据滞后// 使用Lua脚本保证原子性读取多位LongbitVal=redisTemplate.opsForValue().getBit(BLOOM_KEY_PREFIX+"bitmap",hashId(couponId));returnbitVal!=null&&bitVal==1;}privatelonghashId(Stringid){// 简单的哈希映射,实际需配合Redis布隆过滤器模块或更复杂的hash函数returnMath.abs(id.hashCode())%(EXPECTED_INSERTIONS*2);}}

逻辑过期与互斥锁解决热点Key失效

缓存雪崩通常发生在大量Key在同一时间过期,或者某个热点Key(如“双11主会场券”)突然失效,导致所有请求瞬间涌向数据库。我们采用“逻辑过期”策略:数据永不过期,而是在Value内部封装一个过期时间字段。当发现逻辑过期时,不直接返回旧数据,而是启动一个异步线程去重建缓存,当前请求先返回旧值(兜底),从而保护数据库。

packagejuwatech.cn.provinceearn.coupon.service;importcom.fasterxml.jackson.databind.ObjectMapper;importjuwatech.cn.provinceearn.coupon.dto.CouponCacheWrapper;importjuwatech.cn.provinceearn.coupon.entity.CouponTemplate;importjuwatech.cn.provinceearn.coupon.repository.CouponRepository;importlombok.Data;importlombok.RequiredArgsConstructor;importlombok.extern.slf4j.Slf4j;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Service;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;importjava.util.concurrent.TimeUnit;importjava.util.concurrent.locks.ReentrantLock;/** * 优惠券查询服务 * 实现逻辑过期与互斥锁机制,防止雪崩 */@Slf4j@Service@RequiredArgsConstructorpublicclassCouponQueryService{privatefinalStringRedisTemplateredisTemplate;privatefinalCouponRepositorycouponRepository;privatefinalObjectMapperobjectMapper;// 重建缓存的线程池,隔离业务线程privatefinalExecutorServicerebuildExecutor=Executors.newFixedThreadPool(10);// 针对单个Key的互斥锁,防止并发重建privatefinalReentrantLocklock=newReentrantLock();privatestaticfinalStringCACHE_KEY_PREFIX="coupon:info:";privatestaticfinallongLOGICAL_EXPIRE_SECONDS=3600L;// 逻辑过期时间1小时privatestaticfinallongLOCK_EXPIRE_SECONDS=10L;// 锁超时时间@DatapublicstaticclassCouponCacheWrapper{privateCouponTemplatedata;privatelongexpireTime;// 逻辑过期时间戳}/** * 查询优惠券详情 * 核心逻辑:检查逻辑过期 -> 异步重建 -> 返回兜底数据 */publicCouponTemplategetCouponById(StringcouponId){Stringkey=CACHE_KEY_PREFIX+couponId;Stringjson=redisTemplate.opsForValue().get(key);if(json==null){// 缓存彻底缺失,走互斥锁重建流程returnbuildWithLock(key,couponId);}try{CouponCacheWrapperwrapper=objectMapper.readValue(json,CouponCacheWrapper.class);// 检查是否逻辑过期if(System.currentTimeMillis()>wrapper.getExpireTime()){// 尝试获取重建锁,非阻塞尝试if(lock.tryLock()){try{// 双重检查,防止其他线程已重建StringcheckJson=redisTemplate.opsForValue().get(key);if(checkJson!=null){CouponCacheWrappercheckWrapper=objectMapper.readValue(checkJson,CouponCacheWrapper.class);if(System.currentTimeMillis()<=checkWrapper.getExpireTime()){returncheckWrapper.getData();// 别人已重建,直接返回}}// 启动异步线程重建,当前线程直接返回旧数据(不阻塞用户)rebuildExecutor.submit(()->rebuildCache(key,couponId));}finally{lock.unlock();}}// 无论是否抢到锁,都先返回旧数据,保证高可用returnwrapper.getData();}returnwrapper.getData();}catch(Exceptione){log.error("Deserialize cache error",e);returnbuildWithLock(key,couponId);}}/** * 互斥锁重建缓存(用于缓存完全缺失场景) */privateCouponTemplatebuildWithLock(Stringkey,StringcouponId){if(lock.tryLock()){try{// 再次检查DB,防止并发StringcheckJson=redisTemplate.opsForValue().get(key);if(checkJson!=null){// 解析并返回// ...省略解析代码returnnull;// 示意}CouponTemplatedbData=couponRepository.findById(couponId).orElse(null);if(dbData==null){// 数据库中也没有,写入空值防穿透,设置短TTLredisTemplate.opsForValue().set(key,"",5,TimeUnit.MINUTES);returnnull;}saveToCache(key,dbData);returndbData;}finally{lock.unlock();}}else{// 没抢到锁,休眠重试try{Thread.sleep(50);}catch(InterruptedExceptione){e.printStackTrace();}returngetCouponById(couponId);}}/** * 执行缓存重建 */privatevoidrebuildCache(Stringkey,StringcouponId){try{CouponTemplatedata=couponRepository.findById(couponId).orElse(null);if(data!=null){saveToCache(key,data);log.info("Cache rebuilt for coupon: {}",couponId);}}catch(Exceptione){log.error("Rebuild cache failed",e);}}privatevoidsaveToCache(Stringkey,CouponTemplatedata){try{CouponCacheWrapperwrapper=newCouponCacheWrapper();wrapper.setData(data);wrapper.setExpireTime(System.currentTimeMillis()+LOGICAL_EXPIRE_SECONDS*1000);Stringjson=objectMapper.writeValueAsString(wrapper);// 物理上永不过期,靠逻辑时间控制redisTemplate.opsForValue().set(key,json);}catch(Exceptione){log.error("Save cache error",e);}}}

随机TTL防止集体失效

除了上述主动防御,对于普通非热点数据,我们在写入缓存时,会在基础过期时间上增加一个随机值(如1-5分钟),避免同一批次生成的优惠券在同一秒集体过期,从概率上规避雪崩风险。

longrandomTTL=baseTTL+newRandom().nextInt(300);redisTemplate.expire(key,randomTTL,TimeUnit.SECONDS);

结语

通过布隆过滤器拦截非法流量,利用逻辑过期机制实现故障自愈,辅以随机TTL打散过期时间,省赚客APP在多次大促中成功抵御了亿级流量的冲击,实现了Redis集群的零雪崩、零穿透。这套高可用架构已成为我们应对高并发场景的标准范式。

本文著作权归 省赚客app 研发团队,转载请注明出处!

http://www.jsqmd.com/news/1249917/

相关文章:

  • 深入解析I2C总线:从时钟同步到SMBus协议的工业级应用
  • 2026企业级AI编程平台推荐,国内主流企业级AI代码助手市场趋势及选型指南
  • 2026泉州本地SEO/GEO优化公司靠谱口碑推荐,技术实力+落地实战附服务商案例盘点 - 资讯速览
  • 桥隧坡监测服务商怎么选?
  • 2026年北京门头沟管道疏通避坑指南 - 余生黄金回收
  • 线性代数硬核实战:向量组的秩与 SVD 低秩近似(附完整代码)
  • 昇腾NPU上MLP算子融合优化实践与性能提升
  • 百达翡丽专柜中国2026年7月客户服务热线最新,服务更贴心 - 百达翡丽官方售后中心
  • 2026大模型API聚合平台选型指南:三协议统一接入如何降低AI工程复杂度?
  • 银行流水核查怎么自动化?单边匹配、双向勾稽与图聚类异常检测的工程对比
  • 节日送礼/高端礼赠/孕妇关怀全覆盖:2026燕窝礼盒品牌推荐,送礼不踩雷 - 商业科技观察
  • 2026菏泽正规SEO/GEO服务商口碑推荐,服务商筛选及避坑指南全场景适配 - 资讯速览
  • OpenClaw本地AI助手部署与配置指南
  • 2026邢台黄金回收白银回收铂金回收市民首选无隐形扣费正规备案回收门店联系方式推荐
  • 视频前景检测算法在TMS320C64x+ DSP上的优化实践与选型指南
  • 2026年7月常熟装饰装修公司最新盘点:新房整装、旧房改造、全屋工装服务参考指南 - 海棠依旧大
  • 18使用腾讯云「云存储 + 静态网站托管」旧模式搭建复盘
  • 5 分钟落地本地 AI!Hermes Windows 一体化整合包免环境配置完整实操
  • 学生工作管理系统:高效管理与利用学生资料的新方式
  • 2026年7月浪琴香港售後服務中心地址|客戶熱線+網點攻略 - 浪琴官方售后服务中心
  • ====C++map(映射)的常用用法
  • 真假?扁桃体炎可能引发肾炎?
  • Claude Code架构解析:七层设计与AI工程实践
  • 2026新余黄金回收白银回收铂金回收价格高无损耗专业鉴定本地人常去门店联系方式推荐
  • TI Cortex-R4F时钟系统与CCM-R4F安全访问机制深度解析
  • 2026 年企业体系认证咨询服务深度评测与选型指南
  • 江诗丹顿中国售后服务中心网点地址与24小时热线实地考察报告_多信源验证(2026年7月更新) - 江诗丹顿服务中心
  • 经典算法实例应用:N叉树的后序遍历(一)
  • AI做会议纪要全链路拆解(从语音转写到行动项提取):实测17款工具后,这4个组合方案真正落地可用
  • 主板后边为什么经常有两个以太网口?第二个网口的4种用法你绝对不知道