Spring Framework缓存抽象:CacheManager与@Cacheable
【免费下载链接】spring-framework 项目地址: https://gitcode.com/gh_mirrors/spr/spring-framework
在现代应用开发中,频繁的数据查询操作往往会成为系统性能的瓶颈。每次请求都直接访问数据库不仅增加了数据库负担,还延长了响应时间。Spring Framework提供的缓存抽象(Cache Abstraction)通过在内存中存储常用数据,显著减少了对数据库的访问次数,从而提升系统性能。本文将详细介绍Spring缓存抽象的核心组件CacheManager与@Cacheable注解的使用方法,帮助开发者轻松实现高效缓存策略。
缓存抽象核心组件
Spring缓存抽象的核心设计理念是将缓存操作与业务逻辑解耦,通过统一的接口和注解简化缓存管理。其主要组件包括Cache接口、CacheManager接口和一系列缓存注解。
Cache接口
Cache接口定义了缓存操作的基本方法,如获取缓存项、放入缓存项、删除缓存项等。Spring提供了多种Cache接口的实现,以适应不同的缓存需求。常见的实现包括基于内存的ConcurrentMapCache、基于Redis的RedisCache、基于Caffeine的CaffeineCache等。开发者可以根据项目的实际需求选择合适的缓存实现。
CacheManager接口
CacheManager是Spring缓存抽象的核心接口,负责管理Cache实例。它的主要作用是根据缓存名称获取对应的Cache对象,并维护缓存的生命周期。
CacheManager接口的源码位于spring-context/src/main/java/org/springframework/cache/CacheManager.java,其定义如下:
public interface CacheManager {
@Nullable
Cache getCache(String name);
Collection<String> getCacheNames();
}
getCache(String name):根据缓存名称获取对应的Cache实例。如果指定名称的缓存不存在,可能返回null,具体取决于实现类的策略。getCacheNames():返回当前CacheManager管理的所有缓存名称的集合。
Spring提供了多种CacheManager的实现,以支持不同的缓存技术:
| CacheManager实现类 | 说明 |
|---|---|
| ConcurrentMapCacheManager | 使用ConcurrentHashMap作为缓存存储,适用于简单的内存缓存场景 |
| RedisCacheManager | 集成Redis作为缓存存储,适用于分布式缓存场景 |
| CaffeineCacheManager | 集成Caffeine缓存库,提供高性能的内存缓存 |
| EhCacheCacheManager | 集成EhCache缓存库,支持复杂的缓存配置 |
缓存注解
Spring提供了一系列注解来简化缓存操作,常用的包括:
@Cacheable:标记方法的返回结果可以被缓存。当方法被调用时,首先检查缓存中是否存在对应的数据,如果存在则直接返回缓存数据,否则执行方法并将结果存入缓存。@CacheEvict:标记方法执行后需要清除缓存中的某些数据。@CachePut:标记方法执行后将结果存入缓存,无论缓存中是否已存在对应数据。@Caching:组合多个缓存注解,用于复杂的缓存操作场景。@CacheConfig:在类级别统一配置缓存相关属性,如缓存名称、KeyGenerator等。
CacheManager详解
CacheManager作为缓存管理的核心,负责创建、配置和管理Cache实例。不同的CacheManager实现适用于不同的场景,下面详细介绍几种常用的CacheManager。
ConcurrentMapCacheManager
ConcurrentMapCacheManager是Spring提供的默认CacheManager实现,它使用ConcurrentHashMap作为底层存储,适用于单应用、简单的内存缓存场景。
使用ConcurrentMapCacheManager非常简单,只需在Spring配置类中进行如下配置:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
// 设置默认缓存名称
cacheManager.setCacheNames(Arrays.asList("users", "products"));
return cacheManager;
}
}
上述配置创建了一个ConcurrentMapCacheManager实例,并指定了默认的缓存名称为"users"和"products"。当应用启动时,CacheManager会自动创建这些缓存。
RedisCacheManager
在分布式系统中,使用本地内存缓存(如ConcurrentMapCache)无法实现缓存共享,此时RedisCacheManager是更好的选择。RedisCacheManager集成了Redis数据库,将缓存数据存储在Redis中,实现了分布式环境下的缓存共享。
要使用RedisCacheManager,首先需要添加Redis相关依赖。在Maven项目中,可以在pom.xml中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
然后在配置类中配置RedisCacheManager:
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // 设置默认缓存过期时间为10分钟
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 为不同的缓存设置不同的过期时间
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("users", config.entryTtl(Duration.ofHours(1)));
configMap.put("products", config.entryTtl(Duration.ofMinutes(5)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.withInitialCacheConfigurations(configMap)
.build();
}
}
上述配置中,我们通过RedisCacheConfiguration设置了缓存的默认过期时间、键值序列化方式等。同时,为"users"和"products"缓存设置了不同的过期时间,以满足不同业务数据的缓存需求。
@Cacheable注解深度解析
@Cacheable注解是Spring缓存抽象中最常用的注解之一,它可以轻松地将方法的返回结果缓存起来。下面详细介绍@Cacheable注解的属性和使用方法。
@Cacheable注解属性
@Cacheable注解的源码位于spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java,其主要属性如下:
| 属性名 | 说明 |
|---|---|
| value/cacheNames | 指定缓存名称,可以指定多个,数组形式 |
| key | 缓存键的SpEL表达式,用于动态生成缓存键 |
| keyGenerator | 自定义缓存键生成器的bean名称 |
| cacheManager | 指定使用的CacheManager的bean名称 |
| cacheResolver | 指定使用的CacheResolver的bean名称 |
| condition | 缓存条件的SpEL表达式,为true时才缓存 |
| unless | 否决缓存的SpEL表达式,为true时不缓存 |
| sync | 是否同步缓存,默认为false |
基本使用
使用@Cacheable注解非常简单,只需在需要缓存结果的方法上添加该注解,并指定缓存名称:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
System.out.println("查询用户信息,id: " + id);
return userRepository.findById(id).orElse(null);
}
}
上述代码中,@Cacheable(value = "users", key = "#id")表示将getUserById方法的返回结果缓存到名为"users"的缓存中,缓存键为方法参数id的值。当第一次调用该方法时,会执行方法体并将结果存入缓存;后续调用时,如果缓存中存在对应id的用户信息,则直接返回缓存数据,不再执行方法体。
缓存键生成
缓存键的生成是缓存操作的关键,@Cacheable注解提供了多种方式来生成缓存键。
默认键生成器
如果不指定key和keyGenerator属性,Spring会使用默认的键生成器SimpleKeyGenerator。SimpleKeyGenerator的生成规则如下:
- 如果方法没有参数,则使用
SimpleKey.EMPTY作为键。 - 如果方法有一个参数,则使用该参数的值作为键。
- 如果方法有多个参数,则使用包含所有参数值的
SimpleKey对象作为键。
自定义key属性
通过key属性可以使用SpEL表达式动态生成缓存键。例如:
@Cacheable(value = "users", key = "#user.id")
public User getUserByUser(User user) {
return userRepository.findById(user.getId()).orElse(null);
}
@Cacheable(value = "products", key = "'product_' + #name")
public Product getProductByName(String name) {
return productRepository.findByName(name).orElse(null);
}
上述代码中,key = "#user.id"表示使用参数user的id属性作为缓存键;key = "'product_' + #name"表示使用字符串"product_"拼接参数name作为缓存键。
自定义KeyGenerator
如果默认的键生成规则和key属性的SpEL表达式都无法满足需求,可以自定义KeyGenerator。
首先,创建一个实现KeyGenerator接口的类:
@Component
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
// 生成缓存键,例如:类名+方法名+参数值
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName()).append(":");
sb.append(method.getName()).append(":");
for (Object param : params) {
sb.append(param).append(",");
}
return sb.toString();
}
}
然后,在@Cacheable注解中指定keyGenerator属性:
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")
public User getUserByUsername(String username) {
return userRepository.findByUsername(username).orElse(null);
}
缓存条件
@Cacheable注解的condition和unless属性可以用于控制缓存的条件。
condition属性
condition属性用于指定缓存的条件,当SpEL表达式的值为true时才进行缓存。例如:
@Cacheable(value = "users", condition = "#id > 10")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
上述代码中,只有当id大于10时,才会缓存方法的返回结果。
unless属性
unless属性用于指定否决缓存的条件,当SpEL表达式的值为true时不进行缓存。unless表达式可以访问方法的返回结果(使用#result变量)。例如:
@Cacheable(value = "users", unless = "#result == null")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
上述代码中,如果方法返回的User对象为null,则不进行缓存。
同步缓存
sync属性用于设置是否同步缓存,默认为false。当sync = true时,多个线程同时请求同一个缓存键的数据时,只有一个线程会执行方法体,其他线程会等待该线程执行完成并将结果存入缓存后,直接获取缓存数据。这可以有效防止缓存击穿问题。
@Cacheable(value = "users", key = "#id", sync = true)
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
实际应用案例
下面通过一个完整的案例来演示Spring缓存抽象的实际应用。
项目结构
本案例的项目结构如下:
gh_mirrors/spr/spring-framework/
├── spring-context/
│ └── src/
│ └── main/
│ └── java/
│ └── org/
│ └── springframework/
│ └── cache/
│ ├── Cache.java
│ ├── CacheManager.java
│ └── annotation/
│ └── Cacheable.java
├── ...
└── src/
└── main/
└── java/
└── com/
└── example/
├── config/
│ └── CacheConfig.java
├── model/
│ └── User.java
├── repository/
│ └── UserRepository.java
└── service/
└── UserService.java
配置CacheManager
在配置类中配置CacheManager:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
// 使用ConcurrentMapCacheManager作为缓存管理器
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
// 设置缓存名称
cacheManager.setCacheNames(Arrays.asList("users"));
return cacheManager;
}
}
创建实体类和Repository
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
// 省略getter和setter方法
}
public interface UserRepository extends JpaRepository<User, Long> {
}
创建Service层
在Service层的方法上添加@Cacheable注解:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
System.out.println("执行数据库查询,id: " + id);
return userRepository.findById(id).orElse(null);
}
@CacheEvict(value = "users", key = "#id")
public void deleteUserById(Long id) {
System.out.println("删除用户,id: " + id);
userRepository.deleteById(id);
}
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
System.out.println("更新用户,id: " + user.getId());
return userRepository.save(user);
}
}
测试缓存效果
创建测试类,测试缓存效果:
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testCacheable() {
// 第一次调用,执行方法体并缓存结果
User user1 = userService.getUserById(1L);
System.out.println("第一次查询结果:" + user1);
// 第二次调用,直接从缓存获取结果
User user2 = userService.getUserById(1L);
System.out.println("第二次查询结果:" + user2);
// 更新用户,触发@CachePut,更新缓存
user1.setUsername("updated");
User updatedUser = userService.updateUser(user1);
System.out.println("更新后的用户:" + updatedUser);
// 第三次调用,从缓存获取更新后的结果
User user3 = userService.getUserById(1L);
System.out.println("第三次查询结果:" + user3);
// 删除用户,触发@CacheEvict,清除缓存
userService.deleteUserById(1L);
// 第四次调用,缓存已被清除,执行方法体
User user4 = userService.getUserById(1L);
System.out.println("第四次查询结果:" + user4);
}
}
测试输出结果如下:
执行数据库查询,id: 1
第一次查询结果:User{id=1, username='test', email='test@example.com'}
第二次查询结果:User{id=1, username='test', email='test@example.com'}
更新用户,id: 1
更新后的用户:User{id=1, username='updated', email='test@example.com'}
第三次查询结果:User{id=1, username='updated', email='test@example.com'}
删除用户,id: 1
执行数据库查询,id: 1
第四次查询结果:null
从测试结果可以看出,缓存注解成功生效:第一次查询执行了方法体,第二次查询直接从缓存获取结果,更新操作触发了缓存更新,删除操作触发了缓存清除,第四次查询因缓存已被清除而再次执行方法体。
缓存抽象的优势与注意事项
优势
- 简化缓存代码:通过注解方式实现缓存,避免了手动编写缓存逻辑的繁琐工作。
- 统一缓存接口:无论使用何种缓存实现(如Redis、Caffeine等),都通过统一的接口进行操作,降低了更换缓存实现的成本。
- 灵活的缓存策略:支持多种缓存键生成方式、缓存条件控制、过期时间设置等,满足不同的业务需求。
- 与Spring生态无缝集成:可以与Spring Boot、Spring Data等框架无缝集成,进一步简化开发。
注意事项
- 缓存键的唯一性:确保缓存键的唯一性,避免不同方法或参数组合生成相同的缓存键,导致缓存数据混乱。
- 缓存穿透问题:当查询一个不存在的数据时,缓存不会命中,导致每次请求都访问数据库。可以通过缓存空值或使用布隆过滤器来解决。
- 缓存击穿问题:热点数据的缓存过期时,大量并发请求同时访问数据库。可以通过设置
sync = true或使用互斥锁来解决。 - 缓存雪崩问题:大量缓存同时过期,导致大量请求同时访问数据库。可以通过设置不同的缓存过期时间或使用熔断降级机制来解决。
- 缓存与数据库一致性:在更新或删除数据时,需要及时更新或清除对应的缓存,确保缓存数据与数据库数据一致。
总结
Spring Framework的缓存抽象为开发者提供了一种简单、灵活、高效的缓存解决方案。通过CacheManager接口可以方便地管理各种缓存实现,而@Cacheable注解则极大地简化了缓存逻辑的编写。本文详细介绍了CacheManager的核心作用、常用实现以及@Cacheable注解的属性和使用方法,并通过实际案例演示了缓存抽象的应用。在实际项目中,开发者应根据业务需求选择合适的缓存实现和缓存策略,充分发挥缓存的性能优化作用,同时注意避免缓存相关的常见问题。
官方文档:framework-docs/modules/ROOT/pages/core.adoc 缓存抽象源码:spring-context/src/main/java/org/springframework/cache/
【免费下载链接】spring-framework 项目地址: https://gitcode.com/gh_mirrors/spr/spring-framework
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考




