Skip to content

Spring MVC HTTP 缓存机制详解 🚀

概述

HTTP 缓存是提升 Web 应用性能的重要手段之一。想象一下,如果每次用户访问你的网站时,浏览器都要重新下载所有资源,这会多么浪费带宽和时间!Spring MVC 提供了强大的 HTTP 缓存支持,帮助我们优雅地解决这个问题。

NOTE

HTTP 缓存主要围绕 Cache-Control 响应头和条件请求头(如 Last-ModifiedETag)展开。这些机制可以显著减少网络传输,提升用户体验。

核心概念与工作原理

缓存的本质问题

在没有缓存机制的情况下,我们会遇到以下问题:

  • 带宽浪费:重复传输相同的内容
  • 服务器压力:每次请求都需要完整处理
  • 用户体验差:页面加载缓慢

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. 静态资源:长期缓存(1年)+ 版本控制
  2. API 数据:短期缓存(几分钟到几小时)
  3. 个人化内容:私有缓存
  4. 敏感数据:禁止缓存
  5. 频繁变化的数据:使用 ETag 进行条件请求

CAUTION

缓存策略需要根据具体业务场景调整,过度缓存可能导致数据不一致,缓存不足则影响性能。

通过合理使用 Spring MVC 的 HTTP 缓存机制,我们可以显著提升应用性能,减少服务器负载,为用户提供更好的体验。记住,缓存是一门艺术,需要在性能和数据一致性之间找到平衡点! ✅