资讯动态

Redis缓存穿透与击穿:实战解决方案与优化策略

发布时间:2026/9/11 20:54:26 来源:尧图企业网站定制
1. 缓存穿透与击穿Redis实战中的高频痛点去年接手一个电商项目时我遇到了一个诡异现象——凌晨促销活动开始瞬间服务器CPU直接飙到100%但数据库监控显示QPS仅为平时的1/10。这就是典型的缓存击穿现场。当时我们紧急采用多级缓存互斥锁方案5分钟内将系统恢复稳定。今天我就结合黑马点评这类高并发场景拆解缓存穿透与击穿的完整解决方案。缓存异常本质上是对缓存层和存储层的协同考验。穿透是指查询不存在的数据如恶意请求不存在的商品ID导致请求直接穿透缓存击穿数据库而击穿则是热点key突然失效如爆款商品缓存过期导致海量请求瞬间压垮数据库。两者都会造成存储层不可用但触发机制和应对策略各有侧重。2. 缓存穿透防御恶意请求的五大防线2.1 布隆过滤器空间效率之王在黑马点评的商品查询接口中我们采用Guava的BloomFilter做前置校验。初始化时加载所有有效商品ID约1000万条仅占用约12MB内存。关键配置参数// 预期元素数量1000万误判率0.1% BloomFilterLong bloomFilter BloomFilter.create( Funnels.longFunnel(), 10_000_000, 0.001 );实测中需注意误判存在但实际不存在的商品会被放行到缓存层数据库新增商品后需同步更新布隆过滤器分布式环境需改用Redis版的布隆过滤器模块2.2 空值缓存以空间换时间的艺术对于明确不存在的商品ID如-1、0等非法ID我们设置短TTL缓存// 缓存空值示例 redisTemplate.opsForValue().set( product:-123456, NULL, 5, TimeUnit.MINUTES );关键经验TTL建议设置在5-30分钟过短会导致防护失效过长会浪费内存。我们曾因设置1小时TTL导致内存报警最终调整为阶梯式过期策略——首次5分钟相同key重复穿透时逐步延长至30分钟。2.3 参数校验第一道防火墙在黑马点评的实践里我们采用三层校验基础格式校验正则匹配商品ID格式业务范围校验如店铺ID是否属于当前城市风控规则校验如单IP秒级访问频次// 商品ID校验示例 public boolean validateProductId(Long id) { // 1. 数值范围 if (id null || id 0) return false; // 2. 业务规则黑马点评的商品ID以62开头 if (!String.valueOf(id).startsWith(62)) return false; // 3. 布隆过滤器二次校验 return bloomFilter.mightContain(id); }2.4 热点监控实时防御系统我们基于Redis的HyperLogLog实现异常请求统计# 记录每个不存在的key被访问次数 PFADD nonexistent_keys product:999999 # 获取异常访问量 PFCOUNT nonexistent_keys当某key的异常访问量超过阈值如5次/秒自动触发IP封禁或验证码策略。2.5 多级缓存架构最后的防线黑马点评采用的分层缓存方案层级组件命中规则过期策略L1本地缓存(Caffeine)热点商品10秒~1分钟L2Redis集群全量商品30分钟~2小时L3持久层(MySQL)缓存未命中无3. 缓存击穿热点key的生存之战3.1 互斥锁分布式环境下的守门人黑马点评的互斥锁实现方案public Product getProduct(Long id) { String key product: id; // 1. 尝试从缓存获取 Product product redisTemplate.opsForValue().get(key); if (product ! null) { return product; } // 2. 获取分布式锁 String lockKey lock: key; try { boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 30, TimeUnit.SECONDS); if (locked) { // 3. 二次检查缓存防止重复查询 product redisTemplate.opsForValue().get(key); if (product null) { // 4. 查询数据库 product productMapper.selectById(id); // 5. 写入缓存 redisTemplate.opsForValue().set( key, product, getRandomTTL(), TimeUnit.MINUTES ); } return product; } else { // 6. 未获取到锁时的降级策略 return getHotProductFromLocalCache(id); } } finally { // 7. 释放锁 redisTemplate.delete(lockKey); } }避坑指南我们曾遇到锁过期时间设置过短10秒导致数据库查询未完成锁已释放引发雪崩。最终采用业务执行时间缓冲时间如5秒的动态锁超时策略。3.2 逻辑过期时间解耦的智慧对于黑马点评的首页推荐商品我们采用物理永不过期逻辑过期方案// 缓存数据结构 { data: {id: 123, name: 黑马特惠套餐}, expire: 1677720000 // 逻辑过期时间戳 } // 读取逻辑 public Product getProductWithLogicExpire(Long id) { String json redisTemplate.opsForValue().get(product: id); if (json ! null) { CacheItem item JSON.parseObject(json, CacheItem.class); if (item.getExpire() System.currentTimeMillis()/1000) { return item.getData(); } else { // 异步更新缓存 asyncRebuildCache(id); return item.getData(); // 返回旧数据 } } return getProduct(id); // 走正常流程 }3.3 热点发现提前布防的艺术我们基于Redis的zset实现热点key自动识别# 商品访问计数器 ZINCRBY hot_products 1 product:123 # 每10分钟扫描top100 hot_items ZREVRANGE hot_products 0 99 WITHSCORES for item in hot_items: if int(item[1]) 1000: # 阈值 extendTTL(item[0]) # 延长过期时间 preloadToLocal(item[0]) # 预热本地缓存3.4 多级缓存层级化防御体系黑马点评的JVM级缓存配置示例CaffeineCaffeineLong, Product caffeine Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(30, TimeUnit.SECONDS) .refreshAfterWrite(10, TimeUnit.SECONDS) .build(); LoadingCacheLong, Product localCache caffeine.build( id - getProductFromRedis(id) );4. 混合策略实战黑马点评的完整方案4.1 穿透防护组合拳我们在黑马点评商品详情页实施的完整流程前端拦截对非法ID参数直接返回404网关层IP限流100次/分钟业务层布隆过滤器空值缓存存储层MySQL查询添加熔断机制public Product getProductDetail(Long id) { // 1. 参数校验 if (!validateProductId(id)) { throw new IllegalArgumentException(非法商品ID); } // 2. 布隆过滤器检查 if (!bloomFilter.mightContain(id)) { return null; } // 3. 查询缓存 Product product redisTemplate.opsForValue().get(product: id); if (NULL.equals(product)) { return null; // 空值缓存命中 } if (product ! null) { return product; } // 4. 带锁查询数据库 return getProductWithLock(id); }4.2 击穿防御体系针对秒杀活动的特殊处理提前预热活动前1小时加载商品数据到Redis过期时间分散基础TTL 30分钟 随机0-10分钟本地缓存兜底Caffeine缓存最近5分钟的热点降级策略当Redis不可用时启用本地静态数据// 随机TTL生成器 private int getRandomTTL() { return 30 ThreadLocalRandom.current().nextInt(10); } // 降级数据加载 private Product getFallbackProduct(Long id) { return staticProductMap.getOrDefault(id, DEFAULT_PRODUCT); }4.3 监控与调优我们建立的监控指标指标名称计算方式报警阈值缓存穿透率空值查询量/总查询量1%缓存击穿次数热点key失效导致的DB查询峰值100次/秒缓存命中率缓存命中量/总查询量90%基于Prometheus的监控配置示例- name: redis_cache_metrics rules: - record: redis:penetration_rate expr: sum(rate(redis_missed_keys{typenull}[5m])) / sum(rate(redis_total_queries[5m])) - alert: HighCachePenetration expr: redis:penetration_rate 0.01 for: 10m5. 疑难排查那些年我们踩过的坑5.1 布隆过滤器的误判风暴曾遇到布隆过滤器误判率设置过高默认0.03导致大量正常商品被拦截。解决方案根据元素数量精确计算所需bit数采用分层布隆过滤器热数据用更低误判率对误判key进行实时监控和动态调整5.2 锁竞争引发的死循环某次大促时锁等待导致线程堆积最终引发OOM。优化措施引入锁超时和退避机制添加线程池队列监控对锁等待时间超过500ms的请求直接降级// 改进后的锁获取逻辑 boolean locked false; long start System.currentTimeMillis(); while (!locked System.currentTimeMillis()-start 500) { locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 30, TimeUnit.SECONDS); if (!locked) { Thread.sleep(50 random.nextInt(50)); // 随机退避 } }5.3 缓存与数据库的一致性困局黑马点评的最终一致性方案更新数据库后发送MQ事件消费者接受到事件后删除Redis缓存记录binlog位置定时任务补偿丢失的事件// 基于RabbitMQ的缓存更新 RabbitListener(queues cache.update.queue) public void handleCacheUpdate(ProductUpdateMsg msg) { String key product: msg.getId(); // 1. 删除旧缓存 redisTemplate.delete(key); // 2. 记录同步位置 binlogService.markPosition(msg.getBinlogPos()); }5.4 热点key的自动发现滞后我们最终实现的实时热点发现方案使用Redis的stream数据结构记录所有key访问Flink实时计算topN热点key通过配置中心动态调整这些key的TTL# Flink热点发现核心逻辑 env.add_source(RedisSource()) .key_by(lambda x: x[key]) .window(SlidingEventTimeWindows.of(Size.minutes(5), Size.seconds(10))) .aggregate(CountAggregate()) .process(TopNFunction(100)) .add_sink(ConfigCenterSink())在实施这些方案后黑马点评的缓存异常率从最初的1.2%降至0.05%以下数据库负载降低60%。特别是秒杀场景下系统从原来的一抢就挂变成可支撑3000 QPS的稳定状态。缓存治理没有银弹需要根据业务特点不断调整策略组合。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价