Appearance
Spring MVC HTTP 缓存机制详解 🚀
概述
HTTP 缓存是提升 Web 应用性能的重要手段之一。想象一下,如果每次用户访问你的网站时,浏览器都要重新下载所有资源,这会多么浪费带宽和时间!Spring MVC 提供了强大的 HTTP 缓存支持,帮助我们优雅地解决这个问题。
NOTE
HTTP 缓存主要围绕 Cache-Control
响应头和条件请求头(如 Last-Modified
和 ETag
)展开。这些机制可以显著减少网络传输,提升用户体验。
核心概念与工作原理
缓存的本质问题
在没有缓存机制的情况下,我们会遇到以下问题:
- 带宽浪费:重复传输相同的内容
- 服务器压力:每次请求都需要完整处理
- 用户体验差:页面加载缓慢
HTTP 缓存的工作流程
CacheControl:缓存策略的核心
CacheControl
类是 Spring MVC 中配置 Cache-Control
头的主要工具,它采用了面向使用场景的设计理念。
常见缓存策略
kotlin
// 缓存一小时 - "Cache-Control: max-age=3600"
val cacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)
// 禁止缓存 - "Cache-Control: no-store"
val noCache = CacheControl.noStore()
// 复杂缓存策略 - 公共和私有缓存都缓存10天,且不允许转换
// "Cache-Control: max-age=864000, public, no-transform"
val complexCache = CacheControl.maxAge(10, TimeUnit.DAYS)
.noTransform()
.cachePublic()
kotlin
@Service
class CacheConfigService {
// 静态资源:长期缓存
fun getStaticResourceCache(): CacheControl {
return CacheControl.maxAge(365, TimeUnit.DAYS)
.cachePublic() // 允许 CDN 缓存
}
// API 数据:短期缓存
fun getApiDataCache(): CacheControl {
return CacheControl.maxAge(5, TimeUnit.MINUTES)
.cachePrivate() // 仅浏览器缓存
}
// 敏感数据:禁止缓存
fun getSensitiveDataCache(): CacheControl {
return CacheControl.noStore()
.noCache()
}
}
TIP
CacheControl
还支持简化的 cachePeriod
属性:
-1
:不生成Cache-Control
头0
:禁止缓存(no-store
)n > 0
:缓存 n 秒
Controller 中的缓存实现
基础缓存控制
在 Controller 中,我们可以通过 ResponseEntity
轻松添加缓存控制:
kotlin
@RestController
@RequestMapping("/api/books")
class BookController {
@GetMapping("/{id}")
fun getBook(@PathVariable id: Long): ResponseEntity<Book> {
val book = bookService.findById(id)
val version = book.version // 版本号作为 ETag
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // 设置 ETag 用于条件请求
.lastModified(book.lastModified) // 也可以使用最后修改时间
.body(book)
}
}
高级缓存控制:手动检查条件请求
对于更复杂的场景,我们可以手动处理条件请求:
kotlin
@RestController
class AdvancedCacheController {
@GetMapping("/expensive-data")
fun getExpensiveData(request: WebRequest, model: Model): ResponseEntity<String>? {
// 1. 计算资源的唯一标识(通常基于数据内容或版本)
val dataHash = calculateDataHash()
// 2. 检查客户端缓存是否仍然有效
if (request.checkNotModified(dataHash)) {
// 资源未变化,返回 304 Not Modified
return null // Spring 会自动返回 304 状态码
}
// 3. 资源已变化,执行昂贵的计算
val expensiveResult = performExpensiveCalculation()
return ResponseEntity.ok()
.eTag(dataHash.toString())
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
.body(expensiveResult)
}
private fun calculateDataHash(): Long {
// 基于数据内容计算哈希值
return System.currentTimeMillis() / 1000 // 简化示例
}
private fun performExpensiveCalculation(): String {
// 模拟耗时操作
Thread.sleep(1000)
return "Expensive calculation result"
}
}
不同 HTTP 方法的缓存行为
IMPORTANT
条件请求的行为因 HTTP 方法而异:
- GET/HEAD:返回 304 (NOT_MODIFIED)
- POST/PUT/DELETE:返回 412 (PRECONDITION_FAILED),防止并发修改
kotlin
@RestController
class ResourceController {
@PutMapping("/resource/{id}")
fun updateResource(
@PathVariable id: Long,
@RequestBody updateData: ResourceUpdateDto,
request: WebRequest
): ResponseEntity<Resource> {
val currentResource = resourceService.findById(id)
val currentETag = currentResource.version
// 检查并发修改
if (!request.checkNotModified(currentETag)) {
// 资源已被其他请求修改,返回 412 Precondition Failed
return ResponseEntity.status(HttpStatus.PRECONDITION_FAILED).build()
}
// 执行更新操作
val updatedResource = resourceService.update(id, updateData)
return ResponseEntity.ok()
.eTag(updatedResource.version.toString())
.body(updatedResource)
}
}
静态资源缓存
静态资源(如 CSS、JS、图片)通常变化较少,适合长期缓存:
kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
}
}
TIP
对于静态资源,建议结合版本号或文件指纹来实现缓存更新策略。
ETag Filter:自动化的浅层缓存
Spring 提供了 ShallowEtagHeaderFilter
,可以自动为响应生成 ETag:
kotlin
@Configuration
class FilterConfig {
@Bean
fun shallowEtagHeaderFilter(): ShallowEtagHeaderFilter {
return ShallowEtagHeaderFilter()
}
}
注意
ShallowEtagHeaderFilter
通过响应内容生成 ETag,能节省带宽但不能节省 CPU 时间,因为仍需要完整处理请求。
实际应用场景
场景一:博客文章缓存
kotlin
@RestController
@RequestMapping("/api/articles")
class ArticleController {
@GetMapping("/{id}")
fun getArticle(@PathVariable id: Long): ResponseEntity<Article> {
val article = articleService.findById(id)
return ResponseEntity.ok()
.cacheControl(
CacheControl.maxAge(1, TimeUnit.HOURS)
.cachePrivate() // 个人化内容,仅浏览器缓存
)
.eTag(article.lastModified.toString())
.body(article)
}
}
场景二:用户头像缓存
kotlin
@RestController
@RequestMapping("/api/users")
class UserController {
@GetMapping("/{id}/avatar")
fun getUserAvatar(@PathVariable id: Long): ResponseEntity<ByteArray> {
val user = userService.findById(id)
val avatarData = user.avatarData
return ResponseEntity.ok()
.cacheControl(
CacheControl.maxAge(7, TimeUnit.DAYS)
.cachePublic() // 头像可以被 CDN 缓存
)
.eTag(user.avatarHash)
.contentType(MediaType.IMAGE_JPEG)
.body(avatarData)
}
}
最佳实践总结
缓存策略建议
- 静态资源:长期缓存(1年)+ 版本控制
- API 数据:短期缓存(几分钟到几小时)
- 个人化内容:私有缓存
- 敏感数据:禁止缓存
- 频繁变化的数据:使用 ETag 进行条件请求
CAUTION
缓存策略需要根据具体业务场景调整,过度缓存可能导致数据不一致,缓存不足则影响性能。
通过合理使用 Spring MVC 的 HTTP 缓存机制,我们可以显著提升应用性能,减少服务器负载,为用户提供更好的体验。记住,缓存是一门艺术,需要在性能和数据一致性之间找到平衡点! ✅