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)的实现原理。
- 探索缓存穿透与缓存击穿的综合防护方案。