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;
    }
}

优缺点分析

优点

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

缺点

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

总结

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

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

相关推荐
uzong3 小时前
技术故障复盘模版
后端
GetcharZp3 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程4 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研4 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi4 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国5 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy5 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
鼠鼠我捏,要死了捏6 小时前
生产环境Redis缓存穿透与雪崩防护性能优化实战指南
redis·cache
AntBlack6 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
bobz9657 小时前
pip install 已经不再安全
后端