# 在SpringBoot中緩存HTTP請求響應體的方法
## 引言
在現代Web應用開發中,性能優化是一個永恒的話題。HTTP請求響應體的緩存作為提升系統性能的重要手段,能夠顯著減少重復計算、降低數據庫壓力并加快客戶端響應速度。SpringBoot作為Java生態中最流行的微服務框架,提供了豐富的緩存支持。
本文將深入探討在SpringBoot應用中實現HTTP請求響應體緩存的完整方案,涵蓋從基礎概念到高級實現的各個層面,幫助開發者構建高性能的Web服務。
## 一、HTTP緩存基礎概念
### 1.1 什么是HTTP緩存
HTTP緩存是指將請求的響應內容存儲在中間介質(內存、磁盤等)中,當相同請求再次發生時直接返回已存儲的內容,而非重新執行完整處理流程的技術。
```java
// 典型緩存流程示例
if (緩存中存在請求結果) {
return 緩存結果;
} else {
執行業務邏輯;
存儲結果到緩存;
return 結果;
}
緩存類型 | 存儲位置 | 速度 | 容量限制 | 典型應用場景 |
---|---|---|---|---|
瀏覽器緩存 | 客戶端 | 最快 | 小 | 靜態資源緩存 |
CDN緩存 | 邊緣節點 | 快 | 大 | 全局內容分發 |
反向代理緩存 | 服務端前置 | 較快 | 較大 | 全頁緩存 |
應用層緩存 | 應用內存 | 極快 | 較小 | 動態數據緩存 |
分布式緩存 | 獨立服務 | 快 | 大 | 共享數據緩存 |
Spring框架提供了統一的緩存抽象層,通過org.springframework.cache.Cache
和org.springframework.cache.CacheManager
接口支持多種緩存實現。
核心注解:
- @Cacheable
:標記可緩存方法
- @CacheEvict
:清除緩存項
- @CachePut
:更新緩存而不干擾方法執行
- @Caching
:組合多個緩存操作
- @CacheConfig
:類級別共享緩存配置
SpringBoot支持多種緩存實現,通過簡單配置即可切換:
Caffeine(推薦):
@Bean
public CaffeineCacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000));
return cacheManager;
}
EhCache:
<!-- ehcache.xml -->
<cache name="responseCache"
maxEntriesLocalHeap="1000"
timeToLiveSeconds="600"/>
Redis(分布式場景):
spring:
cache:
type: redis
redis:
time-to-live: 600000
key-prefix: "CACHE_"
use-key-prefix: true
推薦配置組合:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager() {
@Override
protected Cache createConcurrentMapCache(String name) {
return new ConcurrentMapCache(name,
Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.maximumSize(1000)
.build().asMap(),
false);
}
};
return cacheManager;
}
@Bean
public KeyGenerator customKeyGenerator() {
return (target, method, params) -> {
StringBuilder key = new StringBuilder();
key.append(target.getClass().getSimpleName());
key.append(".");
key.append(method.getName());
for (Object param : params) {
if (param != null) {
key.append("_");
if (param instanceof HttpServletRequest) {
key.append("req_")
.append(((HttpServletRequest) param).getRequestURI());
} else {
key.append(param.toString());
}
}
}
return key.toString();
};
}
}
最直接的實現方式是在Controller方法上添加緩存注解:
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping("/{id}")
@Cacheable(value = "productResponses", key = "#id")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
Product product = productService.findById(id);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.MINUTES))
.eTag(product.getVersion().toString())
.body(product);
}
}
對于統一格式的響應,可以實現ResponseBodyAdvice
進行全局處理:
@ControllerAdvice
public class CachingResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private CacheManager cacheManager;
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.hasMethodAnnotation(Cacheable.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
Cacheable cacheable = returnType.getMethodAnnotation(Cacheable.class);
if (cacheable != null) {
String cacheName = cacheable.value()[0];
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
String key = generateKey(request, returnType);
cache.put(key, body);
// 設置HTTP緩存頭
if (response instanceof ServletServerHttpResponse) {
HttpServletResponse servletResponse =
((ServletServerHttpResponse) response).getServletResponse();
servletResponse.setHeader("Cache-Control", "max-age=1800");
}
}
}
return body;
}
private String generateKey(ServerHttpRequest request, MethodParameter returnType) {
// 實現自定義key生成邏輯
}
}
對于更底層的控制,可以創建緩存Filter:
public class CachingFilter extends OncePerRequestFilter {
@Autowired
private CacheManager cacheManager;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
String cacheKey = generateCacheKey(request);
Cache cache = cacheManager.getCache("httpResponses");
if (cache != null) {
CachedResponse cachedResponse = cache.get(cacheKey, CachedResponse.class);
if (cachedResponse != null) {
writeCachedResponse(cachedResponse, response);
return;
}
}
ContentCachingResponseWrapper responseWrapper =
new ContentCachingResponseWrapper(response);
filterChain.doFilter(request, responseWrapper);
if (cache != null && response.getStatus() == 200) {
CachedResponse responseToCache = new CachedResponse(
responseWrapper.getContentAsByteArray(),
response.getContentType(),
response.getHeader("ETag"));
cache.put(cacheKey, responseToCache);
}
responseWrapper.copyBodyToResponse();
}
// 輔助方法實現...
}
在微服務架構中,通常需要Redis等分布式緩存:
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
return builder -> builder
.withCacheConfiguration("productResponses",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeValuesWith(SerializationPair.fromSerializer(
new Jackson2JsonRedisSerializer<>(Object.class))));
}
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
基于TTL的自動失效:
@Cacheable(value = "responses", key = "#root.methodName",
unless = "#result == null")
@CacheConfig(cacheNames = "responses")
手動清除策略: “`java @CacheEvict(value = “responses”, allEntries = true) public void clearAllResponses() {}
@CacheEvict(value = “responses”, key = “#id”) public void evictById(Long id) {}
3. **條件緩存**:
```java
@Cacheable(condition = "#request.getHeader('no-cache') == null")
良好的緩存鍵設計應考慮: - 包含所有影響響應的參數 - 排除無關參數(如時間戳) - 保持合理長度
示例實現:
public class CacheKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder key = new StringBuilder();
key.append(target.getClass().getSimpleName());
key.append("_");
key.append(method.getName());
if (params.length > 0) {
key.append("_");
for (Object param : params) {
if (param instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) param;
key.append(request.getRequestURI());
Enumeration<String> paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
String name = paramNames.nextElement();
if (!name.equals("_")) { // 排除隨機參數
key.append("_").append(name)
.append("=").append(request.getParameter(name));
}
}
} else if (param != null) {
key.append("_").append(param.toString());
}
}
}
return DigestUtils.md5DigestAsHex(key.toString().getBytes());
}
}
// 配置類 @Bean public CacheManager randomTtlCacheManager() { return new CaffeineCacheManager() { @Override protected Cache createCaffeineCache(String name) { return new CaffeineCache(name, Caffeine.newBuilder() .expireAfterWrite(30 + new Random().nextInt(15), TimeUnit.MINUTES) .build()); } }; }
2. **穿透防護**:
```java
@Cacheable(value = "products",
key = "#id",
unless = "#result == null")
public Product getProduct(Long id) {
Product product = productRepository.findById(id);
if (product == null) {
// 緩存空值防止穿透
cacheNullValue(id);
return null;
}
return product;
}
集成Micrometer進行指標收集:
@Configuration
public class CacheMetricsConfig {
@Bean
public CacheMetricsRegistrar cacheMetricsRegistrar(
CacheManager cacheManager, MeterRegistry meterRegistry) {
return new CacheMetricsRegistrar(cacheManager, meterRegistry)
.bindTo(meterRegistry);
}
}
配置緩存操作日志:
@Slf4j
@Aspect
@Component
public class CacheLoggerAspect {
@Pointcut("@annotation(org.springframework.cache.annotation.Cacheable)")
public void cacheableMethods() {}
@Pointcut("@annotation(org.springframework.cache.annotation.CacheEvict)")
public void cacheEvictMethods() {}
@Around("cacheableMethods()")
public Object logCacheable(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.debug("Checking cache for {} with args {}", methodName, args);
Object result = joinPoint.proceed();
if (result != null) {
log.debug("Cache hit for {}", methodName);
} else {
log.debug("Cache miss for {}", methodName);
}
return result;
}
}
啟用緩存JMX管理:
spring.cache.jmx.enabled=true
@RestController
@RequestMapping("/api/v2/products")
@CacheConfig(cacheNames = "productDetail")
public class ProductControllerV2 {
@GetMapping("/{id}")
@Cacheable(key = "T(com.example.util.CacheKeyGenerator).generateDetailKey(#id, #request)")
public ResponseEntity<ProductDetailDTO> getProductDetail(
@PathVariable Long id,
@RequestHeader(value = "Accept-Language", defaultValue = "zh-CN") String language,
WebRequest request) {
if (request.checkNotModified(getLastModified(id))) {
return null;
}
ProductDetailDTO detail = productService.getDetail(id, language);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
.eTag(detail.getVersion())
.lastModified(detail.getUpdateTime().toEpochMilli())
.body(detail);
}
}
@GetMapping
@Cacheable(key = "{#page, #size, #sort, #request.queryString}")
public Page<ProductListItem> listProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id,desc") String sort,
HttpServletRequest request) {
return productService.findProducts(PageRequest.of(page, size, Sort.by(sort)));
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleProductUpdate(ProductUpdatedEvent event) {
cacheEvictor.evictProductCache(event.getProductId());
}
@Service
@RequiredArgsConstructor
public class CacheEvictor {
private final CacheManager cacheManager;
public void evictProductCache(Long productId) {
// 清除詳情緩存
Cache detailCache = cacheManager.getCache("productDetail");
if (detailCache != null) {
detailCache.evict(CacheKeyGenerator.generateDetailKey(productId));
}
// 清除相關列表緩存
Cache listCache = cacheManager.getCache("productList");
if (listCache != null) {
listCache.clear();
}
}
}
問題場景: - 數據庫更新后緩存未及時失效 - 分布式環境下各節點緩存不一致
解決方案:
1. 使用Spring的@TransactionalEventListener
確保事務提交后清除緩存
2. 引入消息隊列廣播緩存失效事件
3. 實現雙刪策略:
@CacheEvict(value = "products", key = "#product.id")
@Transactional
public Product updateProduct(Product product) {
product = productRepository.save(product);
eventPublisher.publishEvent(new ProductUpdatedEvent(product.getId()));
return product;
}
// 事件處理器延遲二次刪除
@EventListener
@Async
public void handleProductUpdated(ProductUpdatedEvent event) {
try {
Thread.sleep(1000);
cacheManager.getCache("products").evict(event.getProductId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
優化方案: 1. 壓縮緩存數據:
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
// 配置壓縮序列化器
template.setValueSerializer(new GzipRedisSerializer(
new GenericJackson2JsonRedisSerializer()));
}
@Cacheable(value = “productData”, key = “#ref”) public Product getProductByRef(String ref) { Long id = extractIdFromRef(ref); return productRepository.findById(id); }
### 7.3 敏感數據緩存
**安全措施**:
1. 排除敏感字段:
```java
@JsonIgnore
private String password;
@Bean
public CacheManager secureCacheManager() {
return new CaffeineCacheManager() {
@Override
protected Cache createCaffeineCache(String name) {
return new SecureCacheWrapper(
super.createCaffeineCache(name),
encryptionService);
}
};
}
對于相關資源可考慮使用HTTP/2推送:
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。