在高并发系统中,Redis作为缓存中间件已成为标配,它能有效减轻数据库压力、提升系统响应速度。然而,缓存并非万能,在实际应用中我们常常面临一个严峻问题------缓存穿透。
这种现象可能导致Redis失效,使大量请求直接冲击数据库,造成系统性能急剧下降甚至宕机。
缓存穿透原理分析
什么是缓存穿透?
缓存穿透是指查询一个根本不存在的数据,由于缓存不命中,请求会穿透缓存层直接访问数据库。这种情况下,数据库也无法查询到对应数据,因此无法将结果写入缓存,导致每次同类请求都会重复访问数据库。
典型场景与危害
rust
Client ---> Redis(未命中) ---> Database(查询无果) ---> 不更新缓存 ---> 循环重复
缓存穿透的主要危害:
- 数据库压力激增:大量无效查询直接落到数据库
- 系统响应变慢:数据库负载过高导致整体性能下降
- 资源浪费:无谓的查询消耗CPU和IO资源
- 安全风险:可能被恶意利用作为拒绝服务攻击的手段
缓存穿透通常有两种情况:
- 正常业务查询:查询的数据确实不存在
- 恶意攻击:故意构造不存在的key进行大量请求
下面介绍六种有效的防范策略。
策略一:空值缓存
原理
空值缓存是最简单直接的防穿透策略。当数据库查询不到某个key对应的值时,我们仍然将这个"空结果"缓存起来(通常以null值或特定标记表示),并设置一个相对较短的过期时间。这样,下次请求同一个不存在的key时,可以直接从缓存返回"空结果",避免再次查询数据库。
实现示例
java
@Service
public class UserServiceImpl implements UserService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
private static final String KEY_PREFIX = "user:";
private static final String EMPTY_VALUE = "{}"; // 空值标记
private static final long EMPTY_VALUE_EXPIRE_SECONDS = 300; // 空值过期时间
private static final long NORMAL_EXPIRE_SECONDS = 3600; // 正常值过期时间
@Override
public User getUserById(Long userId) {
String redisKey = KEY_PREFIX + userId;
// 1. 查询缓存
String userJson = redisTemplate.opsForValue().get(redisKey);
// 2. 缓存命中
if (userJson != null) {
// 判断是否为空值
if (EMPTY_VALUE.equals(userJson)) {
return null; // 返回空结果
}
// 正常缓存,反序列化并返回
return JSON.parseObject(userJson, User.class);
}
// 3. 缓存未命中,查询数据库
User user = userMapper.selectById(userId);
// 4. 写入缓存
if (user != null) {
// 数据库查到数据,写入正常缓存
redisTemplate.opsForValue().set(redisKey,
JSON.toJSONString(user),
NORMAL_EXPIRE_SECONDS,
TimeUnit.SECONDS);
} else {
// 数据库未查到数据,写入空值缓存
redisTemplate.opsForValue().set(redisKey,
EMPTY_VALUE,
EMPTY_VALUE_EXPIRE_SECONDS,
TimeUnit.SECONDS);
}
return user;
}
}
优缺点分析
优点
- 实现简单,无需额外组件
- 对系统侵入性低
- 立竿见影的效果
缺点
- 可能会占用较多的缓存空间
- 如果空值较多,可能导致缓存效率下降
- 无法应对大规模的恶意攻击
- 短期内可能造成数据不一致(新增数据后缓存依然返回空值)
策略二:布隆过滤器
原理
布隆过滤器(Bloom Filter)是一种空间效率很高的概率型数据结构,用于检测一个元素是否属于一个集合。它的特点是存在误判,即可能会将不存在的元素误判为存在(false positive),但不会将存在的元素误判为不存在(false negative)。
布隆过滤器包含一个很长的二进制向量和一系列哈希函数。当插入一个元素时,使用各个哈希函数计算该元素的哈希值,并将二进制向量中相应位置置为1。查询时,同样计算哈希值并检查向量中对应位置,如果有任一位为0,则元素必定不存在;如果全部位都为1,则元素可能存在。
实现示例
使用Redis的布隆过滤器模块(Redis 4.0+支持模块扩展,需安装RedisBloom):
typescript
@Service
public class ProductServiceWithBloomFilter implements ProductService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ProductMapper productMapper;
private static final String BLOOM_FILTER_NAME = "product_filter";
private static final String CACHE_KEY_PREFIX = "product:";
private static final long CACHE_EXPIRE_SECONDS = 3600;
// 初始化布隆过滤器,可在应用启动时执行
@PostConstruct
public void initBloomFilter() {
// 判断布隆过滤器是否存在
Boolean exists = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.exists(BLOOM_FILTER_NAME.getBytes()));
if (Boolean.FALSE.equals(exists)) {
// 创建布隆过滤器,预计元素量为100万,错误率为0.01
redisTemplate.execute((RedisCallback<Object>) connection ->
connection.execute("BF.RESERVE",
BLOOM_FILTER_NAME.getBytes(),
"0.01".getBytes(),
"1000000".getBytes()));
// 加载所有商品ID到布隆过滤器
List<Long> allProductIds = productMapper.getAllProductIds();
for (Long id : allProductIds) {
redisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.execute("BF.ADD",
BLOOM_FILTER_NAME.getBytes(),
id.toString().getBytes()) != 0);
}
}
}
@Override
public Product getProductById(Long productId) {
String cacheKey = CACHE_KEY_PREFIX + productId;
// 1. 使用布隆过滤器检查ID是否存在
Boolean mayExist = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.execute("BF.EXISTS",
BLOOM_FILTER_NAME.getBytes(),
productId.toString().getBytes()) != 0);
// 如果布隆过滤器判断不存在,则直接返回
if (Boolean.FALSE.equals(mayExist)) {
return null;
}
// 2. 查询缓存
String productJson = redisTemplate.opsForValue().get(cacheKey);
if (productJson != null) {
return JSON.parseObject(productJson, Product.class);
}
// 3. 查询数据库
Product product = productMapper.selectById(productId);
// 4. 更新缓存
if (product != null) {
redisTemplate.opsForValue().set(cacheKey,
JSON.toJSONString(product),
CACHE_EXPIRE_SECONDS,
TimeUnit.SECONDS);
} else {
// 布隆过滤器误判,数据库中不存在该商品
// 可以考虑记录这类误判情况,优化布隆过滤器参数
log.warn("Bloom filter false positive for productId: {}", productId);
}
return product;
}
// 当新增商品时,需要将ID添加到布隆过滤器
public void addProductToBloomFilter(Long productId) {
redisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.execute("BF.ADD",
BLOOM_FILTER_NAME.getBytes(),
productId.toString().getBytes()) != 0);
}
}
优缺点分析
优点
- 空间效率高,内存占用小
- 查询速度快,时间复杂度O(k),k为哈希函数个数
- 可以有效过滤大部分不存在的ID查询
- 可以与其他策略组合使用
缺点
- 存在误判可能(false positive)
- 无法从布隆过滤器中删除元素(标准实现)
- 需要预先加载所有数据ID,不适合动态变化频繁的场景
- 实现相对复杂,需要额外维护布隆过滤器
- 可能需要定期重建以适应数据变化
策略三:请求参数校验
原理
请求参数校验是一种在业务层面防止缓存穿透的手段。通过对请求参数进行合法性校验,过滤掉明显不合理的请求,避免这些请求到达缓存和数据库层。这种方法特别适合防范恶意攻击。
实现示例
less
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{userId}")
public ResponseEntity<?> getUserById(@PathVariable String userId) {
// 1. 基本格式校验
if (!userId.matches("\d+")) {
return ResponseEntity.badRequest().body("UserId must be numeric");
}
// 2. 基本逻辑校验
long id = Long.parseLong(userId);
if (id <= 0 || id > 100000000) { // 假设ID范围限制
return ResponseEntity.badRequest().body("UserId out of valid range");
}
// 3. 调用业务服务
User user = userService.getUserById(id);
if (user == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(user);
}
}
在服务层也可以增加参数检验:
typescript
@Service
public class UserServiceImpl implements UserService {
// 白名单,只允许查询这些ID前缀(举例)
private static final Set<String> ID_PREFIXES = Set.of("100", "200", "300");
@Override
public User getUserById(Long userId) {
// 更复杂的业务规则校验
String idStr = userId.toString();
boolean valid = false;
for (String prefix : ID_PREFIXES) {
if (idStr.startsWith(prefix)) {
valid = true;
break;
}
}
if (!valid) {
log.warn("Attempt to access invalid user ID pattern: {}", userId);
return null;
}
// 正常业务逻辑...
return getUserFromCacheOrDb(userId);
}
}
优缺点分析
优点
- 实现简单,无需额外组件
- 能在请求早期拦截明显不合理的访问
- 可以结合业务规则进行精细化控制
- 减轻系统整体负担
缺点
- 无法覆盖所有非法请求场景
- 需要对业务非常了解,才能设计合理的校验规则
- 可能引入复杂的业务逻辑
- 校验过于严格可能影响正常用户体验
策略四:接口限流与熔断
原理
限流是控制系统访问频率的有效手段,可以防止突发流量对系统造成冲击。熔断则是在系统负载过高时,暂时拒绝部分请求以保护系统。这两种机制结合使用,可以有效防范缓存穿透带来的系统风险。
实现示例
使用SpringBoot+Resilience4j实现限流和熔断:
kotlin
@Configuration
public class ResilienceConfig {
@Bean
public RateLimiterRegistry rateLimiterRegistry() {
RateLimiterConfig config = RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(1))
.limitForPeriod(100) // 每秒允许100个请求
.timeoutDuration(Duration.ofMillis(25))
.build();
return RateLimiterRegistry.of(config);
}
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 50%失败率触发熔断
.slidingWindowSize(100) // 基于最近100次调用
.minimumNumberOfCalls(10) // 至少10次调用才会触发熔断
.waitDurationInOpenState(Duration.ofSeconds(10)) // 熔断后等待时间
.build();
return CircuitBreakerRegistry.of(config);
}
}
@Service
public class ProductServiceWithResilience {
private final ProductMapper productMapper;
private final StringRedisTemplate redisTemplate;
private final RateLimiter rateLimiter;
private final CircuitBreaker circuitBreaker;
public ProductServiceWithResilience(
ProductMapper productMapper,
StringRedisTemplate redisTemplate,
RateLimiterRegistry rateLimiterRegistry,
CircuitBreakerRegistry circuitBreakerRegistry) {
this.productMapper = productMapper;
this.redisTemplate = redisTemplate;
this.rateLimiter = rateLimiterRegistry.rateLimiter("productService");
this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("productService");
}
public Product getProductById(Long productId) {
// 1. 应用限流器
return rateLimiter.executeSupplier(() -> {
// 2. 应用熔断器
return circuitBreaker.executeSupplier(() -> {
return doGetProduct(productId);
});
});
}
private Product doGetProduct(Long productId) {
String cacheKey = "product:" + productId;
// 查询缓存
String productJson = redisTemplate.opsForValue().get(cacheKey);
if (productJson != null) {
return JSON.parseObject(productJson, Product.class);
}
// 查询数据库
Product product = productMapper.selectById(productId);
// 更新缓存
if (product != null) {
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), 1, TimeUnit.HOURS);
} else {
// 空值缓存,短期有效
redisTemplate.opsForValue().set(cacheKey, "", 5, TimeUnit.MINUTES);
}
return product;
}
// 熔断后的降级方法
private Product fallbackMethod(Long productId, Throwable t) {
log.warn("Circuit breaker triggered for productId: {}", productId, t);
// 返回默认商品或者从本地缓存获取
return new Product(productId, "Temporary Unavailable", 0.0);
}
}
优缺点分析
优点
- 提供系统级别的保护
- 能有效应对突发流量和恶意攻击
- 保障系统稳定性和可用性
- 可以结合监控系统进行动态调整
缺点
- 可能影响正常用户体验
- 配置调优有一定难度
- 需要完善的降级策略
- 无法彻底解决缓存穿透问题,只是减轻其影响
策略五:缓存预热
原理
缓存预热是指在系统启动或特定时间点,提前将可能被查询的数据加载到缓存中,避免用户请求时因缓存不命中而导致的数据库访问。对于缓存穿透问题,预热可以提前将有效数据的空间占满,减少直接查询数据库的可能性。
实现示例
typescript
@Component
public class CacheWarmUpTask {
@Autowired
private ProductMapper productMapper;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RedisBloomFilter bloomFilter;
// 系统启动时执行缓存预热
@PostConstruct
public void warmUpCacheOnStartup() {
// 异步执行预热任务,避免阻塞应用启动
CompletableFuture.runAsync(this::warmUpHotProducts);
}
// 每天凌晨2点刷新热门商品缓存
@Scheduled(cron = "0 0 2 * * ?")
public void scheduledWarmUp() {
warmUpHotProducts();
}
private void warmUpHotProducts() {
log.info("开始预热商品缓存...");
long startTime = System.currentTimeMillis();
try {
// 1. 获取热门商品列表(例如销量TOP5000)
List<Product> hotProducts = productMapper.findHotProducts(5000);
// 2. 更新缓存和布隆过滤器
for (Product product : hotProducts) {
String cacheKey = "product:" + product.getId();
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(product),
6, TimeUnit.HOURS
);
// 更新布隆过滤器
bloomFilter.add("product_filter", product.getId().toString());
}
// 3. 同时预热一些必要的聚合信息
List<Category> categories = productMapper.findAllCategories();
for (Category category : categories) {
String cacheKey = "category:" + category.getId();
List<Long> productIds = productMapper.findProductIdsByCategory(category.getId());
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(productIds),
12, TimeUnit.HOURS
);
}
long duration = System.currentTimeMillis() - startTime;
log.info("缓存预热完成,耗时:{}ms,预热商品数量:{}", duration, hotProducts.size());
} catch (Exception e) {
log.error("缓存预热失败", e);
}
}
}
优缺点分析
优点
- 提高系统启动后的访问性能
- 减少缓存冷启动问题
- 可以定时刷新,保持数据鲜度
- 避免用户等待
缺点
- 无法覆盖所有可能的数据访问
- 占用额外的系统资源
- 对冷门数据无效
- 需要合理选择预热数据范围,避免资源浪费
策略六:分级过滤策略
原理
分级过滤策略是将多种防穿透措施组合使用,形成多层防护网。通过在不同层次设置过滤条件,既能保证系统性能,又能最大限度地防止缓存穿透。一个典型的分级过滤策略包括:前端过滤 -> API网关过滤 -> 应用层过滤 -> 缓存层过滤 -> 数据库保护。
实现示例
以下是一个多层防护的综合示例:
kotlin
// 1. 网关层过滤(使用Spring Cloud Gateway)
@Configuration
public class GatewayFilterConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("product_route", r -> r.path("/api/product/**")
// 路径格式验证
.and().predicate(exchange -> {
String path = exchange.getRequest().getURI().getPath();
// 检查product/{id}路径,确保id为数字
if (path.matches("/api/product/\d+")) {
String id = path.substring(path.lastIndexOf('/') + 1);
long productId = Long.parseLong(id);
return productId > 0 && productId < 10000000; // 合理范围检查
}
return true;
})
// 限流过滤
.filters(f -> f.requestRateLimiter()
.rateLimiter(RedisRateLimiter.class, c -> c.setReplenishRate(10).setBurstCapacity(20))
.and()
.circuitBreaker(c -> c.setName("productCB").setFallbackUri("forward:/fallback"))
)
.uri("lb://product-service")
)
.build();
}
}
// 2. 应用层过滤(Resilience4j + Bloom Filter)
@Service
public class ProductServiceImpl implements ProductService {
private final StringRedisTemplate redisTemplate;
private final ProductMapper productMapper;
private final BloomFilter<String> localBloomFilter;
private final RateLimiter rateLimiter;
private final CircuitBreaker circuitBreaker;
@Value("${cache.product.expire-seconds:3600}")
private int cacheExpireSeconds;
// 构造函数注入...
@PostConstruct
public void initLocalFilter() {
// 创建本地布隆过滤器作为二级保护
localBloomFilter = BloomFilter.create(
Funnels.stringFunnel(StandardCharsets.UTF_8),
1000000, // 预期元素数量
0.001 // 误判率
);
// 初始化本地布隆过滤器数据
List<String> allProductIds = productMapper.getAllProductIdsAsString();
for (String id : allProductIds) {
localBloomFilter.put(id);
}
}
@Override
public Product getProductById(Long productId) {
String productIdStr = productId.toString();
// 1. 本地布隆过滤器预检
if (!localBloomFilter.mightContain(productIdStr)) {
log.info("Product filtered by local bloom filter: {}", productId);
return null;
}
// 2. Redis布隆过滤器二次检查
Boolean mayExist = redisTemplate.execute(
(RedisCallback<Boolean>) connection -> connection.execute(
"BF.EXISTS",
"product_filter".getBytes(),
productIdStr.getBytes()
) != 0
);
if (Boolean.FALSE.equals(mayExist)) {
log.info("Product filtered by Redis bloom filter: {}", productId);
return null;
}
// 3. 应用限流和熔断保护
try {
return rateLimiter.executeSupplier(() ->
circuitBreaker.executeSupplier(() -> {
return getProductFromCacheOrDb(productId);
})
);
} catch (RequestNotPermitted e) {
log.warn("Request rate limited for product: {}", productId);
throw new ServiceException("Service is busy, please try again later");
} catch (CallNotPermittedException e) {
log.warn("Circuit breaker open for product queries");
throw new ServiceException("Service is temporarily unavailable");
}
}
private Product getProductFromCacheOrDb(Long productId) {
String cacheKey = "product:" + productId;
// 4. 查询缓存
String cachedValue = redisTemplate.opsForValue().get(cacheKey);
if (cachedValue != null) {
// 处理空值缓存情况
if (cachedValue.isEmpty()) {
return null;
}
return JSON.parseObject(cachedValue, Product.class);
}
// 5. 查询数据库(加入DB保护)
Product product = null;
try {
product = productMapper.selectById(productId);
} catch (Exception e) {
log.error("Database error when querying product: {}", productId, e);
throw new ServiceException("System error, please try again later");
}
// 6. 更新缓存(空值也缓存)
if (product != null) {
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(product),
cacheExpireSeconds,
TimeUnit.SECONDS
);
// 确保布隆过滤器包含此ID
redisTemplate.execute(
(RedisCallback<Boolean>) connection -> connection.execute(
"BF.ADD",
"product_filter".getBytes(),
productId.toString().getBytes()
) != 0
);
localBloomFilter.put(productId.toString());
} else {
// 缓存空值,短时间过期
redisTemplate.opsForValue().set(
cacheKey,
"",
60, // 空值短期缓存
TimeUnit.SECONDS
);
}
return product;
}
}
优缺点分析
优点
- 提供全方位的系统保护
- 各层防护互为补充,形成完整防线
- 可以灵活配置各层策略
- 最大限度减少资源浪费和性能损耗
缺点
- 实现复杂度高
- 各层配置需要协调一致
- 可能增加系统响应时间
- 维护成本相对较高
总结
防范缓存穿透不仅是技术问题,更是系统设计和运维的重要环节。
在实际应用中,应根据具体业务场景和系统规模选择合适的策略组合。通常,单一策略难以完全解决问题,而组合策略能够提供更全面的防护。无论采用何种策略,定期监控和性能评估都是保障缓存系统高效运行的必要手段。