Redis缓存穿透解决方案实战

发布时间:2026/10/14 2:14:01
Redis缓存穿透解决方案实战
一、写在前面为什么要上缓存做后端开发这几年最常遇到的一个现象就是接口一开始跑得挺顺等流量稍微上来一点数据库的慢查询就冒出来了。尤其是那种热点数据比如商品详情、用户信息、配置项一批请求打过来全是查同一张表里的同一条记录。数据库连接池很快被占满后面的请求只能排队接口响应时间直线上升。这时候最直接的做法就是加一层缓存。Spring Boot 项目里整合 Redis 已经是非常成熟的方案了Redis 本身就是个内存数据库读写都在微秒级别把热点数据挡在数据库前面压力立刻就能降下来。但缓存也不是万能的用不好会引出新的问题其中比较典型的就是缓存穿透。这篇文章就结合我自己项目里的实践把整合 Redis 和解决缓存穿透的完整思路梳理一遍。二、把 Redis 接进 Spring Boot先看依赖。Spring Boot 2.x 和 3.x 的坐标有点区别我项目里用的是 Spring Boot 3引入 starter 就够了dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency配置文件很简单本地开发环境连本机 Redis 就行spring: redis: host: localhost port: 6379 database: 0 password:下一步是配置 RedisTemplate。Spring Boot 自动装配的 RedisTemplate 默认用的是 JDK 序列化存进去的 key 会带一串乱码在 Redis 客户端里根本没法看。所以我一般会自己配一个key 用字符串序列化value 用 JSON 序列化这样排查问题的时候能直接看清楚缓存里存的是什么Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }这里有个人习惯上的小建议key 的命名最好带业务前缀比如商品用product:开头后面跟主键。这样不同业务的缓存不会互相打架清理的时候也好定位。三、缓存穿透是怎么来的缓存穿透说的是请求查的这条数据数据库里根本没有缓存里自然也没有。比如有人拿 id -1 去查商品每次都绕开缓存直接打到数据库。正常用户很少这么干但如果是爬虫或者恶意刷接口大量不存在的 id 涌进来数据库就得反复执行注定返回空的查询缓存完全失效防护作用等于零。跟它容易混淆的是缓存击穿和缓存雪崩。击穿是某个热点 key 刚好过期瞬间大量请求打到数据库雪崩是大批 key 在同一时间过期请求集体涌向后端。这三个问题成因不同处理方式也不一样这里先集中把穿透解决掉。四、方案一缓存空值简单有效对于大多数业务场景最省事的做法是“查不到也缓存”。数据库返回空结果的时候往 Redis 里放一个空对象标记设置一个短一点的过期时间比如三分钟。下次同样的请求进来直接从缓存拿到这个空标记就不用再查数据库了。下面这段是服务层的核心逻辑public Product getProductById(Long id) { String key product: id; Object cacheValue redisTemplate.opsForValue().get(key); if (cacheValue ! null) { if (cacheValue instanceof NullValueMarker) { return null; } return (Product) cacheValue; } Product product productMapper.selectById(id); if (product ! null) { redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES); } else { // 缓存空对象挡住后续的穿透请求 redisTemplate.opsForValue().set(key, new NullValueMarker(), 3, TimeUnit.MINUTES); } return product; }这个方案实现简单大部分场景够用缺点也比较明显如果攻击者故意换着随机 id 查空值会越积越多占用不少内存。所以过期时间要设短配合前面的 key 前缀规范必要时还能统一清理。对于 key 空间特别大、或者不允许存垃圾数据的场景就得考虑第二种方案。五、方案二用布隆过滤器在入口拦一道布隆过滤器的思路很直接在缓存前面再加一个判断先在内存里粗筛一遍。它能明确告诉你“这个值一定不存在”但做不到“一定存在”所以只用来挡掉那些明显非法的请求。我用的是 Guava 里的实现配合 Redis 初始化private BloomFilterLong idFilter BloomFilter.create( Funnels.longFunnel(), 1000000, 0.01); PostConstruct public void initFilter() { ListLong ids productMapper.selectAllIds(); ids.forEach(idFilter::put); } public Product getProductWithBloom(Long id) { if (!idFilter.mightContain(id)) { return null; } return getProductById(id); }这里要先想清楚一点布隆过滤器是在应用启动时把数据库里已有的主键加载进来的之后新增数据要记得同步更新过滤器不然新上架的商品会被误拦线上排查半天还以为是缓存没生效。这个坑我踩过特别提醒一下。六、小结整条链路其实就三层先过布隆过滤器再做缓存判断最后才轮到数据库。缓存空值适合大部分常规业务布隆过滤器适合 key 数量可控、对内存敏感度高的场景两个方案可以单独用也可以组合起来。无论上哪种方案核心思路都是尽量减少无效请求落到数据库。真到了生产环境还要把过期时间、key 规范、慢查询监控一起配合起来缓存这层才算真正稳妥。