30分钟自学教程:Redis缓存穿透原理与解决方案
目标
- 理解缓存穿透的成因及危害。
- 掌握布隆过滤器、空值缓存等核心防御技术。
- 能够通过代码实现请求拦截与缓存保护。
- 学会限流降级、异步加载等应急方案。
教程内容
0~2分钟:缓存穿透的定义与核心原因
- 定义:恶意或异常请求频繁访问数据库中不存在的数据,绕过缓存直接冲击数据库。
- 典型场景:
- 攻击者伪造大量非法ID(如负数、超长字符串)。
- 业务未对查询参数校验,或未缓存空结果。
- 危害:
- 数据库压力激增,甚至宕机。
- 正常服务被恶意请求拖垮。
2~5分钟:代码模拟穿透场景(Java示例)
// 未做防护的查询方法(模拟穿透问题)
public Product getProduct(String id) { String key = "product:" + id; Product product = redisTemplate.opsForValue().get(key); if (product == null) { // 直接查询数据库(未缓存空值) product = productService.loadFromDB(id); if (product != null) { redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS); } } return product; // 恶意请求会反复查询数据库
}
验证问题:
- 使用JMeter发送100次
id=-1
的请求,观察数据库查询次数是否为100次(穿透发生)。
5~12分钟:解决方案1——布隆过滤器(Bloom Filter)
- 原理:基于位数组和哈希函数,快速判断数据是否可能存在于数据库,拦截非法请求。
- 代码实现(Redisson布隆过滤器):
// 初始化布隆过滤器并预热合法数据
public class BloomFilterInit { private RBloomFilter<String> bloomFilter; @PostConstruct public void init() { bloomFilter = redissonClient.getBloomFilter("product_bloom"); bloomFilter.tryInit(100000L, 0.01); // 容量10万,误判率1% List<String> validIds = productService.getAllValidIds(); validIds.forEach(bloomFilter::add); }
} // 查询时拦截非法请求
public Product getProductWithBloomFilter(String id) { if (!bloomFilter.contains(id)) { return null; // 直接拦截 } // 正常查询逻辑...
}
- 注意事项:
- 误判率需根据业务容忍度调整(如0.1%更严格,但占用更多内存)。
- 需定期同步布隆过滤器与数据库的合法数据(如定时任务)。
12~20分钟:解决方案2——空值缓存(Cache Null)
- 原理:即使数据库不存在该数据,也缓存空值(如“NULL”),避免重复穿透。
- 代码实现:
public Product getProductWithNullCache(String id) { String key = "product:" + id; Product product = redisTemplate.opsForValue().get(key); if (product == null) { product = productService.loadFromDB(id); if (product == null) { // 缓存空值,5分钟过期 redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES); return null; } redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS); } else if ("NULL".equals(product)) { return null; // 直接返回空结果 } return product;
}
- 优化点:
- 空值过期时间不宜过长(避免存储大量无效Key)。
- 可结合布隆过滤器,减少空值缓存的数量。
20~25分钟:解决方案3——请求参数校验
- 原理:在业务层拦截非法参数(如非数字ID、越界值)。
- 代码实现(Spring Boot参数校验):
public Product getProduct(@PathVariable String id) { // 校验ID格式(仅允许数字) if (!id.matches("\\d+")) { throw new IllegalArgumentException("非法ID格式"); } // 校验ID范围(如大于0) long numericId = Long.parseLong(id); if (numericId <= 0) { throw new IllegalArgumentException("ID必须为正数"); } // 正常查询逻辑...
}
- 扩展:
- 使用Hibernate Validator实现注解式校验(如
@Min(1)
)。
- 使用Hibernate Validator实现注解式校验(如
25~28分钟:应急处理方案
- 限流降级(Guava RateLimiter):
private RateLimiter rateLimiter = RateLimiter.create(100); // 每秒100个请求 public Product getProduct(String id) { if (!rateLimiter.tryAcquire()) { throw new RuntimeException("请求过于频繁,请稍后重试"); } // 正常查询逻辑...
}
- 异步加载(CompletableFuture):
public Product getProductAsync(String id) { String key = "product:" + id; Product product = redisTemplate.opsForValue().get(key); if (product == null) { CompletableFuture.runAsync(() -> { Product dbProduct = productService.loadFromDB(id); if (dbProduct != null) { redisTemplate.opsForValue().set(key, dbProduct, 1, TimeUnit.HOURS); } }); } return product; // 可能返回空,但避免阻塞请求
}
28~30分钟:总结与优化方向
- 核心原则:拦截非法请求、缓存空值、业务兜底。
- 高级优化:
- 结合Redis Module的
RedisBloom
扩展(生产级布隆过滤器)。 - 动态调整限流阈值(如根据数据库负载自动限流)。
- 结合Redis Module的
练习与拓展
练习
- 实现一个布隆过滤器,拦截
id<=0
的非法请求。 - 修改空值缓存逻辑,动态设置随机过期时间(如5~15分钟)。
推荐拓展
- 学习
RedisBloom
模块的安装与使用。 - 研究分布式限流框架(如Sentinel)的实现原理。
- 探索缓存穿透与缓存击穿的综合防护方案。