Spring Boot 中 @Cacheable 注解详解:提升应用性能的利器

在现代 Web 应用中,缓存是提升系统性能的关键技术之一。

Spring Framework 提供的 @Cacheable 注解让开发者能够以声明式的方式轻松实现缓存功能。

本文将深入探讨 @Cacheable 注解的使用方式、配置策略和最佳实践。


🎯 什么是 @Cacheable 注解?

@Cacheable 是 Spring Cache 抽象层提供的注解,用于标记方法的返回结果需要被缓存。当被标记的方法首次调用时,Spring 会执行方法并将结果存储在缓存中;后续相同参数的调用将直接从缓存返回结果,跳过方法执行。

核心优势

  • 性能提升:减少重复计算和数据库查询
  • 降低延迟:从内存中快速获取数据
  • 减少资源消耗:降低 CPU 和数据库负载
  • 简化实现:声明式编程,代码简洁

🛠️ 基础配置

1. 添加依赖

<!-- Spring Boot Starter Cache -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- Redis 缓存实现 (可选) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. 启用缓存功能

@SpringBootApplication
@EnableCaching  // 启用缓存注解支持
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

3. 缓存配置类

@Configuration
@EnableCaching
public class CacheConfig {

    /**
     * 配置Redis缓存管理器
     */
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 默认缓存配置:30分钟过期,JSON序列化
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues();

        // 不同缓存区域的配置
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();

        // 用户信息缓存 - 1小时过期
        cacheConfigurations.put("users",
            defaultConfig.entryTtl(Duration.ofHours(1)));

        // 产品信息缓存 - 30分钟过期
        cacheConfigurations.put("products",
            defaultConfig.entryTtl(Duration.ofMinutes(30)));

        // 配置数据缓存 - 24小时过期
        cacheConfigurations.put("configs",
            defaultConfig.entryTtl(Duration.ofHours(24)));

        return RedisCacheManager.builder(redisConnectionFactory)
            .cacheDefaults(defaultConfig)
            .withInitialCacheConfigurations(cacheConfigurations)
            .transactionAware()  // 支持事务
            .build();
    }

    /**
     * 本地缓存管理器 (作为备选方案)
     */
    @Bean
    @ConditionalOnMissingBean(name = "cacheManager")
    public CacheManager localCacheManager() {
        return new ConcurrentMapCacheManager("users", "products", "configs");
    }
}

💡 基础使用示例

1. 简单缓存示例

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    /**
     * 根据用户ID缓存用户信息
     */
    @Cacheable(value = "users", key = "#userId")
    public User getUserById(Long userId) {
        log.info("从数据库查询用户: {}", userId);
        return userRepository.findById(userId).orElse(null);
    }

    /**
     * 根据用户名缓存用户信息
     */
    @Cacheable(value = "users", key = "#username")
    public User getUserByUsername(String username) {
        log.info("从数据库查询用户: {}", username);
        return userRepository.findByUsername(username);
    }
}

2. 复合 Key 缓存

@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    /**
     * 使用复合key缓存商品查询结果
     */
    @Cacheable(value = "products",
               key = "#category + ':' + #brand + ':' + #priceRange")
    public List<Product> getProducts(String category, String brand, String priceRange) {
        log.info("查询商品: category={}, brand={}, price={}", category, brand, priceRange);
        return productRepository.findByCategoryAndBrandAndPriceRange(category, brand, priceRange);
    }

    /**
     * 使用SpEL表达式构建复杂key
     */
    @Cacheable(value = "products",
               key = "T(String).format('search:%s_%s_%d', #keyword, #sortBy, #page)")
    public PageResult<Product> searchProducts(String keyword, String sortBy, int page) {
        log.info("搜索商品: keyword={}, sortBy={}, page={}", keyword, sortBy, page);
        return productRepository.search(keyword, sortBy, page);
    }
}

3. 条件缓存

@Service
public class ConfigService {

    @Autowired
    private ConfigRepository configRepository;

    /**
     * 只缓存非空结果
     */
    @Cacheable(value = "configs",
               key = "#configKey",
               unless = "#result == null")
    public ConfigItem getConfig(String configKey) {
        return configRepository.findByKey(configKey);
    }

    /**
     * 根据条件决定是否缓存
     */
    @Cacheable(value = "configs",
               key = "#type + ':' + #env",
               condition = "#type != 'temp'")
    public List<ConfigItem> getConfigsByType(String type, String env) {
        return configRepository.findByTypeAndEnv(type, env);
    }
}

🔧 高级特性

1. 自定义 Key 生成器

@Component
public class CustomKeyGenerator implements KeyGenerator {

    @Override
    public Object generate(Object target, Method method, Object... params) {
        StringBuilder sb = new StringBuilder();
        sb.append(target.getClass().getSimpleName()).append(":");
        sb.append(method.getName()).append(":");

        for (Object param : params) {
            if (param != null) {
                sb.append(param.toString()).append("_");
            }
        }

        return sb.toString();
    }
}

// 使用自定义key生成器
@Cacheable(value = "custom", keyGenerator = "customKeyGenerator")
public String complexMethod(Object param1, Object param2) {
    // 方法实现
    return "result";
}

2. 缓存同步

@Service
public class DataService {

    /**
     * 同步缓存,避免缓存击穿
     */
    @Cacheable(value = "data", key = "#id", sync = true)
    public ExpensiveData getExpensiveData(Long id) {
        log.info("执行耗时操作: {}", id);
        // 模拟耗时操作
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return new ExpensiveData(id, "数据" + id);
    }
}

3. 多级缓存

@Configuration
public class MultiLevelCacheConfig {

    @Bean
    @Primary
    public CacheManager multiLevelCacheManager() {
        CompositeCacheManager cacheManager = new CompositeCacheManager();

        // L1: 本地缓存 (快速访问)
        ConcurrentMapCacheManager localCache = new ConcurrentMapCacheManager();

        // L2: Redis缓存 (分布式共享)
        RedisCacheManager redisCache = RedisCacheManager
            .builder(redisConnectionFactory)
            .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(1)))
            .build();

        cacheManager.setCacheManagers(Arrays.asList(localCache, redisCache));
        cacheManager.setFallbackToNoOpCache(false);

        return cacheManager;
    }
}

🔄 缓存操作注解

1. @CacheEvict - 缓存清除

@Service
public class UserService {

    @Cacheable(value = "users", key = "#userId")
    public User getUserById(Long userId) {
        return userRepository.findById(userId).orElse(null);
    }

    /**
     * 更新用户时清除缓存
     */
    @CacheEvict(value = "users", key = "#user.id")
    public User updateUser(User user) {
        log.info("更新用户并清除缓存: {}", user.getId());
        return userRepository.save(user);
    }

    /**
     * 删除用户时清除缓存
     */
    @CacheEvict(value = "users", key = "#userId")
    public void deleteUser(Long userId) {
        log.info("删除用户并清除缓存: {}", userId);
        userRepository.deleteById(userId);
    }

    /**
     * 清除所有用户缓存
     */
    @CacheEvict(value = "users", allEntries = true)
    public void clearAllUsers() {
        log.info("清除所有用户缓存");
    }
}

2. @CachePut - 缓存更新

@Service
public class UserService {

    /**
     * 强制执行方法并更新缓存
     */
    @CachePut(value = "users", key = "#user.id")
    public User saveUser(User user) {
        log.info("保存用户并更新缓存: {}", user.getId());
        return userRepository.save(user);
    }

    /**
     * 刷新用户信息到缓存
     */
    @CachePut(value = "users", key = "#userId")
    public User refreshUser(Long userId) {
        log.info("刷新用户缓存: {}", userId);
        return userRepository.findById(userId).orElse(null);
    }
}

3. @Caching - 复合缓存操作

@Service
public class UserService {

    /**
     * 同时操作多个缓存
     */
    @Caching(
        cacheable = {
            @Cacheable(value = "users", key = "#user.id"),
            @Cacheable(value = "users", key = "#user.username")
        }
    )
    public User createUser(User user) {
        return userRepository.save(user);
    }

    /**
     * 更新时清除多个缓存
     */
    @Caching(evict = {
        @CacheEvict(value = "users", key = "#user.id"),
        @CacheEvict(value = "users", key = "#user.username"),
        @CacheEvict(value = "user-stats", key = "#user.departmentId")
    })
    public User updateUserInfo(User user) {
        return userRepository.save(user);
    }
}

🎯 最佳实践

1. 缓存 Key 设计原则

@Service
public class BestPracticeService {

    // ✅ 好的做法:简洁明了的key
    @Cacheable(value = "users", key = "#userId")
    public User getUser(Long userId) { /* ... */ }

    // ✅ 好的做法:有意义的组合key
    @Cacheable(value = "orders", key = "#userId + ':' + #status")
    public List<Order> getUserOrders(Long userId, String status) { /* ... */ }

    // ❌ 避免:过于复杂的key表达式
    // @Cacheable(value = "complex", key = "T(some.Util).complexMethod(#param1, #param2, #param3)")

    // ✅ 好的做法:使用常量定义缓存名
    private static final String CACHE_USERS = "users";
    private static final String CACHE_ORDERS = "orders";

    @Cacheable(value = CACHE_USERS, key = "#userId")
    public User getUserWithConstant(Long userId) { /* ... */ }
}

2. 缓存穿透防护

@Service
public class SafeCacheService {

    /**
     * 防止缓存穿透:缓存空结果
     */
    @Cacheable(value = "products",
               key = "#productId",
               unless = "false") // 总是缓存,包括null
    public Product getProductSafely(Long productId) {
        Product product = productRepository.findById(productId).orElse(null);
        // 即使是null也会被缓存,防止重复查询不存在的数据
        return product;
    }

    /**
     * 使用Optional防止null缓存问题
     */
    @Cacheable(value = "products", key = "#productId")
    public Optional<Product> getProductOptional(Long productId) {
        return productRepository.findById(productId);
    }
}

3. 缓存预热策略

@Component
@Slf4j
public class CacheWarmupService {

    @Autowired
    private UserService userService;

    @Autowired
    private ConfigService configService;

    /**
     * 应用启动时预热缓存
     */
    @PostConstruct
    public void warmupCache() {
        log.info("开始缓存预热...");

        // 预热热点用户数据
        List<Long> hotUserIds = Arrays.asList(1L, 2L, 3L, 100L, 1001L);
        hotUserIds.forEach(userId -> {
            try {
                userService.getUserById(userId);
            } catch (Exception e) {
                log.warn("预热用户缓存失败: {}", userId, e);
            }
        });

        // 预热系统配置
        List<String> configKeys = Arrays.asList("system.name", "system.version", "feature.flags");
        configKeys.forEach(key -> {
            try {
                configService.getConfig(key);
            } catch (Exception e) {
                log.warn("预热配置缓存失败: {}", key, e);
            }
        });

        log.info("缓存预热完成");
    }

    /**
     * 定时刷新缓存
     */
    @Scheduled(fixedRate = 3600000) // 每小时执行一次
    public void refreshCache() {
        log.info("开始定时刷新缓存...");
        // 刷新逻辑
    }
}

⚠️ 常见陷阱与注意事项

1. 内部方法调用问题

@Service
public class InternalCallService {

    // ❌ 错误:内部调用不会触发缓存
    public String getDataFromInternal() {
        return getCachedData("internal"); // 不会缓存!
    }

    @Cacheable(value = "data", key = "#key")
    public String getCachedData(String key) {
        return "data for " + key;
    }

    // ✅ 正确:通过自注入解决内部调用问题
    @Autowired
    private InternalCallService self;

    public String getDataCorrectly() {
        return self.getCachedData("internal"); // 会正确缓存
    }
}

2. 缓存雪崩防护

@Service
public class AvalancheProtectionService {

    /**
     * 使用随机TTL防止缓存雪崩
     */
    @Cacheable(value = "random-ttl", key = "#key")
    public String getDataWithRandomTTL(String key) {
        // 在业务层面可以通过随机延迟、限流等方式防护
        return "data for " + key;
    }
}

// 配置中使用随机TTL
@Bean
public CacheManager avalancheProtectionCacheManager() {
    return RedisCacheManager.builder(redisConnectionFactory)
        .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30 + new Random().nextInt(30)))) // 30-60分钟随机TTL
        .build();
}

3. 序列化问题处理

// 确保缓存对象可序列化
public class CacheableUser implements Serializable {
    private static final long serialVersionUID = 1L;

    private Long id;
    private String username;
    // 避免缓存不可序列化的字段
    private transient Connection connection; // 不会被序列化

    // getters and setters
}

🚀 性能优化建议

1. 缓存配置优化

# application.yml
spring:
  redis:
    # 连接池配置
    lettuce:
      pool:
        max-active: 100
        max-idle: 50
        min-idle: 10
    # 超时配置
    timeout: 2000ms
    connect-timeout: 2000ms

  cache:
    # 缓存相关配置
    redis:
      time-to-live: 1800000  # 30分钟默认TTL
      cache-null-values: false # 不缓存null值
    cache-names: users,products,orders,configs # 预定义缓存名

2. 监控和报警

@Component
public class CacheHealthIndicator implements HealthIndicator {

    @Autowired
    private CacheManager cacheManager;

    @Override
    public Health health() {
        try {
            // 检查缓存是否正常工作
            Cache testCache = cacheManager.getCache("health-check");
            if (testCache != null) {
                testCache.put("test", "value");
                String value = testCache.get("test", String.class);

                if ("value".equals(value)) {
                    return Health.up()
                        .withDetail("cache", "正常工作")
                        .build();
                }
            }

            return Health.down()
                .withDetail("cache", "缓存测试失败")
                .build();

        } catch (Exception e) {
            return Health.down()
                .withDetail("cache", "缓存异常: " + e.getMessage())
                .build();
        }
    }
}

🎉 总结

@Cacheable 注解是 Spring Boot 中实现缓存的强大工具,通过合理配置和使用,可以显著提升应用性能:

🔑 关键要点

  1. 正确配置:选择合适的缓存实现和序列化方式
  2. 合理设计:设计清晰的缓存 key 和适当的 TTL
  3. 避免陷阱:注意内部调用、序列化等常见问题
  4. 性能监控:建立完善的缓存监控和报警机制
  5. 测试验证:充分测试缓存功能的正确性和性能

🚀 最佳实践摘要

  • 使用 Redis 作为分布式缓存
  • 设置合理的 TTL 防止数据过期
  • 建立缓存预热和刷新机制
  • 监控缓存命中率和性能指标
  • 做好缓存降级和异常处理

通过遵循这些最佳实践,您可以充分发挥 @Cacheable 注解的威力,构建高性能、高可用的缓存系统!


📚 参考资源

  • Spring Cache 官方文档
  • Spring Boot Cache 文档
  • Redis 缓存最佳实践