缓存是提升应用性能最有效的手段之一。
在Spring Boot项目中,@Cacheable注解为我们提供了一种声明式的缓存解决方案,让我们能够以极简的方式实现高性能的缓存逻辑。
本文将深入探讨@Cacheable的使用方法、工作原理和最佳实践。
一、为什么需要缓存?
在数据驱动的应用中,我们经常遇到这样的场景:
@RestController
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/products/{id}")
public Product getProduct(@PathVariable Long id) {
// 每次请求都查询数据库
// 高并发时数据库压力巨大!
return productService.findById(id);
}
}
对于热门商品,短时间内可能被请求成千上万次。如果每次都要查询数据库,不仅响应慢,数据库也可能被压垮。
缓存的价值:
- 减少数据库查询,提升响应速度(从毫秒级到微秒级)
- 降低数据库负载,提高系统吞吐量
- 提升用户体验,减少等待时间
二、@Cacheable注解的核心作用
@Cacheable就像是应用的"智能备忘录":
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User findById(Long id) {
// 只有第一次调用会执行这里
// 后续相同参数的调用直接返回缓存结果
return userRepository.findById(id).orElse(null);
}
}
工作流程:
- 首次调用:执行方法体,将结果存入缓存
- 后续调用:直接返回缓存结果,跳过方法执行
三、Spring缓存架构解析
Spring的缓存抽象层让我们可以无缝切换不同的缓存实现:
[@Cacheable注解]
→ [Spring Cache Abstraction]
→ [CacheManager]
→ [Redis/Caffeine/Ehcache等]
这种设计让业务代码与具体缓存实现解耦,便于测试和维护。
四、@Cacheable详细使用指南
1. 基础配置
启用缓存:
@SpringBootApplication
@EnableCaching // 关键:启用缓存功能
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
选择缓存依赖:
-
测试环境(内存缓存):
<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> -
高性能本地缓存:
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
2. 核心属性详解
cacheNames/value - 指定缓存空间
@Cacheable("users") // 单缓存空间
@Cacheable({"users", "temp"}) // 多缓存空间
key - 缓存键设计
// SpEL表达式定义key
@Cacheable(value = "users", key = "#id")
public User findById(Long id) { ... }
@Cacheable(value = "orders", key = "#userId + ':' + #orderType")
public List<Order> findOrders(Long userId, String orderType) { ... }
// 调用对象方法
@Cacheable(value = "products", key = "#product.category + ':' + #product.id")
public Product detail(Product product) { ... }
condition - 条件缓存
// 只缓存id大于100的用户
@Cacheable(value = "users", key = "#id", condition = "#id > 100")
public User findUserConditional(Long id) { ... }
// 只缓存管理员用户
@Cacheable(value = "users", condition = "#result != null and #result.role == 'ADMIN'")
public User findAdminUser(Long id) { ... }
unless - 结果过滤
// 结果不为空时才缓存
@Cacheable(value = "users", unless = "#result == null")
public User findUserUnless(Long id) { ... }
// 不缓存异常结果
@Cacheable(value = "data", unless = "#result == null or #result.hasErrors()")
public DataResult computeData() { ... }
3. 完整实战示例
@Service
@CacheConfig(cacheNames = "userService") // 类级别缓存配置
public class UserService {
@Autowired
private UserRepository userRepository;
// 基础缓存
@Cacheable(key = "'user:' + #id")
public User findById(Long id) {
log.info("查询数据库用户: {}", id);
return userRepository.findById(id).orElse(null);
}
// 条件缓存 + 复杂key
@Cacheable(key = "T(org.springframework.util.DigestUtils).md5DigestAsHex(('#page:' + #page + ':size:' + #size + ':condition:' + #condition).getBytes())",
condition = "#page < 5") // 只缓存前5页
public Page<User> findUsers(int page, int size, String condition) {
log.info("分页查询用户: page={}, size={}", page, size);
return userRepository.findByCondition(condition, PageRequest.of(page, size));
}
// 组合条件缓存
@Cacheable(key = "'user_profile:' + #userId",
condition = "#userId != null",
unless = "#result == null or #result.isBanned()")
public UserProfile getUserProfile(Long userId) {
log.info("查询用户详情: {}", userId);
return userRepository.findProfileById(userId);
}
}
五、缓存生命周期管理
完整的缓存方案需要配套注解来管理缓存生命周期:
1. @CacheEvict - 清理缓存
// 更新后清除单个用户缓存
@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
userRepository.save(user);
}
// 方法执行前清理
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
// 清理整个缓存空间
@CacheEvict(value = "users", allEntries = true)
public void reloadAllUsers() {
// 重新加载数据
}
2. @CachePut - 更新缓存
// 总是执行方法体,并更新缓存
@CachePut(value = "users", key = "#user.id")
public User saveUser(User user) {
User saved = userRepository.save(user);
log.info("保存用户并更新缓存: {}", saved.getId());
return saved;
}
3. @Caching - 组合操作
// 同时操作多个缓存
@Caching(
evict = {
@CacheEvict(value = "users", key = "#user.id"),
@CacheEvict(value = "user_list", allEntries = true)
},
put = {
@CachePut(value = "user_stats", key = "#user.department")
}
)
public User updateUserWithStats(User user) {
return userRepository.save(user);
}
六、生产环境最佳实践
1. Redis缓存配置
# application.yml
spring:
cache:
type: redis
redis:
time-to-live: 3600000 # 1小时过期
cache-null-values: false # 不缓存null值
redis:
host: localhost
port: 6379
password:
database: 0
2. 自定义缓存配置
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(getCacheConfigurations())
.build();
}
private Map<String, RedisCacheConfiguration> getCacheConfigurations() {
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
// 用户数据缓存2小时
configMap.put("users", RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(2)));
// 配置数据缓存24小时
configMap.put("configs", RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(24)));
return configMap;
}
}
3. 缓存异常处理
@Service
public class SafeCacheService {
@Cacheable(value = "safeData", key = "#id", unless = "#result == null")
public String getDataSafely(String id) {
try {
return fetchDataFromExternalService(id);
} catch (Exception e) {
log.error("获取数据失败: {}", id, e);
// 异常时不缓存,返回null触发unless条件
return null;
}
}
}
七、常见问题与解决方案
1. 缓存穿透
问题:查询不存在的数据,导致每次都要访问数据库。
解决方案:
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User findById(Long id) {
User user = userRepository.findById(id).orElse(null);
// 对于null值,可以缓存空对象或使用布隆过滤器
return user != null ? user : new NullUser();
}
2. 缓存雪崩
问题:大量缓存同时失效,请求直接打到数据库。
解决方案:
// 设置不同的过期时间
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
return RedisCacheManager.builder(factory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30 + new Random().nextInt(30)))) // 30-60分钟随机过期
.build();
}
}
3. 缓存击穿
问题:热点key失效瞬间,大量请求直接访问数据库。
解决方案:
@Cacheable(value = "hotProducts", key = "#id", sync = true) // 使用sync同步加载
public Product getHotProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
八、性能对比测试
让我们通过实际测试看看缓存的效果:
@SpringBootTest
class CachePerformanceTest {
@Autowired
private UserService userService;
@Test
void testCachePerformance() {
Long userId = 1L;
// 第一次查询(无缓存)
long start1 = System.currentTimeMillis();
userService.findById(userId);
long time1 = System.currentTimeMillis() - start1;
// 第二次查询(有缓存)
long start2 = System.currentTimeMillis();
userService.findById(userId);
long time2 = System.currentTimeMillis() - start2;
System.out.println("首次查询耗时: " + time1 + "ms");
System.out.println("缓存查询耗时: " + time2 + "ms");
System.out.println("性能提升: " + (time1 - time2) + "ms");
}
}
典型结果:
- 数据库查询:50-200ms
- 缓存查询:1-5ms
- 性能提升:20-100倍
总结
@Cacheable注解是Spring Boot中实现声明式缓存的利器,通过简单的注解配置就能获得显著的性能提升。在实际项目中:
- 合理设计缓存键,避免键冲突和内存浪费
- 设置合适的过期时间,平衡数据一致性和性能
- 处理缓存异常,确保系统稳定性
- 配合监控工具,实时观察缓存命中率
掌握@Cacheable的使用,让你的Spring Boot应用性能飞起来!