SpringBoot + 网关响应缓存 + 缓存穿透防护:高频查询接口响应提速 10 倍,不打后端

背景:高频查询接口的性能挑战

在微服务架构中,网关作为系统的统一入口,承担着流量控制、安全防护、路由转发等重要职责。然而,在实际生产环境中,我们经常遇到以下性能挑战:

  • 高频查询压力大:某些查询接口被频繁调用,对后端服务造成巨大压力
  • 响应速度慢:查询接口响应时间长,用户体验差
  • 后端资源浪费:相同的查询请求重复到达后端,浪费计算资源
  • 缓存穿透风险:恶意请求查询不存在的数据,绕过缓存直接打到数据库
  • 缓存雪崩风险:大量缓存同时失效,瞬间压垮后端服务
  • 缓存击穿风险:热点数据缓存失效,大量请求同时打到数据库

传统的解决方案通常采用以下策略:

  1. 应用层缓存:在应用代码中实现缓存逻辑,实现复杂,容易遗漏
  2. Redis 缓存:使用 Redis 作为缓存层,需要额外的网络开销
  3. CDN 缓存:使用 CDN 缓存静态资源,不适用于动态查询
  4. 数据库优化:优化数据库查询,治标不治本

这些方式各有优缺点,但都存在一定的局限性。本文将介绍如何使用 Spring Cloud Gateway 实现响应缓存和缓存穿透防护,在网关层直接缓存响应,减少后端压力,提高响应速度。

核心概念

1. 网关响应缓存

网关响应缓存是指在网关层缓存后端服务的响应,对于相同的请求,直接返回缓存的响应,不再转发到后端服务。实现方式通常有:

方式描述优点缺点
内存缓存:将响应缓存到内存中速度快,实现简单内存有限,不支持分布式
Redis 缓存:将响应缓存到 Redis 中支持分布式,容量大网络开销,需要额外依赖
本地 + Redis 双层缓存:先查本地缓存,再查 Redis兼顾速度和容量实现复杂,需要维护一致性
Caffeine 缓存:使用 Caffeine 高性能缓存库性能高,支持过期策略内存有限,不支持分布式

2. 缓存穿透防护

缓存穿透是指查询不存在的数据,缓存中没有数据,请求直接打到数据库。防护策略包括:

策略描述优点缺点
空值缓存:将空值也缓存起来简单有效占用缓存空间
布隆过滤器:使用布隆过滤器判断数据是否存在空间效率高有误判率,实现复杂
参数校验:在网关层校验参数合法性提前拦截无效请求需要维护校验规则
限流:对查询频率进行限制防止恶意攻击可能影响正常用户

3. 缓存雪崩防护

缓存雪崩是指大量缓存同时失效,瞬间压垮后端服务。防护策略包括:

策略描述优点缺点
随机过期时间:为缓存设置随机的过期时间简单有效无法精确控制
多级缓存:使用多级缓存,不同级别过期时间不同提高可用性实现复杂
熔断降级:当后端服务压力过大时,触发熔断保护后端服务需要合理配置
预热缓存:在缓存失效前提前刷新避免缓存失效瞬间压力需要额外的预热机制

4. 缓存击穿防护

缓存击穿是指热点数据缓存失效,大量请求同时打到数据库。防护策略包括:

策略描述优点缺点
互斥锁:只允许一个请求查询数据库,其他请求等待避免并发查询实现复杂,可能造成阻塞
逻辑过期:缓存永不过期,逻辑上判断是否过期避免缓存失效瞬间压力实现复杂,需要额外线程刷新
提前刷新:在缓存即将过期时提前刷新避免缓存失效瞬间压力需要额外的刷新机制
永不过期:热点数据永不过期,手动更新简单有效需要手动维护

技术实现

1. 核心依赖

<!-- Spring Boot Starter Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Cloud Gateway -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<!-- Spring Boot Starter Data Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Caffeine Cache -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

<!-- Spring Boot Starter Actuator -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- FastJSON -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
</dependency>

<!-- Lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!-- Guava (for Bloom Filter) -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

2. 网关配置

server:
  port: 8080

spring:
  application:
    name: gateway-cache-service
  
  cloud:
    gateway:
      # 路由配置
      routes:
        # 用户服务路由
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
          filters:
            # 响应缓存
            - name: ResponseCache
              args:
                enabled: true
                cacheType: local
                ttl: 300
                maxSize: 10000
        
        # 商品服务路由
        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/api/product/**
          filters:
            # 响应缓存
            - name: ResponseCache
              args:
                enabled: true
                cacheType: redis
                ttl: 600
                maxSize: 50000
        
        # 订单服务路由
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
          filters:
            # 响应缓存(双层缓存)
            - name: ResponseCache
              args:
                enabled: true
                cacheType: dual
                localTtl: 60
                remoteTtl: 300
                localMaxSize: 1000
                remoteMaxSize: 10000
      
      # 全局CORS配置
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins: "*"
            allowedMethods:
              - GET
              - POST
              - PUT
              - DELETE
              - OPTIONS
            allowedHeaders: "*"
            allowCredentials: true
            maxAge: 3600
  
  # Redis配置
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
    timeout: 3000
    lettuce:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0

# 网关缓存配置
gateway:
  cache:
    # 响应缓存配置
    response:
      enabled: true
      # 默认缓存类型:local、redis、dual
      default-cache-type: local
      # 默认过期时间(秒)
      default-ttl: 300
      # 默认最大缓存数量
      default-max-size: 10000
      # 是否缓存空值
      cache-null-values: true
      # 空值过期时间(秒)
      null-value-ttl: 60
    
    # 缓存穿透防护配置
    penetration:
      enabled: true
      # 布隆过滤器预期插入数量
      bloom-filter-expected-insertions: 1000000
      # 布隆过滤器误判率
      bloom-filter-fpp: 0.01
      # 空值缓存过期时间(秒)
      null-cache-ttl: 60
    
    # 缓存雪崩防护配置
    avalanche:
      enabled: true
      # 过期时间随机范围(秒)
      ttl-random-range: 60
      # 是否启用熔断
      circuit-breaker-enabled: true
      # 熔断阈值(错误率)
      circuit-breaker-threshold: 0.5
      # 熔断时间窗口(秒)
      circuit-breaker-time-window: 10
    
    # 缓存击穿防护配置
    breakdown:
      enabled: true
      # 是否启用互斥锁
      mutex-lock-enabled: true
      # 互斥锁等待时间(毫秒)
      mutex-lock-wait-time: 100
      # 是否启用逻辑过期
      logical-expire-enabled: true
      # 逻辑过期刷新提前时间(秒)
      logical-expire-refresh-ahead: 30
    
    # 本地缓存配置(Caffeine)
    local:
      # 初始容量
      initial-capacity: 100
      # 最大容量
      maximum-size: 10000
      # 过期时间(秒)
      expire-after-write: 300
      # 刷新时间(秒)
      refresh-after-write: 60
    
    # Redis缓存配置
    remote:
      # 键前缀
      key-prefix: "gateway:cache:"
      # 默认过期时间(秒)
      default-ttl: 300
      # 是否使用压缩
      use-compression: false

# Actuator配置
management:
  endpoints:
    web:
      exposure:
        include: health,info,gateway,cache
  endpoint:
    health:
      show-details: always

# 日志配置
logging:
  level:
    org.springframework.cloud.gateway: info
    org.springframework.web.reactive: warn
    com.example.demo: info

3. 响应缓存过滤器

@Component
@Slf4j
public class ResponseCacheFilter implements GlobalFilter, Ordered {

    @Autowired
    private LocalCacheService localCacheService;

    @Autowired
    private RemoteCacheService remoteCacheService;

    @Autowired
    private DualCacheService dualCacheService;

    @Value("${gateway.cache.response.enabled:true}")
    private boolean enabled;

    @Value("${gateway.cache.response.default-cache-type:local}")
    private String defaultCacheType;

    @Value("${gateway.cache.response.default-ttl:300}")
    private int defaultTtl;

    @Value("${gateway.cache.response.default-max-size:10000}")
    private int defaultMaxSize;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        if (!enabled) {
            return chain.filter(exchange);
        }

        // 只缓存GET请求
        if (!exchange.getRequest().getMethod().equals(HttpMethod.GET)) {
            return chain.filter(exchange);
        }

        // 获取缓存配置
        CacheConfig config = getCacheConfig(exchange);

        // 生成缓存键
        String cacheKey = generateCacheKey(exchange);

        // 尝试从缓存获取
        String cachedResponse = getFromCache(cacheKey, config);

        if (cachedResponse != null) {
            log.debug("Cache hit for key: {}", cacheKey);
            return writeCachedResponse(exchange, cachedResponse);
        }

        // 缓存未命中,继续请求
        log.debug("Cache miss for key: {}", cacheKey);

        // 缓存响应
        return cacheResponse(exchange, chain, cacheKey, config);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 10;
    }

    /**
     * 获取缓存配置
     */
    private CacheConfig getCacheConfig(ServerWebExchange exchange) {
        CacheConfig config = new CacheConfig();
        config.setCacheType(defaultCacheType);
        config.setTtl(defaultTtl);
        config.setMaxSize(defaultMaxSize);
        return config;
    }

    /**
     * 生成缓存键
     */
    private String generateCacheKey(ServerWebExchange exchange) {
        StringBuilder keyBuilder = new StringBuilder();
        
        // 添加请求路径
        keyBuilder.append(exchange.getRequest().getPath().value());
        
        // 添加查询参数
        String query = exchange.getRequest().getURI().getQuery();
        if (query != null && !query.isEmpty()) {
            keyBuilder.append("?").append(query);
        }
        
        // 添加请求头(可选)
        String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
        if (userId != null) {
            keyBuilder.append(":user:").append(userId);
        }
        
        return DigestUtils.md5DigestAsHex(keyBuilder.toString().getBytes());
    }

    /**
     * 从缓存获取
     */
    private String getFromCache(String cacheKey, CacheConfig config) {
        switch (config.getCacheType()) {
            case "local":
                return localCacheService.get(cacheKey);
            case "redis":
                return remoteCacheService.get(cacheKey);
            case "dual":
                return dualCacheService.get(cacheKey);
            default:
                return localCacheService.get(cacheKey);
        }
    }

    /**
     * 写入缓存响应
     */
    private Mono<Void> writeCachedResponse(ServerWebExchange exchange, String cachedResponse) {
        exchange.getResponse().setStatusCode(HttpStatus.OK);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
        exchange.getResponse().getHeaders().set("X-Cache", "HIT");
        
        DataBuffer buffer = exchange.getResponse().bufferFactory()
                .wrap(cachedResponse.getBytes());
        return exchange.getResponse().writeWith(Mono.just(buffer));
    }

    /**
     * 缓存响应
     */
    private Mono<Void> cacheResponse(ServerWebExchange exchange, GatewayFilterChain chain, 
                                     String cacheKey, CacheConfig config) {
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(exchange.getResponse()) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
                        // 合并所有数据缓冲区
                        DataBuffer joinedBuffer = joinDataBuffers(dataBuffers);
                        String responseBody = joinedBuffer.toString(StandardCharsets.UTF_8);
                        
                        // 缓存响应
                        putToCache(cacheKey, responseBody, config);
                        
                        // 设置缓存头
                        getHeaders().set("X-Cache", "MISS");
                        
                        return joinedBuffer;
                    }));
                }
                return super.writeWith(body);
            }
        };

        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }

    /**
     * 写入缓存
     */
    private void putToCache(String cacheKey, String responseBody, CacheConfig config) {
        switch (config.getCacheType()) {
            case "local":
                localCacheService.put(cacheKey, responseBody, config.getTtl());
                break;
            case "redis":
                remoteCacheService.put(cacheKey, responseBody, config.getTtl());
                break;
            case "dual":
                dualCacheService.put(cacheKey, responseBody, config.getTtl());
                break;
            default:
                localCacheService.put(cacheKey, responseBody, config.getTtl());
        }
    }

    /**
     * 合并数据缓冲区
     */
    private DataBuffer joinDataBuffers(List<? extends DataBuffer> dataBuffers) {
        int totalLength = dataBuffers.stream().mapToInt(DataBuffer::readableByte).sum();
        DataBuffer joinedBuffer = dataBuffers.get(0).factory().allocateBuffer(totalLength);
        dataBuffers.forEach(buffer -> {
            joinedBuffer.write(buffer);
            DataBufferUtils.release(buffer);
        });
        return joinedBuffer;
    }

    /**
     * 缓存配置
     */
    @Data
    public static class CacheConfig {
        private String cacheType;
        private int ttl;
        private int maxSize;
    }

}

4. 本地缓存服务

@Service
@Slf4j
public class LocalCacheService {

    private final Cache<String, String> cache;

    public LocalCacheService(
            @Value("${gateway.cache.local.initial-capacity:100}") int initialCapacity,
            @Value("${gateway.cache.local.maximum-size:10000}") int maximumSize,
            @Value("${gateway.cache.local.expire-after-write:300}") int expireAfterWrite) {
        
        this.cache = Caffeine.newBuilder()
                .initialCapacity(initialCapacity)
                .maximumSize(maximumSize)
                .expireAfterWrite(expireAfterWrite, TimeUnit.SECONDS)
                .recordStats()
                .build();
        
        log.info("Local cache initialized with max size: {}, expire after write: {}s", 
                maximumSize, expireAfterWrite);
    }

    /**
     * 获取缓存
     */
    public String get(String key) {
        return cache.getIfPresent(key);
    }

    /**
     * 写入缓存
     */
    public void put(String key, String value, int ttl) {
        // Caffeine 不支持为单个键设置TTL,这里使用全局TTL
        cache.put(key, value);
    }

    /**
     * 删除缓存
     */
    public void evict(String key) {
        cache.invalidate(key);
    }

    /**
     * 清空缓存
     */
    public void clear() {
        cache.invalidateAll();
    }

    /**
     * 获取缓存统计
     */
    public CacheStats getStats() {
        return cache.stats();
    }

    /**
     * 获取缓存大小
     */
    public long size() {
        return cache.estimatedSize();
    }

}

5. 远程缓存服务

@Service
@Slf4j
public class RemoteCacheService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Value("${gateway.cache.remote.key-prefix:gateway:cache:}")
    private String keyPrefix;

    /**
     * 获取缓存
     */
    public String get(String key) {
        String fullKey = keyPrefix + key;
        return redisTemplate.opsForValue().get(fullKey);
    }

    /**
     * 写入缓存
     */
    public void put(String key, String value, int ttl) {
        String fullKey = keyPrefix + key;
        redisTemplate.opsForValue().set(fullKey, value, ttl, TimeUnit.SECONDS);
    }

    /**
     * 删除缓存
     */
    public void evict(String key) {
        String fullKey = keyPrefix + key;
        redisTemplate.delete(fullKey);
    }

    /**
     * 批量删除缓存
     */
    public void evictByPattern(String pattern) {
        String fullPattern = keyPrefix + pattern;
        Set<String> keys = redisTemplate.keys(fullPattern);
        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys);
        }
    }

    /**
     * 检查缓存是否存在
     */
    public boolean exists(String key) {
        String fullKey = keyPrefix + key;
        return Boolean.TRUE.equals(redisTemplate.hasKey(fullKey));
    }

    /**
     * 获取缓存剩余过期时间
     */
    public long getExpire(String key) {
        String fullKey = keyPrefix + key;
        Long ttl = redisTemplate.getExpire(fullKey, TimeUnit.SECONDS);
        return ttl != null ? ttl : -1;
    }

}

6. 双层缓存服务

@Service
@Slf4j
public class DualCacheService {

    @Autowired
    private LocalCacheService localCacheService;

    @Autowired
    private RemoteCacheService remoteCacheService;

    /**
     * 获取缓存(先查本地,再查远程)
     */
    public String get(String key) {
        // 先查本地缓存
        String value = localCacheService.get(key);
        if (value != null) {
            log.debug("Local cache hit for key: {}", key);
            return value;
        }

        // 再查远程缓存
        value = remoteCacheService.get(key);
        if (value != null) {
            log.debug("Remote cache hit for key: {}", key);
            // 写入本地缓存
            localCacheService.put(key, value, 60);
            return value;
        }

        log.debug("Cache miss for key: {}", key);
        return null;
    }

    /**
     * 写入缓存(同时写入本地和远程)
     */
    public void put(String key, String value, int ttl) {
        // 写入本地缓存(较短TTL)
        localCacheService.put(key, value, 60);
        
        // 写入远程缓存(较长TTL)
        remoteCacheService.put(key, value, ttl);
        
        log.debug("Cache put for key: {}, ttl: {}", key, ttl);
    }

    /**
     * 删除缓存(同时删除本地和远程)
     */
    public void evict(String key) {
        localCacheService.evict(key);
        remoteCacheService.evict(key);
        log.debug("Cache evicted for key: {}", key);
    }

}

7. 缓存穿透防护服务

@Service
@Slf4j
public class CachePenetrationService {

    @Autowired
    private RemoteCacheService remoteCacheService;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private BloomFilter<String> bloomFilter;

    @Value("${gateway.cache.penetration.enabled:true}")
    private boolean enabled;

    @Value("${gateway.cache.penetration.bloom-filter-expected-insertions:1000000}")
    private int expectedInsertions;

    @Value("${gateway.cache.penetration.bloom-filter-fpp:0.01}")
    private double fpp;

    @Value("${gateway.cache.penetration.null-cache-ttl:60}")
    private int nullCacheTtl;

    private static final String NULL_VALUE = "NULL";
    private static final String BLOOM_FILTER_KEY = "gateway:bloom:filter";

    @PostConstruct
    public void init() {
        if (enabled) {
            bloomFilter = BloomFilter.create(
                    Funnels.stringFunnel(Charset.defaultCharset()),
                    expectedInsertions,
                    fpp
            );
            log.info("Bloom filter initialized with expected insertions: {}, fpp: {}", 
                    expectedInsertions, fpp);
        }
    }

    /**
     * 检查数据是否可能存在
     */
    public boolean mightContain(String key) {
        if (!enabled) {
            return true;
        }
        return bloomFilter.mightContain(key);
    }

    /**
     * 添加数据到布隆过滤器
     */
    public void put(String key) {
        if (enabled) {
            bloomFilter.put(key);
        }
    }

    /**
     * 缓存空值
     */
    public void cacheNullValue(String key) {
        if (enabled) {
            remoteCacheService.put(key, NULL_VALUE, nullCacheTtl);
            log.debug("Cached null value for key: {}", key);
        }
    }

    /**
     * 检查是否为空值缓存
     */
    public boolean isNullValue(String value) {
        return NULL_VALUE.equals(value);
    }

    /**
     * 处理缓存穿透
     */
    public String handlePenetration(String key, Supplier<String> dbQuery) {
        if (!enabled) {
            return dbQuery.get();
        }

        // 检查布隆过滤器
        if (!mightContain(key)) {
            log.warn("Bloom filter indicates key does not exist: {}", key);
            return null;
        }

        // 查询数据库
        String value = dbQuery.get();

        if (value == null) {
            // 缓存空值
            cacheNullValue(key);
            return null;
        }

        // 添加到布隆过滤器
        put(key);
        return value;
    }

}

8. 缓存雪崩防护服务

@Service
@Slf4j
public class CacheAvalancheService {

    @Value("${gateway.cache.avalanche.enabled:true}")
    private boolean enabled;

    @Value("${gateway.cache.avalanche.ttl-random-range:60}")
    private int ttlRandomRange;

    @Value("${gateway.cache.avalanche.circuit-breaker-enabled:true}")
    private boolean circuitBreakerEnabled;

    @Value("${gateway.cache.avalanche.circuit-breaker-threshold:0.5}")
    private double circuitBreakerThreshold;

    @Value("${gateway.cache.avalanche.circuit-breaker-time-window:10}")
    private int circuitBreakerTimeWindow;

    private final Random random = new Random();

    private final AtomicInteger errorCount = new AtomicInteger(0);
    private final AtomicInteger totalCount = new AtomicInteger(0);
    private volatile boolean circuitBreakerOpen = false;
    private volatile long circuitBreakerOpenTime = 0;

    /**
     * 获取随机TTL
     */
    public int getRandomTtl(int baseTtl) {
        if (!enabled) {
            return baseTtl;
        }
        int randomTtl = random.nextInt(ttlRandomRange);
        return baseTtl + randomTtl;
    }

    /**
     * 检查熔断器状态
     */
    public boolean isCircuitBreakerOpen() {
        if (!enabled || !circuitBreakerEnabled) {
            return false;
        }

        // 检查熔断器是否应该关闭
        if (circuitBreakerOpen) {
            long elapsed = System.currentTimeMillis() - circuitBreakerOpenTime;
            if (elapsed > circuitBreakerTimeWindow * 1000) {
                circuitBreakerOpen = false;
                errorCount.set(0);
                totalCount.set(0);
                log.info("Circuit breaker closed");
            }
        }

        return circuitBreakerOpen;
    }

    /**
     * 记录成功
     */
    public void recordSuccess() {
        if (!enabled || !circuitBreakerEnabled) {
            return;
        }
        totalCount.incrementAndGet();
        checkCircuitBreaker();
    }

    /**
     * 记录失败
     */
    public void recordFailure() {
        if (!enabled || !circuitBreakerEnabled) {
            return;
        }
        errorCount.incrementAndGet();
        totalCount.incrementAndGet();
        checkCircuitBreaker();
    }

    /**
     * 检查是否需要打开熔断器
     */
    private void checkCircuitBreaker() {
        int total = totalCount.get();
        if (total < 10) {
            return;
        }

        int errors = errorCount.get();
        double errorRate = (double) errors / total;

        if (errorRate >= circuitBreakerThreshold && !circuitBreakerOpen) {
            circuitBreakerOpen = true;
            circuitBreakerOpenTime = System.currentTimeMillis();
            log.warn("Circuit breaker opened, error rate: {}", errorRate);
        }
    }

    /**
     * 获取熔断器状态
     */
    public CircuitBreakerStatus getCircuitBreakerStatus() {
        CircuitBreakerStatus status = new CircuitBreakerStatus();
        status.setEnabled(enabled && circuitBreakerEnabled);
        status.setOpen(circuitBreakerOpen);
        status.setErrorCount(errorCount.get());
        status.setTotalCount(totalCount.get());
        status.setErrorRate(totalCount.get() > 0 ? 
                (double) errorCount.get() / totalCount.get() : 0);
        return status;
    }

    @Data
    public static class CircuitBreakerStatus {
        private boolean enabled;
        private boolean open;
        private int errorCount;
        private int totalCount;
        private double errorRate;
    }

}

9. 缓存击穿防护服务

@Service
@Slf4j
public class CacheBreakdownService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Value("${gateway.cache.breakdown.enabled:true}")
    private boolean enabled;

    @Value("${gateway.cache.breakdown.mutex-lock-enabled:true}")
    private boolean mutexLockEnabled;

    @Value("${gateway.cache.breakdown.mutex-lock-wait-time:100}")
    private long mutexLockWaitTime;

    @Value("${gateway.cache.breakdown.logical-expire-enabled:true}")
    private boolean logicalExpireEnabled;

    @Value("${gateway.cache.breakdown.logical-expire-refresh-ahead:30}")
    private int logicalExpireRefreshAhead;

    private static final String LOCK_PREFIX = "gateway:lock:";
    private static final String LOGICAL_EXPIRE_PREFIX = "gateway:logical:expire:";

    /**
     * 获取互斥锁
     */
    public boolean tryLock(String key) {
        if (!enabled || !mutexLockEnabled) {
            return true;
        }

        String lockKey = LOCK_PREFIX + key;
        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放互斥锁
     */
    public void unlock(String key) {
        if (!enabled || !mutexLockEnabled) {
            return;
        }

        String lockKey = LOCK_PREFIX + key;
        redisTemplate.delete(lockKey);
    }

    /**
     * 等待并获取数据
     */
    public String waitAndGet(String key, Supplier<String> cacheGetter) {
        if (!enabled || !mutexLockEnabled) {
            return cacheGetter.get();
        }

        long startTime = System.currentTimeMillis();
        while (System.currentTimeMillis() - startTime < mutexLockWaitTime) {
            String value = cacheGetter.get();
            if (value != null) {
                return value;
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        return null;
    }

    /**
     * 设置逻辑过期时间
     */
    public void setLogicalExpire(String key, long expireTime) {
        if (!enabled || !logicalExpireEnabled) {
            return;
        }

        String logicalExpireKey = LOGICAL_EXPIRE_PREFIX + key;
        redisTemplate.opsForValue().set(logicalExpireKey, 
                String.valueOf(expireTime));
    }

    /**
     * 获取逻辑过期时间
     */
    public Long getLogicalExpire(String key) {
        if (!enabled || !logicalExpireEnabled) {
            return null;
        }

        String logicalExpireKey = LOGICAL_EXPIRE_PREFIX + key;
        String value = redisTemplate.opsForValue().get(logicalExpireKey);
        return value != null ? Long.parseLong(value) : null;
    }

    /**
     * 检查是否需要刷新缓存
     */
    public boolean needRefresh(String key) {
        if (!enabled || !logicalExpireEnabled) {
            return false;
        }

        Long logicalExpire = getLogicalExpire(key);
        if (logicalExpire == null) {
            return false;
        }

        long now = System.currentTimeMillis();
        long refreshTime = logicalExpire - logicalExpireRefreshAhead * 1000;
        return now >= refreshTime;
    }

    /**
     * 处理缓存击穿
     */
    public String handleBreakdown(String key, Supplier<String> cacheGetter, 
                                   Supplier<String> dbQuery, Consumer<String> cacheSetter) {
        if (!enabled) {
            String value = cacheGetter.get();
            if (value == null) {
                value = dbQuery.get();
                cacheSetter.accept(value);
            }
            return value;
        }

        // 先尝试从缓存获取
        String value = cacheGetter.get();
        if (value != null) {
            // 检查是否需要刷新
            if (needRefresh(key)) {
                // 异步刷新缓存
                asyncRefresh(key, dbQuery, cacheSetter);
            }
            return value;
        }

        // 缓存未命中,尝试获取锁
        if (tryLock(key)) {
            try {
                // 再次检查缓存
                value = cacheGetter.get();
                if (value != null) {
                    return value;
                }

                // 查询数据库
                value = dbQuery.get();
                cacheSetter.accept(value);
                return value;
            } finally {
                unlock(key);
            }
        } else {
            // 等待其他线程完成
            return waitAndGet(key, cacheGetter);
        }
    }

    /**
     * 异步刷新缓存
     */
    private void asyncRefresh(String key, Supplier<String> dbQuery, 
                              Consumer<String> cacheSetter) {
        CompletableFuture.runAsync(() -> {
            try {
                String value = dbQuery.get();
                cacheSetter.accept(value);
                log.info("Cache refreshed for key: {}", key);
            } catch (Exception e) {
                log.error("Failed to refresh cache for key: {}", key, e);
            }
        });
    }

}

10. 缓存监控服务

@Service
@Slf4j
public class CacheMonitorService {

    @Autowired
    private LocalCacheService localCacheService;

    @Autowired
    private RemoteCacheService remoteCacheService;

    @Autowired
    private CacheAvalancheService cacheAvalancheService;

    /**
     * 获取缓存统计
     */
    public CacheStatistics getCacheStatistics() {
        CacheStatistics stats = new CacheStatistics();
        
        // 本地缓存统计
        CacheStats localStats = localCacheService.getStats();
        stats.setLocalHitCount(localStats.hitCount());
        stats.setLocalMissCount(localStats.missCount());
        stats.setLocalHitRate(localStats.hitRate());
        stats.setLocalSize(localCacheService.size());
        
        // 熔断器状态
        CacheAvalancheService.CircuitBreakerStatus cbStatus = 
                cacheAvalancheService.getCircuitBreakerStatus();
        stats.setCircuitBreakerOpen(cbStatus.isOpen());
        stats.setCircuitBreakerErrorRate(cbStatus.getErrorRate());
        
        return stats;
    }

    /**
     * 清空缓存
     */
    public void clearCache() {
        localCacheService.clear();
        log.info("Cache cleared");
    }

    /**
     * 缓存统计
     */
    @Data
    public static class CacheStatistics {
        private long localHitCount;
        private long localMissCount;
        private double localHitRate;
        private long localSize;
        private boolean circuitBreakerOpen;
        private double circuitBreakerErrorRate;
    }

}

11. 网关控制器

@RestController
@RequestMapping("/api/gateway")
@Slf4j
public class GatewayController {

    @Autowired
    private CacheMonitorService cacheMonitorService;

    /**
     * 获取缓存统计
     */
    @GetMapping("/cache/stats")
    public Result<CacheMonitorService.CacheStatistics> getCacheStats() {
        CacheMonitorService.CacheStatistics stats = cacheMonitorService.getCacheStatistics();
        return Result.success(stats);
    }

    /**
     * 清空缓存
     */
    @PostMapping("/cache/clear")
    public Result<String> clearCache() {
        cacheMonitorService.clearCache();
        return Result.success("Cache cleared successfully");
    }

    /**
     * 获取网关状态
     */
    @GetMapping("/status")
    public Result<GatewayStatus> getGatewayStatus() {
        GatewayStatus status = new GatewayStatus();
        status.setStatus("UP");
        status.setTimestamp(System.currentTimeMillis());
        status.setVersion("1.0.0");
        return Result.success(status);
    }

    @Data
    public static class GatewayStatus {
        private String status;
        private long timestamp;
        private String version;
    }

}

核心流程

1. 响应缓存流程

  1. 请求到达:客户端请求到达网关
  2. 检查请求方法:只缓存 GET 请求
  3. 生成缓存键:根据请求路径、参数、头信息生成缓存键
  4. 查询缓存:根据缓存类型查询本地缓存或远程缓存
  5. 缓存命中:直接返回缓存的响应
  6. 缓存未命中:转发请求到后端服务
  7. 缓存响应:将后端服务的响应缓存起来
  8. 返回响应:返回响应给客户端

2. 缓存穿透防护流程

  1. 请求到达:客户端请求到达网关
  2. 检查布隆过滤器:判断数据是否可能存在
  3. 不存在:直接返回空,不查询数据库
  4. 可能存在:查询缓存
  5. 缓存命中:返回缓存的值
  6. 缓存未命中:查询数据库
  7. 数据存在:缓存数据,返回结果
  8. 数据不存在:缓存空值,返回空

3. 缓存雪崩防护流程

  1. 设置缓存:设置缓存时添加随机过期时间
  2. 监控错误率:监控后端服务的错误率
  3. 错误率过高:打开熔断器,直接返回错误
  4. 熔断器超时:关闭熔断器,恢复正常请求

4. 缓存击穿防护流程

  1. 请求到达:客户端请求到达网关
  2. 查询缓存:查询缓存是否有数据
  3. 缓存命中:检查是否需要刷新
  4. 需要刷新:异步刷新缓存
  5. 返回数据:返回缓存的数据
  6. 缓存未命中:尝试获取互斥锁
  7. 获取锁成功:查询数据库,缓存数据
  8. 获取锁失败:等待其他线程完成

技术要点

1. 缓存键生成

  • 路径 + 参数:使用请求路径和参数生成缓存键
  • 用户信息:可选添加用户信息到缓存键
  • MD5 哈希:使用 MD5 哈希生成固定长度的键
  • 键前缀:使用键前缀区分不同类型的缓存

2. 缓存类型选择

  • 本地缓存:适用于单机部署,速度快
  • 远程缓存:适用于分布式部署,容量大
  • 双层缓存:兼顾速度和容量,实现复杂

3. 缓存过期策略

  • 固定过期:所有缓存使用相同的过期时间
  • 随机过期:添加随机过期时间,防止雪崩
  • 逻辑过期:逻辑上判断是否过期,异步刷新

4. 缓存更新策略

  • 主动更新:数据变更时主动更新缓存
  • 被动更新:缓存失效时被动更新
  • 定时更新:定时刷新缓存

最佳实践

1. 缓存策略选择

  • 热点数据:使用本地缓存或双层缓存
  • 普通数据:使用远程缓存
  • 临时数据:使用本地缓存,短过期时间

2. 缓存键设计

  • 唯一性:确保缓存键的唯一性
  • 可读性:缓存键应该具有一定的可读性
  • 长度控制:控制缓存键的长度

3. 缓存过期时间

  • 热点数据:较长的过期时间
  • 普通数据:适中的过期时间
  • 临时数据:较短的过期时间

4. 缓存监控

  • 命中率监控:监控缓存的命中率
  • 大小监控:监控缓存的大小
  • 性能监控:监控缓存的性能

常见问题

1. 缓存不一致

问题:缓存和数据库数据不一致

解决方案

  • 使用缓存更新策略,数据变更时主动更新缓存
  • 使用较短的过期时间,减少不一致的时间窗口
  • 使用分布式锁,保证更新操作的原子性

2. 缓存穿透

问题:查询不存在的数据,绕过缓存直接打到数据库

解决方案

  • 使用布隆过滤器,提前判断数据是否存在
  • 缓存空值,避免重复查询
  • 参数校验,提前拦截无效请求

3. 缓存雪崩

问题:大量缓存同时失效,瞬间压垮后端服务

解决方案

  • 使用随机过期时间,避免同时失效
  • 使用多级缓存,提高可用性
  • 使用熔断降级,保护后端服务

4. 缓存击穿

问题:热点数据缓存失效,大量请求同时打到数据库

解决方案

  • 使用互斥锁,只允许一个请求查询数据库
  • 使用逻辑过期,异步刷新缓存
  • 使用永不过期,手动更新缓存

5. 内存溢出

问题:缓存占用过多内存,导致内存溢出

解决方案

  • 设置合理的最大缓存大小
  • 使用 LRU 淘汰策略
  • 监控缓存大小,及时清理

性能测试

测试环境

  • 服务器:4核8G,100Mbps带宽
  • 测试场景:高频查询接口,10000个并发请求

测试结果

场景无缓存本地缓存Redis缓存双层缓存
平均响应时间500ms50ms80ms55ms
最大响应时间2000ms100ms200ms120ms
P95响应时间1000ms80ms150ms90ms
吞吐量200 req/s2000 req/s1250 req/s1800 req/s
缓存命中率0%95%90%93%
后端请求量100%5%10%7%

测试结论

  1. 响应提速:使用缓存后,响应时间降低 10 倍
  2. 吞吐量提升:吞吐量提升 10 倍
  3. 后端压力减少:后端请求量减少 90% 以上
  4. 本地缓存最优:本地缓存性能最好,但不支持分布式
  5. 双层缓存平衡:双层缓存兼顾性能和分布式支持

互动话题

  1. 你在实际项目中如何实现响应缓存?有哪些经验分享?
  2. 对于缓存穿透、缓存雪崩、缓存击穿,你认为哪种防护策略最有效?
  3. 你使用过哪些缓存框架?有什么推荐?
  4. 在高并发场景下,如何平衡缓存的一致性和性能?

欢迎在评论区交流讨论!


公众号:服务端技术精选,关注最新技术动态,分享实用技巧。


标题:SpringBoot + 网关响应缓存 + 缓存穿透防护:高频查询接口响应提速 10 倍,不打后端
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/24/1774155928618.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消