Appearance
Spring WebFlux HTTP 缓存机制详解 🚀
概述
HTTP 缓存是现代 Web 应用性能优化的重要手段之一。在 Spring WebFlux 中,HTTP 缓存围绕着 Cache-Control
响应头和相关的条件请求头(如 Last-Modified
和 ETag
)展开。
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 支持三种条件请求检查方式:
条件请求检查方式
- 仅检查 ETag:
exchange.checkNotModified(eTag)
- 仅检查 Last-Modified:
exchange.checkNotModified(lastModified)
- 同时检查两者:
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 中:
- 使用
CacheControl
类配置缓存策略,支持多种常见场景 - 在 Controller 中通过
ResponseEntity
或ServerWebExchange
实现缓存控制 - 合理选择 ETag 和 Last-Modified,根据业务场景决定缓存时间
- 区分不同 HTTP 方法的缓存行为,避免并发修改问题
IMPORTANT
缓存策略需要在性能提升和数据一致性之间找到平衡点。过长的缓存时间可能导致数据不一致,过短的缓存时间则无法充分发挥缓存的优势。
通过合理使用 HTTP 缓存,你的 Spring WebFlux 应用将能够:
- 🚀 显著提升响应速度
- 💰 减少服务器资源消耗
- 📱 改善用户体验
- 🌐 降低网络带宽使用