1. 为什么需要网关响应缓存与穿透防护?
在分布式系统中,高频查询接口往往成为性能瓶颈的集中地。我最近接手的一个电商平台项目就遇到了典型场景:商品详情页接口在促销期间QPS突破2万,直接导致数据库CPU飙升至90%以上。通过引入网关层缓存配合穿透防护机制,最终将平均响应时间从120ms降至12ms,效果立竿见影。
1.1 高频查询的典型特征
这类接口通常具有三个显著特点:
- 数据实时性要求中等:比如商品信息允许分钟级延迟,不像库存数据需要强一致性
- 查询参数集中化:80%请求集中在20%的热点数据上(遵循二八定律)
- 结果集体积稳定:返回的JSON结构固定且体积可控(通常<10KB)
1.2 传统方案的性能瓶颈
早期我们尝试过以下方案,但都存在明显缺陷:
- 数据库缓存:MySQL查询缓存命中率不足30%,且8.0版本后已被移除
- ORM二级缓存:Hibernate缓存受限于JVM内存,集群环境下一致性难保证
- 业务层缓存:每个微服务各自为战,缓存策略难以统一管理
关键教训:缓存应该尽可能靠近流量入口,网关层是理想位置。就像快递公司在区域分拣中心提前备货,比从总仓每次调货快得多。
2. SpringBoot网关缓存实现方案
2.1 技术选型对比
我们最终采用Spring Cloud Gateway + Redis组合,对比其他方案优势明显:
| 方案 | 吞吐量(QPS) | 内存开销 | 集群支持 | 功能扩展性 |
|---|---|---|---|---|
| Caffeine本地缓存 | 150,000 | 低 | ❌ | 中 |
| Redis单节点 | 50,000 | 中 | ✅ | 高 |
| Redis Cluster | 120,000 | 高 | ✅ | 高 |
| Memcached | 80,000 | 中 | ✅ | 低 |
2.2 核心代码实现
在Gateway的GlobalFilter中实现缓存逻辑:
java复制public class CacheFilter implements GlobalFilter {
private final RedisTemplate<String, Object> redisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String cacheKey = buildCacheKey(exchange.getRequest());
return redisTemplate.opsForValue().get(cacheKey)
.switchIfEmpty(Mono.defer(() -> {
// 缓存未命中时继续执行过滤器链
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
if (response.getStatusCode() == HttpStatus.OK) {
// 将响应体写入缓存
byte[] body = response.getBody().toString().getBytes();
redisTemplate.opsForValue().set(
cacheKey,
body,
Duration.ofMinutes(30));
}
}));
}))
.flatMap(body -> {
// 缓存命中时直接返回
exchange.getResponse().getHeaders()
.setContentType(MediaType.APPLICATION_JSON);
return exchange.getResponse()
.writeWith(Mono.just(exchange.getResponse()
.bufferFactory().wrap((byte[]) body)));
});
}
}
2.3 缓存键设计策略
缓存键的生成直接影响命中率,我们采用多层维度组合:
code复制method:path:param1=value1¶m2=value2:auth=token
例如:
code复制GET:/api/products:skuId=12345&type=2:auth=Bearerxxxx
实测发现包含认证信息的缓存键可使命中率提升40%,但要注意敏感数据脱敏处理。
3. 缓存穿透防护机制
3.1 布隆过滤器实现
防止恶意请求穿透到数据库,我们在网关前置布隆过滤器:
java复制public class BloomFilterHelper {
private final RedissonClient redissonClient;
private static final String BLOOM_NAME = "api:bloom:filter";
public boolean mightContain(String key) {
RBloomFilter<String> bloomFilter = redissonClient
.getBloomFilter(BLOOM_NAME);
bloomFilter.tryInit(1000000L, 0.01);
return bloomFilter.contains(key);
}
public void put(String key) {
RBloomFilter<String> bloomFilter = redissonClient
.getBloomFilter(BLOOM_NAME);
bloomFilter.tryInit(1000000L, 0.01);
bloomFilter.add(key);
}
}
3.2 多级防护策略
我们建立了立体化防护体系:
-
第一层:参数校验
- 检查必填字段
- 数值范围验证(如ID必须>0)
-
第二层:布隆过滤器
- 合法但不存在的数据快速拦截
- 误判率控制在1%以内
-
第三层:空值缓存
- 对查无结果的请求缓存空结果
- 设置较短TTL(如2分钟)
java复制if (!bloomFilter.mightContain(cacheKey)) {
exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
return exchange.getResponse().setComplete();
}
4. 性能优化实战技巧
4.1 缓存预热策略
通过分析历史日志,我们实现了智能预热:
sql复制-- 分析Nginx日志获取热点API
SELECT
request_uri,
COUNT(*) as freq
FROM gateway_logs
WHERE create_time > NOW() - INTERVAL 7 DAY
GROUP BY request_uri
ORDER BY freq DESC
LIMIT 100;
然后通过Job定时预热:
java复制@Scheduled(cron = "0 0 6 * * ?")
public void warmUpCache() {
hotApis.forEach(api -> {
restTemplate.getForObject(api, String.class);
});
}
4.2 动态TTL调整
根据数据特性设置差异化过期时间:
| 数据类型 | 基础TTL | 波动范围 | 刷新机制 |
|---|---|---|---|
| 商品基础信息 | 30min | ±5min | 更新时主动清除 |
| 价格信息 | 1min | 固定 | 依赖短TTL自然过期 |
| 库存信息 | 10s | 固定 | 通过消息队列实时更新 |
4.3 监控指标埋点
通过Micrometer暴露关键指标:
java复制Metrics.counter("gateway.cache.hits", "type", "redis").increment();
Metrics.timer("gateway.cache.latency").record(() -> {
// 缓存操作代码
});
Grafana监控看板应包含:
- 缓存命中率(按接口分组)
- 平均响应时间对比(缓存vs穿透)
- 布隆过滤器拦截率
- Redis内存使用趋势
5. 踩坑实录与解决方案
5.1 大Value导致Redis阻塞
现象:某个商品接口缓存后,Redis偶尔出现超时
根因:该商品包含50张高清图,缓存Value达8MB
解决:
- 压缩图片为WebP格式
- 拆分缓存:基础信息存Redis,图片存CDN
- 增加Redis最大内存检测:
java复制if (redisTemplate.getRequiredConnectionFactory()
.getConnection().info("memory").get("used_memory") > 10_000_000) {
log.warn("Redis内存使用过高");
}
5.2 缓存雪崩应对
场景:凌晨批量任务导致大量缓存同时失效
方案:
- 基础TTL增加随机扰动:
java复制int baseTtl = 1800; // 30分钟
int randomTtl = baseTtl + ThreadLocalRandom.current().nextInt(300);
- 采用二级缓存策略:
- L1:Caffeine(2秒)
- L2:Redis(30分钟)
5.3 灰度发布方案
为保证缓存策略变更安全,我们设计了三阶段发布:
- Shadow模式:双写新旧缓存,不读取新缓存
- 混合模式:50%流量读取新缓存
- 全量模式:100%切新缓存,旧缓存异步清理
对应的流量标记策略:
nginx复制location /api {
proxy_set_header X-Cache-Version "v2";
}
在代码中根据Header决定缓存版本:
java复制String cacheVersion = exchange.getRequest()
.getHeaders().getFirst("X-Cache-Version");
经过三个月的生产验证,这套方案使得核心接口的P99响应时间稳定在20ms以内,Redis集群负载下降60%。最关键的收获是:缓存设计必须与业务场景深度结合,没有放之四海而皆准的银弹方案。比如我们后来发现用户画像接口适合用本地缓存+推模式更新,这与商品接口的拉模式就有本质区别。
