在微服务,分布式的大环境下,缓存绝对是提升系统性能的关键手段,Spring作为 Java生态中最流行的企业级应用框架,它是如何实现缓存的呢?这篇文章,我们将深入探讨 Spring中 5个核心的缓存注解。
1. 什么是缓存?
缓存(Cache)是一种存储机制,旨在临时存储数据副本,以便快速访问。缓存一般位于应用程序与数据源(如数据库)之间,能够显著降低数据访问延迟和减轻数据源的压力。
2. 缓存的类型
缓存一般可以分为下面 4种类型:
- 本地缓存:存在于应用程序本地内存中,例如使用
ConcurrentHashMap
、Guava Cache等。 - 分布式缓存:跨多个应用实例共享的缓存,例如Redis、Memcached、EhCache的分布式配置等。
- 持久化缓存:将缓存数据持久化到磁盘,以应对应用重启后的数据恢复。
- 非持久化缓存:缓存数据存储于内存,应用重启后数据丢失。
3. Spring缓存
Spring 从4.0版本起开始引入了 Cache模块,并提供了一套统一的缓存API,隐藏了底层缓存实现的复杂性。开发者只需通过配置和注解即可实现缓存功能,支持多种缓存实现,如EhCache、Redis、Caffeine等。
Spring缓存模块的核心组件包括:
- CacheManager:管理多个Cache实例,根据需要选择合适的Cache。
- Cache:具体的缓存操作接口,定义了基本的缓存操作方法,如
get
、put
、evict
等。 - CacheResolver:根据方法信息动态解析需要使用的Cache。
- KeyGenerator:生成缓存键的策略。
通过合理配置和使用,Spring缓存抽象能够灵活地满足各种应用场景的需求。
4. Spring缓存注解详解
Spring缓存注解主要有以下 5个:
@Cacheable
@CachePut
@CacheEvict
@Caching
@CacheConfig
下面我们将逐一对这些注解进行分析。
4.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
:指定缓存解析器。
4.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
等属性,用于指定缓存名称、键及其他配置。
4.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.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
提供了更灵活的缓存操作能力。
4.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
通过提供类级别的缓存配置,简化了属性的配置和维护,提高了代码的可读性和可维护性。
5. 缓存框架
要使 Spring的缓存注解生效,必须配置一个缓存管理器(CacheManager
)和相应的缓存提供者。Spring支持多种缓存实现,常见的包括 EhCache、Redis、Caffeine等。下面,我们介绍这 3种常用缓存提供者的配置方法。
5.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()); } }
5.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(); } }
5.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; } }
6. 案例分析
下面我们通过一个简单的 CRUD应用,演示如何在 Spring Boot项目中集成和使用缓存注解。
6.1 项目介绍
构建一个用户管理系统,包含用户的增删改查功能。通过缓存优化其中的读取操作,以提升系统性能。
6.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>
6.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); } }
6.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); }
6.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.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(); } }
6.7 测试缓存效果
- 启动应用程序。
- 调用
GET /api/users/{id}
接口:- 首次调用会触发数据库查询并缓存结果。
- 第二次调用相同的接口,将直接从缓存中获取用户信息,响应速度更快。
- 调用
POST /api/users
接口更新用户:- 更新操作会通过
@CachePut
注解将新的用户信息更新到缓存中。
- 更新操作会通过
- 调用
DELETE /api/users/{id}
接口删除用户:- 删除操作会通过
@CacheEvict
注解从缓存中移除用户信息。
- 删除操作会通过
通过上述步骤,可以验证缓存的实际效果,发现读取操作的响应时间明显降低。
7. 增强功能
7.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) { // 方法实现 }
7.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'
,则不缓存结果。
7.3 缓存同步与异步
在分布式系统中,缓存的一致性和同步性是至关重要的。Spring Cache本身不直接提供同步机制,但可以通过结合其他工具实现。
方案:
- 使用消息队列(如Kafka、RabbitMQ)同步缓存更新。
- 利用分布式锁(如Redis的RedLock)防止缓存击穿和缓存穿透。
- 实现基于事件驱动的缓存更新策略。
7.4 缓存与事务的结合
在涉及事务的操作中,缓存的更新需要与事务保持一致性。
方案:
- 缓存更新操作应在事务提交后执行,确保数据的一致性。
- 使用
@CacheEvict
的beforeInvocation
属性控制缓存清除的时机。
示例:
@CacheEvict(value = "users", key = "#id", beforeInvocation = false) @Transactional public void deleteUser(Long id) { userRepository.deleteById(id); }
在上述示例中,缓存清除操作将在事务提交后执行,确保数据成功删除后再清除缓存。
8. 总结
本文,我们分析了缓存技术,它在提升应用性能、降低数据库压力、改善用户体验方面发挥着重要作用。
另外,我们重点分析了 Spring中 5个核心的缓存注解以及示例分析,Spring通过提供全面的缓存抽象和简洁的缓存注解,使得开发者能够轻松地集成和管理缓存机制。