【Java】Spring Boot 缓存实战:深入理解 @Cacheable 注解及其参数详解

一、内容简介

在现代 Web 应用开发中,性能优化是一个永恒的话题。Spring Framework 提供了强大的缓存抽象机制,通过简单的注解即可实现方法级别的缓存控制。其中,@Cacheable 是最常用的核心注解之一。本文将深入讲解 @Cacheable 注解的所有参数含义,并结合实际代码和测试案例,帮助开发者掌握其使用技巧与注意事项。

二、Spring Cache 简介

Spring Cache 是 Spring Framework 提供的一套声明式缓存抽象,它允许开发者通过注解方式轻松启用缓存功能,而无需关心底层缓存实现(如Redis、Caffeine、Ehcache 等)。只需在启动类上添加 @EnableCaching,并在方法上使用 @Cacheable@CachePut@CacheEvict 等注解,即可完成缓存逻辑。

本文重点聚焦于 @Cacheable 注解的使用。

三、@Cacheable 注解详解

@Cacheable 用于标记一个方法的结果可以被缓存。当方法被调用时,Spring 会先检查缓存中是否存在对应 key 的结果,若存在则直接返回缓存值,不再执行方法体;若不存在,则执行方法并将结果存入缓存。

核心参数说明
在这里插入图片描述

⚠️ 注意:value cacheNames 功能相同,建议统一使用 cacheNames 以提高可读性。

四、代码案例

1. 项目依赖(Maven)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <!-- 可选:使用 Caffeine 作为本地缓存 -->
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>
</dependencies>

2. 启用缓存

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

3. 服务类(使用 @Cacheable)

@Service
public class UserService {

    private final Map<Long, String> userDb = new HashMap<>();

    public UserService() {
        userDb.put(1L, "Alice");
        userDb.put(2L, "Bob");
        userDb.put(3L, "Charlie");
    }

    /**
     * 基础用法:缓存用户信息
     */
    @Cacheable(cacheNames = "users", key = "#id")
    public String getUserById(Long id) {
        System.out.println("Fetching user from DB: " + id);
        return userDb.get(id);
    }

    /**
     * 使用 condition:仅当 id > 0 时缓存
     */
    @Cacheable(cacheNames = "users", key = "#id", condition = "#id > 0")
    public String getUserWithCondition(Long id) {
        System.out.println("Fetching user with condition: " + id);
        return userDb.get(id);
    }

    /**
     * 使用 unless:当返回 null 时不缓存
     */
    @Cacheable(cacheNames = "users", key = "#id", unless = "#result == null")
    public String getUserUnlessNull(Long id) {
        System.out.println("Fetching user unless null: " + id);
        return userDb.get(id); // 若 id 不存在,返回 null
    }

    /**
     * 同步模式:防止缓存击穿
     */
    @Cacheable(cacheNames = "users", key = "#id", sync = true)
    public String getUserSync(Long id) {
        System.out.println("Fetching user in sync mode: " + id);
        try {
            Thread.sleep(1000); // 模拟耗时操作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return userDb.get(id);
    }
}

五、测试案例

1. 单元测试(JUnit 5)

@SpringBootTest
class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    void testCacheableBasic() {
        // 第一次调用:执行方法并缓存
        String user1 = userService.getUserById(1L);
        assertEquals("Alice", user1);

        // 第二次调用:应从缓存读取,不打印日志
        String user2 = userService.getUserById(1L);
        assertEquals("Alice", user2);
        // 控制台应只打印一次 "Fetching user from DB: 1"
    }

    @Test
    void testCondition() {
        // id=1 > 0,应缓存
        userService.getUserWithCondition(1L);
        userService.getUserWithCondition(1L); // 第二次不应打印

        // id=-1 <= 0,不缓存,每次都会执行
        userService.getUserWithCondition(-1L);
        userService.getUserWithCondition(-1L); // 两次都打印
    }

    @Test
    void testUnless() {
        // id=1 存在,应缓存
        userService.getUserUnlessNull(1L);
        userService.getUserUnlessNull(1L); // 第二次不打印

        // id=99 不存在,返回 null,不缓存
        assertNull(userService.getUserUnlessNull(99L));
        assertNull(userService.getUserUnlessNull(99L)); // 两次都打印日志
    }

    @Test
    void testSyncMode() throws InterruptedException {
        // 并发调用,验证 sync=true 防止多次执行
        ExecutorService executor = Executors.newFixedThreadPool(5);
        CountDownLatch latch = new CountDownLatch(5);

        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                userService.getUserSync(2L);
                latch.countDown();
            });
        }

        latch.await(); // 等待所有线程完成
        executor.shutdown();

        // 控制台应只打印一次 "Fetching user in sync mode: 2"
    }
}

六、注意事项

  • 缓存穿透:查询不存在的数据会导致每次穿透到数据库。可通过 unless = “#result == null” 避免缓存 null 值,或使用布隆过滤器。
  • 缓存雪崩:大量缓存同时失效。建议设置随机过期时间。
  • 缓存击穿:热点 key 失效瞬间高并发访问。使用 sync = true 可缓解(仅限单机)。
  • key 的唯一性:确保 key 表达式能唯一标识方法调用。避免不同参数产生相同 key。
  • 序列化问题:若使用 Redis 等远程缓存,需确保对象可序列化(推荐使用 JSON 或自定义序列化器)。
  • 事务与缓存:缓存操作在事务提交后才生效(若使用 Spring 事务),否则可能缓存脏数据。

七、结语

@Cacheable 是 Spring Cache 中最核心、最实用的注解之一。通过合理配置其参数,可以显著提升系统性能并降低数据库压力。但在实际使用中,也需警惕缓存一致性、穿透、雪崩等问题。建议结合业务场景选择合适的缓存策略与底层实现(如 Caffeine、Redis 等)。

希望本文能帮助你快速掌握 @Cacheable 的使用技巧。欢迎在评论区交流你的缓存实践经验!


📌 作者:保加利亚的风

📅 发布日期:2025年11月26日

🔗 相关标签:#SpringBoot #Cache #@Cacheable #性能优化 #Java

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值