我Spring Boot缓存实战:@Cacheable注解详解与性能优化

2026-07-06 15:22:04

缓存是提升应用性能最有效的手段之一。

在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);

}

}

选择缓存依赖:

测试环境(内存缓存):

org.springframework.boot

spring-boot-starter-cache

生产环境(Redis):

org.springframework.boot

spring-boot-starter-data-redis

高性能本地缓存:

com.github.ben-manes.caffeine

caffeine

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 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 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 getCacheConfigurations() {

Map 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应用性能飞起来!