Skip to content

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

缓存是一把双刃剑,合理使用能大幅提升系统性能,但不当使用也可能带来数据一致性问题。在实际应用中,要根据业务特点选择合适的缓存策略和过期时间。

记住:缓存不是银弹,但是性能优化的重要手段 🎉