Appearance
Spring Boot 缓存技术详解 🚀
什么是缓存?为什么需要缓存?
想象一下,你是一家图书馆的管理员。每当有读者询问某本书的位置时,你都需要翻遍整个目录册来查找。这个过程既耗时又重复。聪明的你决定在桌子上放一个小本子,记录最常被询问的书籍位置。下次再有人问同样的问题时,你只需要看看小本子就能立即回答。
这个小本子就是"缓存"的概念!在软件开发中,缓存是一种将计算结果或数据临时存储起来的技术,当下次需要相同数据时,可以直接从缓存中获取,而不需要重新计算或查询数据库。
IMPORTANT
缓存的核心价值在于以空间换时间,通过牺牲一定的存储空间来大幅提升系统性能和响应速度。
Spring Boot 缓存抽象的设计哲学
Spring Framework 提供的缓存抽象有一个非常优雅的设计理念:透明化缓存。
什么是透明化缓存?
透明化意味着:
- 对调用者无感知:客户端代码不需要知道缓存的存在
- 对业务逻辑无侵入:业务代码专注于业务逻辑,缓存逻辑通过注解声明
- 可插拔的实现:可以轻松切换不同的缓存提供商
快速上手:第一个缓存示例
让我们从一个实际的例子开始理解 Spring Boot 缓存:
1. 启用缓存支持
kotlin
@SpringBootApplication
@EnableCaching
class CacheApplication
fun main(args: Array<String>) {
runApplication<CacheApplication>(*args)
}
> `@EnableCaching` 注解是启用 Spring 缓存功能的关键,它会自动配置缓存基础设施。
2. 创建缓存服务
kotlin
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service
import kotlin.random.Random
@Service
class MathService {
/**
* 计算圆周率的指定位数
* 这是一个计算密集型操作,非常适合缓存
*/
@Cacheable("piDecimals")
fun computePiDecimal(precision: Int): Double {
println("正在计算圆周率,精度: $precision")
// 模拟复杂计算过程
Thread.sleep(2000) // 模拟2秒的计算时间
// 简化的圆周率计算(实际应用中会更复杂)
return Math.PI * Random.nextDouble(0.9, 1.1)
}
}
3. 测试缓存效果
kotlin
@RestController
class CacheController(
private val mathService: MathService
) {
@GetMapping("/pi/{precision}")
fun getPi(@PathVariable precision: Int): Map<String, Any> {
val startTime = System.currentTimeMillis()
val result = mathService.computePiDecimal(precision)
val endTime = System.currentTimeMillis()
val executionTime = endTime - startTime
return mapOf(
"precision" to precision,
"pi" to result,
"executionTime" to "${executionTime}ms"
)
}
}
4. 观察缓存效果
bash
curl http://localhost:8080/pi/10
# 响应:
{
"precision": 10,
"pi": 3.141592653589793,
"executionTime": "2003ms" // 需要2秒计算时间
}
# 控制台输出:
正在计算圆周率,精度: 10
bash
curl http://localhost:8080/pi/10
# 响应:
{
"precision": 10,
"pi": 3.141592653589793, // 相同的结果
"executionTime": "5ms" // 几乎瞬间返回!
}
# 控制台输出:
(没有输出,因为方法没有被执行)
TIP
从上面的例子可以看出,第二次请求相同参数时,响应时间从 2 秒降低到 5 毫秒,性能提升了 400 倍!
缓存注解详解
@Cacheable:缓存方法结果
@Cacheable
是最常用的缓存注解,它会在方法执行前检查缓存,如果存在则直接返回缓存结果。
kotlin
@Service
class UserService {
@Cacheable(
value = ["users"], // 缓存名称
key = "#id", // 缓存键(支持SpEL表达式)
condition = "#id > 0", // 缓存条件
unless = "#result == null" // 排除条件
)
fun getUserById(id: Long): User? {
println("从数据库查询用户: $id")
// 模拟数据库查询
return if (id > 0) {
User(id, "User-$id", "user$id@example.com")
} else {
null
}
}
}
@CacheEvict:清除缓存
kotlin
@Service
class UserService {
@CacheEvict(
value = ["users"],
key = "#user.id"
)
fun updateUser(user: User): User {
println("更新用户并清除缓存: ${user.id}")
// 更新数据库
return user
}
@CacheEvict(
value = ["users"],
allEntries = true // 清除所有缓存项
)
fun clearAllUsers() {
println("清除所有用户缓存")
}
}
@CachePut:更新缓存
kotlin
@Service
class UserService {
@CachePut(
value = ["users"],
key = "#user.id"
)
fun saveUser(user: User): User {
println("保存用户并更新缓存: ${user.id}")
// 保存到数据库
return user
}
}
@Caching:组合缓存操作
kotlin
@Service
class UserService {
@Caching(
evict = [
CacheEvict(value = ["users"], key = "#user.id"),
CacheEvict(value = ["userProfiles"], key = "#user.id")
],
put = [
CachePut(value = ["users"], key = "#user.id")
]
)
fun updateUserProfile(user: User): User {
// 复杂的更新操作
return user
}
}
Spring Boot 支持的缓存提供商
Spring Boot 按照以下优先级自动检测和配置缓存提供商:
1. Simple Provider(默认)
如果没有添加任何缓存库,Spring Boot 会使用基于 ConcurrentHashMap
的简单实现:
kotlin
// 无需额外配置,Spring Boot 自动使用 Simple Provider
@Service
class SimpleService {
@Cacheable("simpleCache")
fun getData(key: String): String {
Thread.sleep(1000) // 模拟耗时操作
return "Data for $key"
}
}
WARNING
Simple Provider 仅适用于开发和测试环境,生产环境建议使用专业的缓存解决方案。
2. Redis Provider
Redis 是最受欢迎的分布式缓存解决方案:
添加依赖
kotlin
// build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
}
配置 Redis
yaml
spring:
redis:
host: localhost
port: 6379
password: your-password
database: 0
cache:
type: redis
cache-names: users,products,orders
redis:
time-to-live: 10m # 缓存过期时间
key-prefix: myapp # 键前缀
properties
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=your-password
spring.cache.type=redis
spring.cache.cache-names=users,products,orders
spring.cache.redis.time-to-live=10m
自定义 Redis 缓存配置
kotlin
@Configuration
class RedisCacheConfig {
@Bean
fun redisCacheManagerBuilderCustomizer(): RedisCacheManagerBuilderCustomizer {
return RedisCacheManagerBuilderCustomizer { builder ->
builder
// 为不同缓存设置不同的过期时间
.withCacheConfiguration(
"users",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer()))
)
.withCacheConfiguration(
"products",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
)
}
}
}
3. Caffeine Provider
Caffeine 是一个高性能的本地缓存库:
添加依赖
kotlin
dependencies {
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("com.github.ben-manes.caffeine:caffeine")
}
配置 Caffeine
yaml
spring:
cache:
type: caffeine
cache-names: users,products
caffeine:
spec: maximumSize=500,expireAfterAccess=600s
自定义 Caffeine 配置
kotlin
@Configuration
class CaffeineCacheConfig {
@Bean
fun caffeineConfig(): Caffeine<Any, Any> {
return Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats() // 启用统计信息
}
}
实际业务场景应用
场景 1:用户信息缓存
kotlin
@Service
class UserService(
private val userRepository: UserRepository
) {
@Cacheable(
value = ["user-info"],
key = "#userId",
unless = "#result == null"
)
fun getUserInfo(userId: Long): UserInfo? {
log.info("从数据库查询用户信息: $userId")
return userRepository.findById(userId)
?.let { UserInfo(it.id, it.name, it.email, it.avatar) }
}
@CacheEvict(value = ["user-info"], key = "#userId")
fun updateUserInfo(userId: Long, userInfo: UserInfo): UserInfo {
log.info("更新用户信息并清除缓存: $userId")
val user = userRepository.findById(userId)
?: throw UserNotFoundException("用户不存在: $userId")
user.apply {
name = userInfo.name
email = userInfo.email
avatar = userInfo.avatar
}
return userRepository.save(user).let {
UserInfo(it.id, it.name, it.email, it.avatar)
}
}
}
场景 2:商品分类缓存
kotlin
@Service
class CategoryService(
private val categoryRepository: CategoryRepository
) {
@Cacheable(
value = ["categories"],
key = "'all-categories'",
condition = "true"
)
fun getAllCategories(): List<Category> {
log.info("从数据库加载所有商品分类")
return categoryRepository.findAllByOrderBySort()
}
@Cacheable(
value = ["category-tree"],
key = "'category-tree'",
condition = "true"
)
fun getCategoryTree(): List<CategoryNode> {
log.info("构建分类树")
val allCategories = getAllCategories()
return buildCategoryTree(allCategories)
}
@CacheEvict(
value = ["categories", "category-tree"],
allEntries = true
)
fun refreshCategories() {
log.info("刷新分类缓存")
}
private fun buildCategoryTree(categories: List<Category>): List<CategoryNode> {
// 构建分类树的复杂逻辑
// ...
return emptyList()
}
}
场景 3:配置信息缓存
kotlin
@Service
class ConfigService(
private val configRepository: ConfigRepository
) {
@Cacheable(
value = ["system-config"],
key = "#configKey",
condition = "#configKey != null && #configKey.length() > 0"
)
fun getConfig(configKey: String): String? {
log.info("从数据库查询配置: $configKey")
return configRepository.findByKey(configKey)?.value
}
@Cacheable(
value = ["system-config"],
key = "#configKey"
)
fun getConfigWithDefault(configKey: String, defaultValue: String): String {
return getConfig(configKey) ?: defaultValue
}
@CachePut(
value = ["system-config"],
key = "#configKey"
)
fun updateConfig(configKey: String, configValue: String): String {
log.info("更新配置: $configKey = $configValue")
val config = configRepository.findByKey(configKey)
?: Config(key = configKey, value = configValue)
config.value = configValue
configRepository.save(config)
return configValue
}
}
缓存管理器自定义
多缓存管理器配置
kotlin
@Configuration
class CacheConfig {
@Bean
@Primary
fun primaryCacheManager(): CacheManager {
// Redis 作为主要缓存管理器
return RedisCacheManager.builder(redisConnectionFactory())
.cacheDefaults(
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
)
.build()
}
@Bean("localCacheManager")
fun localCacheManager(): CacheManager {
// Caffeine 作为本地缓存管理器
return CaffeineCacheManager().apply {
setCaffeine(
Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
)
}
}
}
缓存管理器定制器
kotlin
@Configuration
class CacheCustomizerConfig {
@Bean
fun cacheManagerCustomizer(): CacheManagerCustomizer<ConcurrentMapCacheManager> {
return CacheManagerCustomizer { cacheManager ->
cacheManager.isAllowNullValues = false
cacheManager.isStoreByValue = true
}
}
@Bean
fun redisCacheManagerCustomizer(): RedisCacheManagerBuilderCustomizer {
return RedisCacheManagerBuilderCustomizer { builder ->
builder
.withCacheConfiguration(
"short-lived",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
)
.withCacheConfiguration(
"long-lived",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(24))
)
}
}
}
缓存最佳实践
1. 缓存键设计原则
kotlin
@Service
class BestPracticeService {
// ✅ 良好的缓存键设计
@Cacheable(
value = ["user-orders"],
key = "'user:' + #userId + ':orders:' + #status + ':page:' + #page"
)
fun getUserOrders(userId: Long, status: String, page: Int): List<Order> {
// 查询逻辑
return emptyList()
}
// ❌ 避免的缓存键设计
@Cacheable(
value = ["orders"],
key = "#userId" // 键太简单,可能冲突
)
fun getBadUserOrders(userId: Long, status: String): List<Order> {
return emptyList()
}
}
2. 缓存条件控制
kotlin
@Service
class ConditionalCacheService {
@Cacheable(
value = ["expensive-data"],
key = "#id",
condition = "#id > 0 && #id < 10000", // 只缓存特定范围的数据
unless = "#result == null || #result.size() == 0" // 不缓存空结果
)
fun getExpensiveData(id: Long): List<DataItem> {
// 昂贵的计算或查询
return emptyList()
}
}
3. 缓存预热
kotlin
@Component
class CacheWarmUpService(
private val userService: UserService,
private val categoryService: CategoryService
) {
@EventListener(ApplicationReadyEvent::class)
fun warmUpCache() {
log.info("开始缓存预热...")
// 预热用户缓存
val popularUserIds = listOf(1L, 2L, 3L, 4L, 5L)
popularUserIds.forEach { userId ->
try {
userService.getUserInfo(userId)
} catch (e: Exception) {
log.warn("预热用户缓存失败: $userId", e)
}
}
// 预热分类缓存
try {
categoryService.getAllCategories()
categoryService.getCategoryTree()
} catch (e: Exception) {
log.warn("预热分类缓存失败", e)
}
log.info("缓存预热完成")
}
}
4. 缓存监控
kotlin
@Component
class CacheMetricsService {
@EventListener
fun handleCacheGetEvent(event: CacheGetEvent) {
// 记录缓存命中情况
log.debug("缓存查询: cache={}, key={}, hit={}",
event.cacheName, event.key, event.result != null)
}
@EventListener
fun handleCachePutEvent(event: CachePutEvent) {
// 记录缓存写入情况
log.debug("缓存写入: cache={}, key={}",
event.cacheName, event.key)
}
}
常见问题与解决方案
问题 1:缓存穿透
问题描述:大量请求查询不存在的数据,导致缓存无效,请求直接打到数据库。
解决方案:
kotlin
@Service
class AntiPenetrationService {
@Cacheable(
value = ["users"],
key = "#id",
unless = "false" // 即使结果为null也缓存
)
fun getUserById(id: Long): User? {
val user = userRepository.findById(id)
// 对于不存在的用户,返回一个特殊的空对象
return user ?: User.EMPTY_USER
}
}
// 定义空对象
data class User(
val id: Long,
val name: String,
val email: String
) {
companion object {
val EMPTY_USER = User(-1, "", "")
}
fun isEmpty(): Boolean = this === EMPTY_USER
}
问题 2:缓存雪崩
问题描述:大量缓存同时过期,导致请求全部打到数据库。
解决方案:
kotlin
@Configuration
class AntiAvalancheConfig {
@Bean
fun redisCacheManagerBuilderCustomizer(): RedisCacheManagerBuilderCustomizer {
return RedisCacheManagerBuilderCustomizer { builder ->
builder.withCacheConfiguration(
"users",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30 + Random.nextInt(10))) // 随机过期时间
)
}
}
}
问题 3:缓存击穿
问题描述:热点数据过期时,大量并发请求同时查询数据库。
解决方案:
kotlin
@Service
class AntiBreakdownService {
private val lockMap = ConcurrentHashMap<String, ReentrantLock>()
fun getHotData(key: String): String? {
// 先尝试从缓存获取
var result = cacheManager.getCache("hot-data")?.get(key)?.get() as String?
if (result == null) {
val lock = lockMap.computeIfAbsent(key) { ReentrantLock() }
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 双重检查
result = cacheManager.getCache("hot-data")?.get(key)?.get() as String?
if (result == null) {
// 从数据库查询
result = queryFromDatabase(key)
// 放入缓存
cacheManager.getCache("hot-data")?.put(key, result)
}
} finally {
lock.unlock()
lockMap.remove(key)
}
}
}
return result
}
private fun queryFromDatabase(key: String): String {
// 模拟数据库查询
Thread.sleep(100)
return "Data for $key"
}
}
总结
Spring Boot 的缓存抽象为我们提供了一个优雅、简洁的缓存解决方案。通过本文的学习,你应该掌握了:
✅ 核心概念:理解缓存的作用和 Spring Boot 缓存抽象的设计哲学
✅ 基础使用:掌握 @Cacheable
、@CacheEvict
、@CachePut
等注解的使用
✅ 提供商选择:了解不同缓存提供商的特点和适用场景
✅ 实践应用:学会在实际业务场景中合理使用缓存
✅ 最佳实践:掌握缓存设计的最佳实践和常见问题的解决方案
TIP
缓存是一把双刃剑,合理使用能大幅提升系统性能,但不当使用也可能带来数据一致性问题。在实际应用中,要根据业务特点选择合适的缓存策略和过期时间。
记住:缓存不是银弹,但是性能优化的重要手段 🎉