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

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

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

工作流程

  1. 首次调用:执行方法体,将结果存入缓存
  2. 后续调用:直接返回缓存结果,跳过方法执行

三、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中实现声明式缓存的利器,通过简单的注解配置就能获得显著的性能提升。在实际项目中:

  1. 合理设计缓存键,避免键冲突和内存浪费
  2. 设置合适的过期时间,平衡数据一致性和性能
  3. 处理缓存异常,确保系统稳定性
  4. 配合监控工具,实时观察缓存命中率

掌握@Cacheable的使用,让你的Spring Boot应用性能飞起来!

相关推荐
java_logo2 小时前
TOMCAT Docker 容器化部署指南
java·linux·运维·docker·容器·tomcat
麦克马2 小时前
Netty和Tomcat有什么区别
java·tomcat
战血石LoveYY2 小时前
mybatis踩坑值 <if test=> 不能用大写AND
mybatis
程序员小假2 小时前
SQL 语句左连接右连接内连接如何使用,区别是什么?
java·后端
怕什么真理无穷2 小时前
C++_面试题_21_字符串操作
java·开发语言·c++
Lxinccode2 小时前
docker(25) : 银河麒麟 V10离线安装docker
java·docker·eureka·银河麒麟安装docker·银河麒麟安装compose
遇见火星2 小时前
LINUX的 jq命令行处理json字段指南
java·linux·json·jq
高山上有一只小老虎3 小时前
等差数列前n项的和
java·算法
rockmelodies3 小时前
东方通安装
java