Skip to content

Spring WebFlux HTTP 缓存机制详解 🚀

概述

HTTP 缓存是现代 Web 应用性能优化的重要手段之一。在 Spring WebFlux 中,HTTP 缓存围绕着 Cache-Control 响应头和相关的条件请求头(如 Last-ModifiedETag)展开。

NOTE

HTTP 缓存不仅能显著提升 Web 应用的性能,还能减少服务器负载和网络带宽消耗。

为什么需要 HTTP 缓存? 🤔

想象一下,如果没有缓存机制:

  • 每次用户访问同一个页面,服务器都要重新生成完整的响应
  • 即使内容没有变化,也要传输完整的数据
  • 服务器资源被大量重复计算消耗
  • 用户体验因为加载时间过长而变差

HTTP 缓存就是为了解决这些痛点而生的!

CacheControl 核心类 📋

Spring WebFlux 提供了 CacheControl 类来简化缓存控制头的配置。它采用了面向使用场景的设计方式,让开发者能够轻松配置常见的缓存策略。

常见缓存策略示例

kotlin
// 缓存1小时 - "Cache-Control: max-age=3600"
val cacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)

// 禁止缓存 - "Cache-Control: no-store"
val noStore = CacheControl.noStore()

// 复杂缓存策略
// "Cache-Control: max-age=864000, public, no-transform"
val customCache = CacheControl
    .maxAge(10, TimeUnit.DAYS)    // 缓存10天
    .noTransform()                // 禁止转换响应
    .cachePublic()               // 允许公共缓存
kotlin
// 用户头像缓存(变化频率低)
val avatarCache = CacheControl.maxAge(7, TimeUnit.DAYS)

// API 数据缓存(需要及时更新)
val apiCache = CacheControl.maxAge(5, TimeUnit.MINUTES)

// 静态资源缓存(几乎不变)
val staticCache = CacheControl
    .maxAge(365, TimeUnit.DAYS)
    .cachePublic()

TIP

选择合适的缓存时间是关键:静态资源可以长时间缓存,而动态数据需要较短的缓存时间。

Controller 中的缓存实现 🎯

在 Controller 中实现 HTTP 缓存有两种主要方式:

方式一:使用 ResponseEntity 设置缓存头

kotlin
@RestController
class BookController {

    @GetMapping("/book/{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)                                        
            .body(book)
    }
}

IMPORTANT

当客户端再次请求时,如果 ETag 匹配,服务器会自动返回 304 状态码,无需传输响应体。

方式二:手动检查条件请求

kotlin
@RestController
class ArticleController {

    @GetMapping("/article/{id}")
    fun getArticle(
        @PathVariable id: Long,
        exchange: ServerWebExchange,
        model: Model
    ): String? {
        
        // 计算当前资源的 ETag(基于内容或版本)
        val currentETag = calculateETag(id) 
        
        // 检查客户端缓存是否仍然有效
        if (exchange.checkNotModified(currentETag)) { 
            return null // 返回 304 Not Modified,停止处理
        }
        
        // 缓存失效,继续处理请求
        val article = articleService.findById(id)
        model.addAttribute("article", article)
        return "article-detail"
    }
    
    private fun calculateETag(id: Long): Long {
        // 实际业务中可能基于:
        // - 数据库记录的更新时间
        // - 内容的哈希值
        // - 版本号等
        return articleService.getLastModifiedTime(id)
    }
}

缓存检查的三种变体

Spring WebFlux 支持三种条件请求检查方式:

条件请求检查方式

  1. 仅检查 ETagexchange.checkNotModified(eTag)
  2. 仅检查 Last-Modifiedexchange.checkNotModified(lastModified)
  3. 同时检查两者exchange.checkNotModified(eTag, lastModified)

不同 HTTP 方法的行为差异

kotlin
@RestController
class ResourceController {

    @GetMapping("/resource/{id}")
    fun getResource(@PathVariable id: Long, exchange: ServerWebExchange): ResponseEntity<Resource> {
        val resource = findResource(id)
        val etag = resource.version
        
        if (exchange.checkNotModified(etag)) {
            // GET/HEAD: 返回 304 Not Modified
            return ResponseEntity.notModified().build()
        }
        
        return ResponseEntity.ok()
            .eTag(etag)
            .body(resource)
    }
    
    @PutMapping("/resource/{id}")
    fun updateResource(
        @PathVariable id: Long,
        @RequestBody updateData: ResourceUpdateRequest,
        exchange: ServerWebExchange
    ): ResponseEntity<Resource> {
        val currentETag = getCurrentResourceETag(id)
        
        if (exchange.checkNotModified(currentETag)) {
            // PUT/POST/DELETE: 返回 412 Precondition Failed
            // 防止并发修改冲突
            return ResponseEntity.status(HttpStatus.PRECONDITION_FAILED).build() 
        }
        
        val updatedResource = resourceService.update(id, updateData)
        return ResponseEntity.ok()
            .eTag(updatedResource.version)
            .body(updatedResource)
    }
}

WARNING

对于修改操作(POST、PUT、DELETE),条件检查失败时应返回 412 状态码,而不是 304,以防止并发修改问题。

实际业务场景示例 💼

场景:博客文章缓存

kotlin
@RestController
class BlogController(
    private val blogService: BlogService
) {

    @GetMapping("/blog/posts/{slug}")
    fun getBlogPost(
        @PathVariable slug: String,
        exchange: ServerWebExchange
    ): ResponseEntity<BlogPost> {
        
        val post = blogService.findBySlug(slug)
        
        // 使用文章的最后修改时间作为 ETag
        val lastModified = post.updatedAt.toEpochMilli()
        
        if (exchange.checkNotModified(lastModified)) {
            return ResponseEntity.notModified().build()
        }
        
        return ResponseEntity.ok()
            .cacheControl(
                CacheControl.maxAge(1, TimeUnit.HOURS)  // 缓存1小时
                    .mustRevalidate()                    // 必须重新验证
            )
            .lastModified(post.updatedAt.toEpochMilli())
            .body(post)
    }
    
    @GetMapping("/blog/posts")
    fun getBlogPosts(
        @RequestParam(defaultValue = "0") page: Int,
        exchange: ServerWebExchange
    ): ResponseEntity<List<BlogPost>> {
        
        // 对于列表数据,可以使用页面内容的哈希作为 ETag
        val postsHash = blogService.getPostsHash(page)
        
        if (exchange.checkNotModified(postsHash)) {
            return ResponseEntity.notModified().build()
        }
        
        val posts = blogService.getPosts(page)
        
        return ResponseEntity.ok()
            .cacheControl(
                CacheControl.maxAge(10, TimeUnit.MINUTES) // 列表缓存时间较短
            )
            .eTag(postsHash)
            .body(posts)
    }
}

场景:用户个人资料缓存

kotlin
@RestController
class UserController(
    private val userService: UserService
) {

    @GetMapping("/users/{userId}/profile")
    fun getUserProfile(
        @PathVariable userId: Long,
        exchange: ServerWebExchange
    ): ResponseEntity<UserProfile> {
        
        val profile = userService.getProfile(userId)
        val profileVersion = profile.version
        
        if (exchange.checkNotModified(profileVersion)) {
            return ResponseEntity.notModified().build()
        }
        
        return ResponseEntity.ok()
            .cacheControl(
                CacheControl.maxAge(30, TimeUnit.MINUTES)
                    .cachePrivate() // 个人信息只能私有缓存
            )
            .eTag(profileVersion)
            .body(profile)
    }
}

静态资源缓存 📁

对于静态资源,Spring WebFlux 提供了专门的配置选项:

kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/static/**")
            .addResourceLocations("classpath:/static/")
            .setCacheControl(
                CacheControl.maxAge(365, TimeUnit.DAYS)
                    .cachePublic()
            )
    }
}

TIP

静态资源通常可以设置很长的缓存时间,因为它们很少变化。如果需要更新,可以通过修改文件名或添加版本号来实现缓存失效。

缓存策略最佳实践 ✅

1. 根据内容特性选择缓存策略

kotlin
// 用户相关的动态内容
val userDataCache = CacheControl
    .maxAge(5, TimeUnit.MINUTES)
    .cachePrivate()
    .mustRevalidate()
kotlin
// 配置信息、分类数据等
val configCache = CacheControl
    .maxAge(1, TimeUnit.HOURS)
    .cachePublic()
kotlin
// 图片、CSS、JS 等静态资源
val staticCache = CacheControl
    .maxAge(30, TimeUnit.DAYS)
    .cachePublic()

2. 合理使用 ETag 和 Last-Modified

kotlin
@Service
class CacheService {

    fun generateETag(content: Any): String {
        // 方式1: 基于内容哈希
        return DigestUtils.md5DigestAsHex(content.toString().toByteArray())
    }
    
    fun generateETagFromVersion(version: Long): String {
        // 方式2: 基于版本号
        return "\"$version\""
    }
    
    fun getLastModified(entity: BaseEntity): Long {
        // 基于实体的更新时间
        return entity.updatedAt.toEpochMilli()
    }
}

3. 处理缓存失效

kotlin
@RestController
class CacheManagementController(
    private val cacheManager: CacheManager
) {

    @PostMapping("/admin/cache/clear/{cacheName}")
    fun clearCache(@PathVariable cacheName: String): ResponseEntity<String> {
        cacheManager.getCache(cacheName)?.clear()
        return ResponseEntity.ok("Cache cleared: $cacheName")
    }
    
    @PostMapping("/admin/cache/clear-all")
    fun clearAllCaches(): ResponseEntity<String> {
        cacheManager.cacheNames.forEach { name ->
            cacheManager.getCache(name)?.clear()
        }
        return ResponseEntity.ok("All caches cleared")
    }
}

总结 📝

HTTP 缓存是 Web 应用性能优化的重要工具。在 Spring WebFlux 中:

  1. 使用 CacheControl配置缓存策略,支持多种常见场景
  2. 在 Controller 中通过 ResponseEntityServerWebExchange 实现缓存控制
  3. 合理选择 ETag 和 Last-Modified,根据业务场景决定缓存时间
  4. 区分不同 HTTP 方法的缓存行为,避免并发修改问题

IMPORTANT

缓存策略需要在性能提升和数据一致性之间找到平衡点。过长的缓存时间可能导致数据不一致,过短的缓存时间则无法充分发挥缓存的优势。

通过合理使用 HTTP 缓存,你的 Spring WebFlux 应用将能够:

  • 🚀 显著提升响应速度
  • 💰 减少服务器资源消耗
  • 📱 改善用户体验
  • 🌐 降低网络带宽使用