在微服务,分布式的大环境下,缓存绝对是提升系统性能的关键手段,Spring作为 Java生态中最流行的企业级应用框架,它是如何实现缓存的呢?这篇文章,我们将深入探讨 Spring中 5个核心的缓存注解。
一、什么是缓存?
缓存(Cache)是一种存储机制,旨在临时存储数据副本,以便快速访问。缓存一般位于应用程序与数据源(如数据库)之间,能够显著降低数据访问延迟和减轻数据源的压力。
二、缓存的类型
缓存一般可以分为下面 4种类型:
- 本地缓存:存在于应用程序本地内存中,例如使用ConcurrentHashMap、Guava Cache等。
- 分布式缓存:跨多个应用实例共享的缓存,例如Redis、Memcached、EhCache的分布式配置等。
- 持久化缓存:将缓存数据持久化到磁盘,以应对应用重启后的数据恢复。
- 非持久化缓存:缓存数据存储于内存,应用重启后数据丢失。
三、Spring缓存
Spring 从4.0版本起开始引入了 Cache模块,并提供了一套统一的缓存API,隐藏了底层缓存实现的复杂性。开发者只需通过配置和注解即可实现缓存功能,支持多种缓存实现,如EhCache、Redis、Caffeine等。
Spring缓存模块的核心组件包括:
- CacheManager:管理多个Cache实例,根据需要选择合适的Cache。
- Cache:具体的缓存操作接口,定义了基本的缓存操作方法,如get、put、evict等。
- CacheResolver:根据方法信息动态解析需要使用的Cache。
- KeyGenerator:生成缓存键的策略。
通过合理配置和使用,Spring缓存抽象能够灵活地满足各种应用场景的需求。
四、Spring缓存注解详解
Spring缓存注解主要有以下 5个:
- @Cacheable
- @CachePut
- @CacheEvict
- @Caching
- @CacheConfig
下面我们将逐一对这些注解进行分析。
1. @Cacheable
@Cacheable注解用于方法级别,表示方法执行的结果可以被缓存。当方法被调用时,Spring会先检查缓存中是否存在对应的键值对,如果存在,则直接返回缓存中的结果;如果不存在,则执行方法,并将结果存入缓存。
使用示例:
@Service
publicclass UserService {
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
// 模拟数据库访问
simulateSlowService();
returnnew User(id, "John Doe");
}
private void simulateSlowService() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
thrownew IllegalStateException(e);
}
}
}
在上述示例中,getUserById方法被@Cacheable注解修饰,指定使用users缓存,并以方法参数id作为缓存键。首次调用该方法时,缓存中不存在对应的用户信息,方法会被执行并将结果存入缓存。后续相同的调用将直接从缓存中获取结果,避免了重复的业务逻辑执行。
关键属性:
- value / cacheNames:指定缓存的名称,可以有多个,表示多个缓存同时生效。
- key:指定缓存的键,支持SpEL表达式,默认基于方法参数生成。
- condition:缓存条件,符合条件的情况下才进行缓存。
- unless:否决缓存条件,符合条件的情况下不缓存。
- keyGenerator:自定义键生成策略。
- cacheManager:指定使用的缓存管理器。
- cacheResolver:指定缓存解析器。
2. @CachePut
@CachePut注解同样用于方法级别,但与@Cacheable不同,它总是执行方法,并将结果存入缓存。@CachePut适用于需要更新缓存但不影响方法执行结果的场景。
使用示例:
@Service
public class UserService {
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
// 模拟更新数据库
return user;
}
}
在上述示例中,updateUser方法被@CachePut注解修饰,每次调用该方法时,都会执行方法逻辑(更新操作),并将返回的User对象更新到users缓存中。这样可以确保缓存中的数据与数据库中的数据保持一致。
关键属性:与@Cacheable相同,@CachePut也支持value、cacheNames、key等属性,用于指定缓存名称、键及其他配置。
3. @CacheEvict
@CacheEvict注解用于方法级别,表示在方法执行后,清除指定缓存中的一个或多个条目。它常用于删除操作,以确保缓存中的数据与数据源保持一致。
使用示例:
@Service
public class UserService {
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
// 模拟删除数据库
}
}
在上述示例中,deleteUser方法被@CacheEvict注解修饰,指定从users缓存中移除键为id的条目。这样,在用户被删除后,相应的缓存数据也被清除,防止缓存中的数据不一致。
关键属性:
- value / cacheNames:指定缓存的名称。
- key:指定要清除的缓存键。
- allEntries:指定是否清除缓存中的所有条目,默认为false。
- beforeInvocation:指定清除缓存的时机,默认为方法执行成功后。
- cacheManager:指定使用的缓存管理器。
- cacheResolver:指定缓存解析器。
4. @Caching
@Caching注解用于组合多个缓存注解,使得在一个方法上可以执行多个缓存操作。它适用于需要同时执行多个缓存行为的复杂场景。
使用示例:
@Service
public class UserService {
@Caching(
put = { @CachePut(value = "users", key = "#user.id"),
@CachePut(value = "username", key = "#user.username") },
evict = { @CacheEvict(value = "userCache", allEntries = true) }
)
public User addUser(User user) {
// 模拟添加用户到数据库
return user;
}
}
在上述示例中,addUser方法通过@Caching注解同时执行了两个@CachePut操作,将用户信息存入不同的缓存中,并且执行了一个@CacheEvict操作,清除userCache中的所有条目。
关键属性:
- @Caching主要包含以下属性:
- cacheable:@Cacheable注解数组。
- put:@CachePut注解数组。
- evict:@CacheEvict注解数组。
通过组合不同类型的缓存注解,@Caching提供了更灵活的缓存操作能力。
5. @CacheConfig
@CacheConfig注解用于类级别,为该类中的所有缓存注解提供公共配置。例如,可以指定统一的缓存名称、缓存管理器等,减少重复配置的工作量。
使用示例:
@Service
@CacheConfig(cacheNames = "users", cacheManager = "cacheManager")
publicclass UserService {
@Cacheable(key = "#id")
public User getUserById(Long id) {
// 模拟数据库访问
returnnew User(id, "John Doe");
}
@CachePut(key = "#user.id")
public User updateUser(User user) {
// 模拟更新数据库
return user;
}
@CacheEvict(key = "#id")
public void deleteUser(Long id) {
// 模拟删除数据库
}
}
在上述示例中,@CacheConfig注解指定了默认的缓存名称和缓存管理器,使得类中的所有缓存注解无需重复指定这些属性,只需关注特定的键或其他配置。
关键属性:
- cacheNames / value:指定默认的缓存名称。
- cacheManager:指定默认的缓存管理器。
- cacheResolver:指定默认的缓存解析器。
- keyGenerator:指定默认的键生成策略。
@CacheConfig通过提供类级别的缓存配置,简化了属性的配置和维护,提高了代码的可读性和可维护性。
五、缓存框架
要使 Spring的缓存注解生效,必须配置一个缓存管理器(CacheManager)和相应的缓存提供者。Spring支持多种缓存实现,常见的包括 EhCache、Redis、Caffeine等。下面,我们介绍这 3种常用缓存提供者的配置方法。
1. EhCache
EhCache是一款常用的开源缓存库,支持本地内存和磁盘存储,配置灵活,适用于单机应用。
依赖配置(Maven):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.6</version>
</dependency>
配置示例:
创建一个EhCache配置文件ehcache.xml:
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxEntriesLocalHeap="1000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="false"/>
<cache name="users"
maxEntriesLocalHeap="500"
timeToLiveSeconds="3600"
eternal="false"
overflowToDisk="false"/>
</ehcache>
Spring配置:
@Configuration
@EnableCaching
publicclass CacheConfig {
@Bean
public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
factoryBean.setShared(true);
return factoryBean;
}
@Bean
public CacheManager cacheManager(EhCacheManagerFactoryBean factoryBean) {
returnnew EhCacheCacheManager(factoryBean.getObject());
}
}
2. Redis
Redis是一种高性能的NoSQL缓存数据库,支持分布式部署,适用于大规模应用场景。
依赖配置(Maven):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置示例(application.properties):
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=yourpassword
Spring配置:
Spring Boot会自动配置RedisCacheManager,无需额外配置。如果需要自定义配置,可以如下:
@Configuration
@EnableCaching
publicclass RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 配置默认缓存过期时间等
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(60))
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
3. Caffeine
Caffeine是一个高性能的本地缓存库,具有丰富的缓存策略和高并发性能。
依赖配置(Maven):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.6</version>
</dependency>
Spring配置:
@Configuration
@EnableCaching
publicclass CaffeineCacheConfig {
@Bean
public Caffeine<Object, Object> caffeineConfig() {
return Caffeine.newBuilder()
.expireAfterWrite(60, TimeUnit.MINUTES)
.maximumSize(1000);
}
@Bean
public CacheManager cacheManager(Caffeine<Object, Object> caffeine) {
CaffeineCacheManager manager = new CaffeineCacheManager("users");
manager.setCaffeine(caffeine);
return manager;
}
}
六、案例分析
下面我们通过一个简单的 CRUD应用,演示如何在 Spring Boot项目中集成和使用缓存注解。
1. 项目介绍
构建一个用户管理系统,包含用户的增删改查功能。通过缓存优化其中的读取操作,以提升系统性能。
2. 环境搭建
技术栈:
- Spring Boot:快速构建项目基础。
- Spring Data JPA:数据访问层。
- H2数据库:内存数据库,方便演示。
- Spring Cache:缓存抽象。
- EhCache:作为缓存实现。
依赖配置(Maven):
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Boot Cache Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- EhCache -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.6</version>
</dependency>
<!-- Lombok(可选,用于简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
3. 缓存配置
创建ehcache.xml文件:
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxEntriesLocalHeap="1000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="false"/>
<cache name="users"
maxEntriesLocalHeap="500"
timeToLiveSeconds="3600"
eternal="false"
overflowToDisk="false"/>
</ehcache>
配置类:
@Configuration
@EnableCaching
publicclass CacheConfig {
@Bean
public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
factoryBean.setShared(true);
return factoryBean;
}
@Bean
public CacheManager cacheManager(net.sf.ehcache.CacheManager cm) {
returnnew EhCacheCacheManager(cm);
}
}
4. 实体和仓库
用户实体类:
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
private Long id;
private String username;
}
用户仓库接口:
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
5. 服务层与缓存注解应用
@Service
@CacheConfig(cacheNames = "users")
publicclass UserService {
@Autowired
private UserRepository userRepository;
@Cacheable(key = "#id")
public User getUserById(Long id) {
simulateSlowService();
return userRepository.findById(id).orElse(null);
}
@Cacheable(key = "#username")
public User getUserByUsername(String username) {
simulateSlowService();
return userRepository.findByUsername(username);
}
@CachePut(key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
@CacheEvict(key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
private void simulateSlowService() {
try {
Thread.sleep(2000L); // 模拟耗时操作
} catch (InterruptedException e) {
thrownew IllegalStateException(e);
}
}
}
在上述示例中:
- getUserById和getUserByUsername方法被@Cacheable注解修饰,表示查询用户时会先从缓存中查找,若缓存不存在则执行数据库查询并将结果缓存在users缓存中。
- updateUser方法被@CachePut注解修饰,表示更新用户信息时,会将更新后的用户对象写入缓存。
- deleteUser方法被@CacheEvict注解修饰,表示删除用户时,会从缓存中移除对应的用户信息。
6. 控制层
@RestController
@RequestMapping("/api/users")
publicclass UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@GetMapping("/username/{username}")
public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
User user = userService.getUserByUsername(username);
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<User> addUser(@RequestBody User user) {
User savedUser = userService.updateUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
7. 测试缓存效果
(1) 启动应用程序。
(2) 调用GET /api/users/{id}接口:
- 首次调用会触发数据库查询并缓存结果。
- 第二次调用相同的接口,将直接从缓存中获取用户信息,响应速度更快。
(3) 调用POST /api/users接口更新用户:更新操作会通过@CachePut注解将新的用户信息更新到缓存中。
(4) 调用DELETE /api/users/{id}接口删除用户:删除操作会通过@CacheEvict注解从缓存中移除用户信息。
通过上述步骤,可以验证缓存的实际效果,发现读取操作的响应时间明显降低。
七、增强功能
1. 自定义缓存键生成策略
默认情况下,Spring根据方法的参数生成缓存键。对于复杂的业务场景,可能需要自定义缓存键生成策略。
自定义KeyGenerator:
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName() + "_" + Arrays.stream(params)
.map(Object::toString)
.collect(Collectors.joining("_"));
}
}
使用自定义KeyGenerator:
@Cacheable(cacheNames = "users", keyGenerator = "customKeyGenerator")
public User getUser(Long id, String type) {
// 方法实现
}
2. 缓存条件与排除
通过condition和unless属性,可以控制是否进行缓存操作。
- condition:在满足条件时才进行缓存。
- unless:在满足条件时不进行缓存。
示例:
@Cacheable(value = "users", key = "#id", condition = "#id > 10", unless = "#result.username == 'admin'")
public User getUserById(Long id) {
// 方法实现
}
在上述示例中:
- 只有当id > 10时,方法执行结果才会被缓存。
- 即使满足condition条件,如果result.username == 'admin',则不缓存结果。
3. 缓存同步与异步
在分布式系统中,缓存的一致性和同步性是至关重要的。Spring Cache本身不直接提供同步机制,但可以通过结合其他工具实现。
方案:
- 使用消息队列(如Kafka、RabbitMQ)同步缓存更新。
- 利用分布式锁(如Redis的RedLock)防止缓存击穿和缓存穿透。
- 实现基于事件驱动的缓存更新策略。
4. 缓存与事务的结合
在涉及事务的操作中,缓存的更新需要与事务保持一致性。
方案:
- 缓存更新操作应在事务提交后执行,确保数据的一致性。
- 使用@CacheEvict的beforeInvocation属性控制缓存清除的时机。
示例:
@CacheEvict(value = "users", key = "#id", beforeInvocation = false)
@Transactional
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
在上述示例中,缓存清除操作将在事务提交后执行,确保数据成功删除后再清除缓存。
八、总结
本文,我们分析了缓存技术,它在提升应用性能、降低数据库压力、改善用户体验方面发挥着重要作用。
另外,我们重点分析了 Spring中 5个核心的缓存注解以及示例分析,Spring通过提供全面的缓存抽象和简洁的缓存注解,使得开发者能够轻松地集成和管理缓存机制。