Redis缓存击穿与穿透4种生产级解决方案在高并发业务场景中Redis作为高性能缓存中间件是缓解数据库压力、提升系统响应速度的核心组件。但随着业务流量增长缓存击穿、穿透等问题逐渐成为系统稳定性的潜在风险大量请求绕过缓存直接冲击数据库可能导致数据库连接耗尽、服务雪崩。本文将深度解析缓存击穿与穿透的核心原理结合生产实践提供4种可落地的解决方案并通过对比分析帮助开发者选择适配场景的最优方案。一、缓存击穿与穿透的核心原理要解决问题首先要明确问题的本质。缓存击穿和穿透虽然都会导致数据库压力激增但二者的触发场景、核心原因存在本质区别。1. 缓存穿透不存在的请求直击数据库是什么缓存穿透是指用户请求的数据既不存在于Redis缓存中也不存在于后端数据库中导致所有请求直接绕过缓存访问数据库的现象。为什么会发生常见场景包括恶意攻击如批量请求不存在的用户ID、商品ID、业务逻辑漏洞如前端传入非法参数、数据已被删除但缓存未同步清理。怎么工作的当请求到达系统时先查询Redis缓存缓存未命中后查询数据库数据库也未命中最终返回空结果。由于缓存中不会存储不存在的数据后续相同请求会重复触发“缓存未命中→数据库查询”的流程形成持续的数据库压力。危害短时间内大量穿透请求会耗尽数据库连接池导致正常业务请求无法访问数据库引发服务雪崩。2. 缓存击穿热点缓存过期后的集中冲击是什么缓存击穿是指某个热点Key在缓存中过期的瞬间大量并发请求同时到达绕过缓存直接访问数据库的现象。为什么会发生热点Key通常是访问量极高的数据如秒杀商品详情、热门新闻当这类Key的缓存过期时恰好遇到业务高峰期大量请求同时命中缺失的缓存瞬间将压力转移到数据库。怎么工作的热点Key的缓存有效期结束后第一个请求触发缓存重建逻辑去数据库查询数据并更新缓存但在缓存重建完成前后续的大量请求都会直接访问数据库形成集中冲击。危害数据库在短时间内接收远超承载能力的请求可能导致数据库响应超时、连接耗尽甚至宕机。二、4种生产级解决方案详解针对缓存穿透和击穿问题业界已经形成了多种成熟的解决方案下面结合原理、实现方式、优缺点进行深度讲解。方案1空值缓存 短有效期解决穿透核心原理当数据库查询返回空结果时将这个空结果存储到Redis缓存中并设置一个较短的有效期如5分钟。后续相同请求会直接命中缓存中的空值避免重复查询数据库。实现逻辑接收用户请求先查询Redis缓存缓存命中则直接返回结果包括空值缓存未命中则查询数据库数据库命中则将数据写入缓存并返回数据库未命中则将空值写入缓存设置短有效期并返回。实战代码Java RedisTemplateimportorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.stereotype.Service;importjava.util.concurrent.TimeUnit;ServicepublicclassUserService{privatefinalRedisTemplateredisTemplate;privatefinalUserDaouserDao;// 构造函数注入依赖publicUserService(RedisTemplateredisTemplate,UserDaouserDao){this.redisTemplateredisTemplate;this.userDaouserDao;}publicUsergetUserById(LonguserId){StringcacheKeyuser:userId;// 1. 先查询缓存Useruser(User)redisTemplate.opsForValue().get(cacheKey);if(user!null){// 缓存命中直接返回包括空值returnuser;}// 2. 缓存未命中查询数据库useruserDao.selectById(userId);if(user!null){// 数据库命中写入缓存设置较长有效期如1小时redisTemplate.opsForValue().set(cacheKey,user,1,TimeUnit.HOURS);}else{// 数据库未命中写入空值缓存设置短有效期如5分钟redisTemplate.opsForValue().set(cacheKey,null,5,TimeUnit.MINUTES);}returnuser;}}常见坑点空值缓存的有效期不能过长否则当数据库中新增对应数据时缓存会返回旧的空值导致数据不一致需要确保Redis的内存足够存储大量空值避免占用过多内存影响正常缓存数据。优缺点| 优点 | 缺点 ||------|------|| 实现简单无需额外组件 | 会占用Redis内存存储空值 || 有效拦截重复的穿透请求 | 无法应对动态生成的大量不存在Key如随机攻击 || 对业务代码侵入性低 | 存在短暂的数据不一致风险空值缓存有效期内新增数据 |方案2布隆过滤器解决穿透核心原理布隆过滤器是一种空间效率极高的概率型数据结构通过多个哈希函数将数据映射到一个二进制数组中用于快速判断某个元素是否存在于集合中。将数据库中所有存在的Key预先存入布隆过滤器请求到达时先通过布隆过滤器判断Key是否存在不存在则直接拦截避免访问缓存和数据库。为什么需要空值缓存无法应对海量随机不存在Key的攻击布隆过滤器可以在请求到达缓存前就拦截无效请求从根源上避免穿透。怎么工作的初始化布隆过滤器设置合适的容量和误判率将数据库中所有有效Key如用户ID、商品ID插入布隆过滤器接收用户请求时先通过布隆过滤器判断Key是否存在若布隆过滤器判断不存在则直接返回错误若判断存在则继续执行缓存查询→数据库查询的流程。实战代码Java Guava布隆过滤器importcom.google.common.hash.BloomFilter;importcom.google.common.hash.Funnels;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.stereotype.Service;importjava.nio.charset.StandardCharsets;importjava.util.concurrent.TimeUnit;ServicepublicclassProductService{privatefinalRedisTemplateredisTemplate;privatefinalProductDaoproductDao;// 布隆过滤器预计存储100万条数据误判率0.01%privatefinalBloomFilterbloomFilterBloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8),1000000,0.0001);// 构造函数注入依赖初始化时加载所有有效商品ID到布隆过滤器publicProductService(RedisTemplateredisTemplate,ProductDaoproductDao){this.redisTemplateredisTemplate;this.productDaoproductDao;// 从数据库查询所有有效商品ID并插入布隆过滤器productDao.listAllValidProductIds().forEach(id-bloomFilter.put(product:id));}publicProductgetProductById(LongproductId){StringcacheKeyproduct:productId;// 1. 布隆过滤器判断Key是否存在if(!bloomFilter.mightContain(cacheKey)){// 不存在直接返回空returnnull;}// 2. 查询缓存Productproduct(Product)redisTemplate.opsForValue().get(cacheKey);if(product!null){returnproduct;}// 3. 查询数据库productproductDao.selectById(productId);if(product!null){redisTemplate.opsForValue().set(cacheKey,product,2,TimeUnit.HOURS);}else{// 布隆过滤器误判写入空值缓存避免重复查询redisTemplate.opsForValue().set(cacheKey,null,1,TimeUnit.MINUTES);}returnproduct;}}常见坑点布隆过滤器的容量和误判率需要根据实际业务数据量提前规划容量过小会导致误判率上升当数据库中新增或删除数据时需要同步更新布隆过滤器否则会出现误判如新增数据无法被查询到布隆过滤器无法删除元素若存在大量数据删除场景需要定期重建布隆过滤器。优缺点| 优点 | 缺点 ||------|------|| 空间效率极高占用内存极少 | 存在一定的误判率可通过参数调整 || 拦截效率接近100%从根源上避免穿透 | 无法处理动态数据需要定期更新或重建 || 对业务代码侵入性低 | 无法删除元素不适合频繁删除数据的场景 |方案3互斥锁解决击穿核心原理当热点Key的缓存过期时只有第一个请求可以获取锁并执行缓存重建逻辑其他请求等待锁释放后直接查询更新后的缓存避免大量请求同时访问数据库。怎么工作的请求到达后查询缓存未命中则尝试获取分布式锁获取锁成功则查询数据库更新缓存然后释放锁获取锁失败则等待一段时间后重新查询缓存直到缓存重建完成。实战代码Java Redis分布式锁importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.stereotype.Service;importjava.util.concurrent.TimeUnit;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;ServicepublicclassSeckillService{privatefinalRedisTemplateredisTemplate;privatefinalSeckillDaoseckillDao;// 本地锁同一JVM内的请求互斥减少分布式锁的竞争privatefinalLocklocalLocknewReentrantLock();publicSeckillService(RedisTemplateredisTemplate,SeckillDaoseckillDao){this.redisTemplateredisTemplate;this.seckillDaoseckillDao;}publicSeckillProductgetSeckillProduct(LongproductId){StringcacheKeyseckill:product:productId;// 1. 查询缓存SeckillProductproduct(SeckillProduct)redisTemplate.opsForValue().get(cacheKey);if(product!null){returnproduct;}// 2. 缓存未命中尝试获取本地锁localLock.lock();try{// 双重检查防止本地锁等待期间缓存已被其他线程更新product(SeckillProduct)redisTemplate.opsForValue().get(cacheKey);if(product!null){returnproduct;}// 3. 获取分布式锁避免多JVM实例同时重建缓存StringlockKeylock:seckill:product:productId;BooleanlockAcquiredredisTemplate.opsForValue().setIfAbsent(lockKey,1,30,TimeUnit.SECONDS);if(lockAcquirednull||!lockAcquired){// 未获取到锁等待50ms后重试Thread.sleep(50);returngetSeckillProduct(productId);}try{// 4. 查询数据库并更新缓存productseckillDao.selectById(productId);if(product!null){// 热点数据设置较短有效期避免缓存过期后再次击穿redisTemplate.opsForValue().set(cacheKey,product,10,TimeUnit.MINUTES);}returnproduct;}finally{// 释放分布式锁redisTemplate.delete(lockKey);}}catch(InterruptedExceptione){Thread.currentThread().interrupt();returnnull;}finally{// 释放本地锁localLock.unlock();}}}常见坑点分布式锁的有效期需要设置合理避免缓存重建未完成时锁过期导致多个线程同时重建缓存必须确保锁的释放逻辑在finally块中执行避免异常导致锁无法释放双重检查缓存不可缺少否则本地锁等待期间缓存可能已被其他线程更新导致重复重建。优缺点| 优点 | 缺点 ||------|------|| 彻底解决热点Key击穿问题 | 实现复杂需要处理锁的获取、释放、超时等问题 || 缓存重建逻辑唯一避免重复查询数据库 | 未获取锁的请求会等待增加响应时间 || 适用于所有热点Key场景 | 存在死锁风险如锁释放失败 |方案4热点Key永不过期 后台异步更新解决击穿核心原理对于访问量极高的热点Key设置其缓存永不过期同时通过后台线程定期从数据库查询最新数据并更新缓存避免缓存过期导致的击穿问题。怎么工作的初始化热点Key的缓存时设置为永不过期启动后台定时任务定期查询数据库中热点Key的最新数据将最新数据更新到Redis缓存中请求到达时直接查询缓存无需担心缓存过期。实战代码Java Spring定时任务importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.scheduling.annotation.Scheduled;importorg.springframework.stereotype.Service;importjava.util.List;importjava.util.concurrent.TimeUnit;ServicepublicclassHotNewsService{privatefinalRedisTemplateredisTemplate;privatefinalNewsDaonewsDao;// 热点新闻ID列表可通过监控系统动态获取privatefinalListhotNewsIdsList.of(1001L,1002L,1003L);publicHotNewsService(RedisTemplateredisTemplate,NewsDaonewsDao){this.redisTemplateredisTemplate;this.newsDaonewsDao;// 初始化热点新闻缓存initHotNewsCache();}// 初始化热点新闻缓存设置永不过期privatevoidinitHotNewsCache(){hotNewsIds.forEach(id-{StringcacheKeynews:hot:id;NewsnewsnewsDao.selectById(id);if(news!null){redisTemplate.opsForValue().set(cacheKey,news);}});}// 后台定时任务每5分钟更新一次热点新闻缓存Scheduled(fixedRate300000)publicvoidupdateHotNewsCache(){hotNewsIds.forEach(id-{StringcacheKeynews:hot:id;NewsnewsnewsDao.selectById(id);if(news!null){redisTemplate.opsForValue().set(cacheKey,news);}});}publicNewsgetHotNewsById(LongnewsId){StringcacheKeynews:hot:newsId;// 直接查询缓存无需处理过期逻辑return(News)redisTemplate.opsForValue().get(cacheKey);}}常见坑点热点Key列表需要动态维护否则新增的热点Key无法被覆盖后台定时任务的执行频率需要根据数据更新频率调整频率过高会增加数据库压力频率过低会导致缓存数据不一致需要确保后台任务的可靠性避免任务失败导致缓存长期未更新。优缺点| 优点 | 缺点 ||------|------|| 彻底避免缓存击穿请求响应速度快 | 无法应对临时突发的热点Key || 无需处理锁逻辑实现相对简单 | 存在缓存与数据库的数据不一致风险定时更新间隔内 || 对业务请求无阻塞 | 需要额外的后台任务维护成本 |三、方案对比分析为了帮助开发者快速选择适配场景的解决方案下面从适用场景、实现复杂度、性能影响、数据一致性等维度进行对比方案适用场景实现复杂度性能影响数据一致性解决问题类型空值缓存少量不存在Key的场景业务数据删除频率低低无额外性能损耗短暂不一致空值有效期内穿透布隆过滤器海量不存在Key的攻击场景数据新增/删除频率低中极微性能损耗哈希计算可能存在误判需定期同步穿透互斥锁所有热点Key场景数据更新频率高高未获取锁的请求会等待强一致缓存重建后立即同步击穿热点Key永不过期稳定的高访问热点Key数据更新频率低中无额外性能损耗