随着用户规模扩大,数据库查询往往成为性能瓶颈,引入缓存是提升体验的有效手段。Spring Boot 提供了便捷的自动化配置,让我们能低侵入地为现有应用增加缓存能力。
快速入门
我们以一个基于 Spring Data JPA 访问 User 数据的案例为基础,为其添加缓存以减少数据库 IO。假设您已熟悉基础的 MySQL 读写操作,我们先回顾一下实体定义。
User 实体
@Entity
@Data
@NoArgsConstructor
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
}
数据访问接口
public interface UserRepository extends JpaRepository<User, Long> {
User findByName(String name);
User findByNameAndAge(String name, Integer age);
@Query("from User u where u.name=:name")
User findUser(@Param("name") String name);
}
为了观察缓存效果,先在 application.properties 中开启 SQL 打印:
spring.jpa.show-sql=true
修改单元测试类,插入一条用户名为 AAA、年龄为 10 的数据,并通过 findByName 函数完成两次查询:
@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter51ApplicationTests {
@Autowired
private UserRepository userRepository;
@Test
public void test() throws Exception {
// 创建 1 条记录
userRepository.save(new User("AAA", 10));
User u1 = userRepository.findByName("AAA");
System.out.println("第一次查询:" + u1.getAge());
User u2 = userRepository.findByName("AAA");
System.out.println("第二次查询:" + u2.getAge());
}
}
在没有加入缓存之前执行测试,日志显示两次查询都执行了 SQL:
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=?
第一次查询:10
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=?
第二次查询:10
引入缓存
首先,在 pom.xml 中添加缓存启动器依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
接着,在主启动类上添加 @EnableCaching 注解开启缓存功能:
@EnableCaching
@SpringBootApplication
public class Chapter51Application {
public static void main(String[] args) {
SpringApplication.run(Chapter51Application.class, args);
}
}
然后在数据访问接口上配置缓存注解,这里使用 @CacheConfig 统一指定缓存名称,并在具体方法上使用 @Cacheable:
@CacheConfig(cacheNames = "users")
public interface UserRepository extends JpaRepository<User, Long> {
@Cacheable
User findByName(String name);
}
再次运行单元测试,你会发现第二次调用 findByName 时不再执行 SELECT 语句,直接减少了数据库读取操作。如果想深入观察缓存存储,可以在测试中注入 CacheManager 查看 users 集合中的对象状态。
核心注解解析
回过头来看这里用到的两个注解分别做了什么:
- @CacheConfig: 主要用于配置该类中会用到的一些共用缓存属性。比如这里的
cacheNames = "users",表示该接口返回的内容将存储在名为 users 的缓存对象中。如果不使用它,也可以直接在@Cacheable中单独配置缓存集名字。 - @Cacheable: 标记在查询方法上。首次调用会执行方法并将结果存入缓存,后续相同参数直接返回缓存值。该注解支持多个参数来控制行为:
value/cacheNames: 指定缓存集合名,两者等价。key: 默认使用所有参数组合作为 Key,也可用 SpEL 表达式自定义,例如#p0代表第一个参数。condition: 满足条件时才缓存,需使用 SpEL 表达式,例如#p0.length() < 3。unless: 方法执行后判断是否缓存,常用于根据返回值决定是否缓存,与condition的区别在于判断时机不同。keyGenerator: 指定自定义 Key 生成器,实现KeyGenerator接口即可,注意它与key互斥。cacheManager/cacheResolver: 当存在多个缓存管理器或解析器时使用。
除了这两个,还有两个核心注解值得注意:
- @CachePut: 每次都会真实调用方法,主要用于数据新增和修改操作,更新缓存内容。
- @CacheEvict: 通常用在删除方法上,用来从缓存中移除相应数据。支持
allEntries清空全部数据,以及beforeInvocation控制是在方法调用前还是调用后移除。
通过合理组合这些注解,就能灵活地管理应用内的缓存策略,显著提升数据访问效率。

