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 中实现缓存的强大工具,通过合理配置和使用,可以显著提升应用性能:
🔑 关键要点
- 正确配置:选择合适的缓存实现和序列化方式
- 合理设计:设计清晰的缓存 key 和适当的 TTL
- 避免陷阱:注意内部调用、序列化等常见问题
- 性能监控:建立完善的缓存监控和报警机制
- 测试验证:充分测试缓存功能的正确性和性能
🚀 最佳实践摘要
- 使用 Redis 作为分布式缓存
- 设置合理的 TTL 防止数据过期
- 建立缓存预热和刷新机制
- 监控缓存命中率和性能指标
- 做好缓存降级和异常处理
通过遵循这些最佳实践,您可以充分发挥 @Cacheable 注解的威力,构建高性能、高可用的缓存系统!
📚 参考资源
- Spring Cache 官方文档
- Spring Boot Cache 文档
- Redis 缓存最佳实践
相关文章