Redis缓存穿透的6种防范策略

在高并发系统中,Redis作为缓存中间件已成为标配,它能有效减轻数据库压力、提升系统响应速度。然而,缓存并非万能,在实际应用中我们常常面临一个严峻问题------缓存穿透。

这种现象可能导致Redis失效,使大量请求直接冲击数据库,造成系统性能急剧下降甚至宕机。

缓存穿透原理分析

什么是缓存穿透?

缓存穿透是指查询一个根本不存在的数据,由于缓存不命中,请求会穿透缓存层直接访问数据库。这种情况下,数据库也无法查询到对应数据,因此无法将结果写入缓存,导致每次同类请求都会重复访问数据库。

典型场景与危害

rust 复制代码
Client ---> Redis(未命中) ---> Database(查询无果) ---> 不更新缓存 ---> 循环重复

缓存穿透的主要危害:

  1. 数据库压力激增:大量无效查询直接落到数据库
  2. 系统响应变慢:数据库负载过高导致整体性能下降
  3. 资源浪费:无谓的查询消耗CPU和IO资源
  4. 安全风险:可能被恶意利用作为拒绝服务攻击的手段

缓存穿透通常有两种情况:

  • 正常业务查询:查询的数据确实不存在
  • 恶意攻击:故意构造不存在的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;
    }
}

优缺点分析

优点

  • 提供全方位的系统保护
  • 各层防护互为补充,形成完整防线
  • 可以灵活配置各层策略
  • 最大限度减少资源浪费和性能损耗

缺点

  • 实现复杂度高
  • 各层配置需要协调一致
  • 可能增加系统响应时间
  • 维护成本相对较高

总结

防范缓存穿透不仅是技术问题,更是系统设计和运维的重要环节。

在实际应用中,应根据具体业务场景和系统规模选择合适的策略组合。通常,单一策略难以完全解决问题,而组合策略能够提供更全面的防护。无论采用何种策略,定期监控和性能评估都是保障缓存系统高效运行的必要手段。

相关推荐
bxlj12 分钟前
RocketMQ消息类型
后端
Asthenia041214 分钟前
从NIO到Netty:盘点那些零拷贝解决方案
后端
米开朗基杨42 分钟前
Cursor 最强竞争对手来了,专治复杂大项目,免费一个月
前端·后端
Asthenia041242 分钟前
anal到Elasticsearch数据一致性保障分析(基于RocketMQ)
后端
Asthenia041243 分钟前
整理面试复盘:设计Elasticsearch索引与高效多级分类筛选
后端
Asthenia041244 分钟前
RocketMQ延迟消息可靠性分析与补偿机制
后端
Zhang3451 小时前
深入理解 Java:从基础到进阶的全方位解析
后端
用户4221626741551 小时前
Go八股文——类型断言
后端·面试
brzhang1 小时前
效率神器!TmuxAI:一款无痕融入终端的AI助手,让我的开发体验翻倍提升
前端·后端·算法
用户4221626741551 小时前
Go语言八股文——map
后端·面试