Redis之缓存穿透
文章目录
- Redis之缓存穿透
- 一、什么是缓存穿透?
- 二、缓存穿透常见的解决方案
- 1. 缓存空对象(Null Caching)
- 2. 布隆过滤器(Bloom Filter)
- 3. 互斥锁(Mutex Lock)
- 4. 接口层校验
- 5. 热点数据永不过期
- 6. 缓存预热
- 7. 实时监控与限流
- ☆☆☆组合方案推荐☆☆☆
- 四、实践:缓存空对象
一、什么是缓存穿透?
- 缓存穿透的定义:缓存穿透(Cache Penetration)是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,导致数据库压力骤增甚至崩溃。
- 触发原因:恶意攻击、参数伪造、业务逻辑漏洞。
- 核心问题:大量请求访问数据库中不存在的数据,缓存无法拦截。
- 示例场景:攻击者发送大量随机ID查询商品信息,这些ID在数据库中不存在,导致每次请求都穿透缓存直达数据库。
-
与缓存击穿的区别
- 缓存击穿的定义:缓存击穿(Cache Breakdown)是指某个热点key(如爆款商品信息)在缓存中过期后,大量并发请求同时访问数据库(请求数据存在),导致数据库压力骤增。
- 触发原因:缓存过期时间到期,且高并发场景下请求集中失效。
- 核心问题:单个热点key失效后,大量请求同时访问数据库。
- 示例场景:某明星商品突然爆火,缓存中存储的商品信息过期后,所有用户请求同时涌入数据库查询。
维度 缓存穿透 缓存击穿 触发原因 请求不存在的数据 热点key过期后高并发请求 数据合法性 数据本身不存在(非法参数) 数据存在但缓存失效(合法参数) 攻击性 可能是恶意攻击 正常业务高并发 影响范围 分散的无效请求 集中在某个热点key 解决方案 布隆过滤器、缓存空对象 互斥锁、永不过期、后台更新
二、缓存穿透常见的解决方案
1. 缓存空对象(Null Caching)
- 原理: 当查询数据库发现数据不存在时,将空结果(如
null
)写入缓存,并设置较短的过期时间。 - 优点:简单易实现,直接拦截后续相同请求。
- 缺点:
- 内存浪费(存储大量无效
null
值)。 - 可能出现短时不一致,如:数据已补录,但缓存未及时失效。(如需强一致性,可以在更新数据时,删除/覆盖缓存)
- 内存浪费(存储大量无效
- 实现:
public Object getData(String key) {// 1. 查询缓存Object data = cache.get(key);if (data != null) return data;// 2. 查询数据库data = db.query(key);if (data == null) {// 缓存空对象,设置短期过期时间(如5分钟)cache.set(key, "NULL", 5 * 60);} else // 正常数据设置较长过期时间cache.set(key, data, 60 * 60);}return data; }
2. 布隆过滤器(Bloom Filter)
- 原理:在缓存层前加布隆过滤器,预先存储所有合法 Key 的哈希值。查询时先检查布隆过滤器:
- 若返回“不存在”,直接拦截请求。
- 若返回“可能存在”,继续查询缓存/数据库。
- 优点:内存占用低(没有多余的Key),适合海量数据;查询时间复杂度 O(1)。
- 缺点:
- 存在误的可能(可能将不存在判断为存在)。
- 实现复杂,删除元素困难(需重建过滤器)。
- 适用场景:数据量大且允许误判(如黑名单校验)。
- 实现:
// 初始化布隆过滤器(伪代码) BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(), expectedInsertions);// 数据预热时加载存在的key db.keys().forEach(k -> bloomFilter.put(k));public Object getData(String key) {// 1. 先查布隆过滤器if (!bloomFilter.mightContain(key)) {return null; // 直接拦截不存在的key}// 2. 查询缓存/数据库Object data = cache.get(key);if (data == null) {data = db.query(key);cache.set(key, data);}return data; }
3. 互斥锁(Mutex Lock)
- 原理:缓存未命中时,通过互斥锁(如 Redis 的
SETNX
)保证只有一个线程查询数据库,其他线程等待回填缓存。 - 优点:避免大量请求同时穿透到数据库。
- 缺点:
- 分布式环境下需使用分布式锁(如 Redis RedLock)。
- 锁竞争可能成为性能瓶颈。
- 实现:
public Object getData(String key) {Object data = cache.get(key);if (data != null) return data;// 加锁(如Redis的SETNX)String lockKey = "lock:" + key;if (redis.setnx(lockKey, "1", 10)) { // 10秒锁超时try {// 二次检查缓存(防止锁竞争期间其他线程已加载)data = cache.get(key);if (data != null) return data;data = db.query(key);cache.set(key, data);} finally {redis.del(lockKey); // 释放锁}} else {// 等待重试Thread.sleep(100);return getData(key);}return data; }
4. 接口层校验
- 原理:在 API 入口处校验参数合法性,拦截明显无效的请求(如非法 ID 格式、负数等)。
- 优点:低成本防御恶意攻击(如扫描全表 ID)。
- 缺点:无法拦截合法参数但实际不存在的数据请求。
- 示例:校验 ID 是否为正整数、长度是否符合预期。
- 实现:
public ResponseEntity<?> handleRequest(@PathVariable String id) {if (!isValidId(id)) { // 校验ID合法性return ResponseEntity.badRequest().build();}// 继续处理业务逻辑 }
5. 热点数据永不过期
- 原理:对高频访问的热点数据设置永不过期,通过后台线程主动更新缓存。
- 优点:彻底避免缓存失效导致的穿透。
- 缺点:数据一致性依赖更新机制,需处理脏数据问题。
- 实现:结合定时任务或事件驱动更新缓存。
// 缓存写入时设置永不过期 cache.set("hot_key", data);// 后台定时任务刷新数据 @Scheduled(fixedRate = 300000) void refreshHotData() {Object newData = db.query("hot_key");cache.set("hot_key", newData); }
6. 缓存预热
- 原理:在系统启动或低峰期,预先加载热点数据到缓存中。
- 优点:减少冷启动时的缓存穿透风险。
- 缺点:需提前知道热点数据(可通过历史日志分析)。
7. 实时监控与限流
- 原理:监控异常流量(如大量
null
响应),触发限流策略(如令牌桶、漏桶算法),保护数据库。 - 优点:兜底防御,避免突发攻击。
- 缺点:需配套监控和告警系统。
☆☆☆组合方案推荐☆☆☆
- 常规场景:缓存空对象 + 接口参数校验。
- 海量数据:布隆过滤器 + 缓存空对象。
- 高并发热点数据:永不过期缓存 + 后台更新线程 + 互斥锁。
四、实践:缓存空对象
解决根据id查询商铺信息过程中的缓存穿透
@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryById(Long id) {// 1.从redis查询商铺缓存String key = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}// 判断命中的是否是空值if(shopJson != null) {// 返回错误信息,解决缓存穿透问题return Result.fail("店铺信息不存在!");}// 4.不存在,根据id查询数据库Shop shop = getById(id);if (shop == null) {// 5.数据库不存在,将空字串写入Redis,设置过期时间,解决缓存穿透问题stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息,解决缓存穿透问题return Result.fail("店铺不存在!");}// 6.存在,写入redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);return Result.ok(shop);}
总结:缓存穿透的解决方案需结合业务场景选择,通常需要多种手段协同(如布隆过滤器拦截非法 Key + 缓存空对象减少数据库压力)。同时需权衡内存、一致性和性能,避免过度设计。