SpringBoot + 网关响应缓存 + 缓存穿透防护:高频查询接口响应提速 10 倍,不打后端
背景:高频查询接口的性能挑战
在微服务架构中,网关作为系统的统一入口,承担着流量控制、安全防护、路由转发等重要职责。然而,在实际生产环境中,我们经常遇到以下性能挑战:
- 高频查询压力大:某些查询接口被频繁调用,对后端服务造成巨大压力
- 响应速度慢:查询接口响应时间长,用户体验差
- 后端资源浪费:相同的查询请求重复到达后端,浪费计算资源
- 缓存穿透风险:恶意请求查询不存在的数据,绕过缓存直接打到数据库
- 缓存雪崩风险:大量缓存同时失效,瞬间压垮后端服务
- 缓存击穿风险:热点数据缓存失效,大量请求同时打到数据库
传统的解决方案通常采用以下策略:
- 应用层缓存:在应用代码中实现缓存逻辑,实现复杂,容易遗漏
- Redis 缓存:使用 Redis 作为缓存层,需要额外的网络开销
- CDN 缓存:使用 CDN 缓存静态资源,不适用于动态查询
- 数据库优化:优化数据库查询,治标不治本
这些方式各有优缺点,但都存在一定的局限性。本文将介绍如何使用 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. 响应缓存流程
- 请求到达:客户端请求到达网关
- 检查请求方法:只缓存 GET 请求
- 生成缓存键:根据请求路径、参数、头信息生成缓存键
- 查询缓存:根据缓存类型查询本地缓存或远程缓存
- 缓存命中:直接返回缓存的响应
- 缓存未命中:转发请求到后端服务
- 缓存响应:将后端服务的响应缓存起来
- 返回响应:返回响应给客户端
2. 缓存穿透防护流程
- 请求到达:客户端请求到达网关
- 检查布隆过滤器:判断数据是否可能存在
- 不存在:直接返回空,不查询数据库
- 可能存在:查询缓存
- 缓存命中:返回缓存的值
- 缓存未命中:查询数据库
- 数据存在:缓存数据,返回结果
- 数据不存在:缓存空值,返回空
3. 缓存雪崩防护流程
- 设置缓存:设置缓存时添加随机过期时间
- 监控错误率:监控后端服务的错误率
- 错误率过高:打开熔断器,直接返回错误
- 熔断器超时:关闭熔断器,恢复正常请求
4. 缓存击穿防护流程
- 请求到达:客户端请求到达网关
- 查询缓存:查询缓存是否有数据
- 缓存命中:检查是否需要刷新
- 需要刷新:异步刷新缓存
- 返回数据:返回缓存的数据
- 缓存未命中:尝试获取互斥锁
- 获取锁成功:查询数据库,缓存数据
- 获取锁失败:等待其他线程完成
技术要点
1. 缓存键生成
- 路径 + 参数:使用请求路径和参数生成缓存键
- 用户信息:可选添加用户信息到缓存键
- MD5 哈希:使用 MD5 哈希生成固定长度的键
- 键前缀:使用键前缀区分不同类型的缓存
2. 缓存类型选择
- 本地缓存:适用于单机部署,速度快
- 远程缓存:适用于分布式部署,容量大
- 双层缓存:兼顾速度和容量,实现复杂
3. 缓存过期策略
- 固定过期:所有缓存使用相同的过期时间
- 随机过期:添加随机过期时间,防止雪崩
- 逻辑过期:逻辑上判断是否过期,异步刷新
4. 缓存更新策略
- 主动更新:数据变更时主动更新缓存
- 被动更新:缓存失效时被动更新
- 定时更新:定时刷新缓存
最佳实践
1. 缓存策略选择
- 热点数据:使用本地缓存或双层缓存
- 普通数据:使用远程缓存
- 临时数据:使用本地缓存,短过期时间
2. 缓存键设计
- 唯一性:确保缓存键的唯一性
- 可读性:缓存键应该具有一定的可读性
- 长度控制:控制缓存键的长度
3. 缓存过期时间
- 热点数据:较长的过期时间
- 普通数据:适中的过期时间
- 临时数据:较短的过期时间
4. 缓存监控
- 命中率监控:监控缓存的命中率
- 大小监控:监控缓存的大小
- 性能监控:监控缓存的性能
常见问题
1. 缓存不一致
问题:缓存和数据库数据不一致
解决方案:
- 使用缓存更新策略,数据变更时主动更新缓存
- 使用较短的过期时间,减少不一致的时间窗口
- 使用分布式锁,保证更新操作的原子性
2. 缓存穿透
问题:查询不存在的数据,绕过缓存直接打到数据库
解决方案:
- 使用布隆过滤器,提前判断数据是否存在
- 缓存空值,避免重复查询
- 参数校验,提前拦截无效请求
3. 缓存雪崩
问题:大量缓存同时失效,瞬间压垮后端服务
解决方案:
- 使用随机过期时间,避免同时失效
- 使用多级缓存,提高可用性
- 使用熔断降级,保护后端服务
4. 缓存击穿
问题:热点数据缓存失效,大量请求同时打到数据库
解决方案:
- 使用互斥锁,只允许一个请求查询数据库
- 使用逻辑过期,异步刷新缓存
- 使用永不过期,手动更新缓存
5. 内存溢出
问题:缓存占用过多内存,导致内存溢出
解决方案:
- 设置合理的最大缓存大小
- 使用 LRU 淘汰策略
- 监控缓存大小,及时清理
性能测试
测试环境
- 服务器:4核8G,100Mbps带宽
- 测试场景:高频查询接口,10000个并发请求
测试结果
| 场景 | 无缓存 | 本地缓存 | Redis缓存 | 双层缓存 |
|---|---|---|---|---|
| 平均响应时间 | 500ms | 50ms | 80ms | 55ms |
| 最大响应时间 | 2000ms | 100ms | 200ms | 120ms |
| P95响应时间 | 1000ms | 80ms | 150ms | 90ms |
| 吞吐量 | 200 req/s | 2000 req/s | 1250 req/s | 1800 req/s |
| 缓存命中率 | 0% | 95% | 90% | 93% |
| 后端请求量 | 100% | 5% | 10% | 7% |
测试结论
- 响应提速:使用缓存后,响应时间降低 10 倍
- 吞吐量提升:吞吐量提升 10 倍
- 后端压力减少:后端请求量减少 90% 以上
- 本地缓存最优:本地缓存性能最好,但不支持分布式
- 双层缓存平衡:双层缓存兼顾性能和分布式支持
互动话题
- 你在实际项目中如何实现响应缓存?有哪些经验分享?
- 对于缓存穿透、缓存雪崩、缓存击穿,你认为哪种防护策略最有效?
- 你使用过哪些缓存框架?有什么推荐?
- 在高并发场景下,如何平衡缓存的一致性和性能?
欢迎在评论区交流讨论!
公众号:服务端技术精选,关注最新技术动态,分享实用技巧。
标题:SpringBoot + 网关响应缓存 + 缓存穿透防护:高频查询接口响应提速 10 倍,不打后端
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/24/1774155928618.html
公众号:服务端技术精选
- 背景:高频查询接口的性能挑战
- 核心概念
- 1. 网关响应缓存
- 2. 缓存穿透防护
- 3. 缓存雪崩防护
- 4. 缓存击穿防护
- 技术实现
- 1. 核心依赖
- 2. 网关配置
- 3. 响应缓存过滤器
- 4. 本地缓存服务
- 5. 远程缓存服务
- 6. 双层缓存服务
- 7. 缓存穿透防护服务
- 8. 缓存雪崩防护服务
- 9. 缓存击穿防护服务
- 10. 缓存监控服务
- 11. 网关控制器
- 核心流程
- 1. 响应缓存流程
- 2. 缓存穿透防护流程
- 3. 缓存雪崩防护流程
- 4. 缓存击穿防护流程
- 技术要点
- 1. 缓存键生成
- 2. 缓存类型选择
- 3. 缓存过期策略
- 4. 缓存更新策略
- 最佳实践
- 1. 缓存策略选择
- 2. 缓存键设计
- 3. 缓存过期时间
- 4. 缓存监控
- 常见问题
- 1. 缓存不一致
- 2. 缓存穿透
- 3. 缓存雪崩
- 4. 缓存击穿
- 5. 内存溢出
- 性能测试
- 测试环境
- 测试结果
- 测试结论
- 互动话题
评论
0 评论