Spring 的緩存帝國,得益于這五個(gè)注解!
在微服務(wù),分布式的大環(huán)境下,緩存絕對是提升系統(tǒng)性能的關(guān)鍵手段,Spring作為 Java生態(tài)中最流行的企業(yè)級應(yīng)用框架,它是如何實(shí)現(xiàn)緩存的呢?這篇文章,我們將深入探討 Spring中 5個(gè)核心的緩存注解。
一、什么是緩存?
緩存(Cache)是一種存儲機(jī)制,旨在臨時(shí)存儲數(shù)據(jù)副本,以便快速訪問。緩存一般位于應(yīng)用程序與數(shù)據(jù)源(如數(shù)據(jù)庫)之間,能夠顯著降低數(shù)據(jù)訪問延遲和減輕數(shù)據(jù)源的壓力。
二、緩存的類型
緩存一般可以分為下面 4種類型:
- 本地緩存:存在于應(yīng)用程序本地內(nèi)存中,例如使用ConcurrentHashMap、Guava Cache等。
- 分布式緩存:跨多個(gè)應(yīng)用實(shí)例共享的緩存,例如Redis、Memcached、EhCache的分布式配置等。
- 持久化緩存:將緩存數(shù)據(jù)持久化到磁盤,以應(yīng)對應(yīng)用重啟后的數(shù)據(jù)恢復(fù)。
- 非持久化緩存:緩存數(shù)據(jù)存儲于內(nèi)存,應(yīng)用重啟后數(shù)據(jù)丟失。
三、Spring緩存
Spring 從4.0版本起開始引入了 Cache模塊,并提供了一套統(tǒng)一的緩存API,隱藏了底層緩存實(shí)現(xiàn)的復(fù)雜性。開發(fā)者只需通過配置和注解即可實(shí)現(xiàn)緩存功能,支持多種緩存實(shí)現(xiàn),如EhCache、Redis、Caffeine等。
Spring緩存模塊的核心組件包括:
- CacheManager:管理多個(gè)Cache實(shí)例,根據(jù)需要選擇合適的Cache。
- Cache:具體的緩存操作接口,定義了基本的緩存操作方法,如get、put、evict等。
- CacheResolver:根據(jù)方法信息動態(tài)解析需要使用的Cache。
- KeyGenerator:生成緩存鍵的策略。
通過合理配置和使用,Spring緩存抽象能夠靈活地滿足各種應(yīng)用場景的需求。
四、Spring緩存注解詳解
Spring緩存注解主要有以下 5個(gè):
- @Cacheable
- @CachePut
- @CacheEvict
- @Caching
- @CacheConfig
下面我們將逐一對這些注解進(jìn)行分析。
1. @Cacheable
@Cacheable注解用于方法級別,表示方法執(zhí)行的結(jié)果可以被緩存。當(dāng)方法被調(diào)用時(shí),Spring會先檢查緩存中是否存在對應(yīng)的鍵值對,如果存在,則直接返回緩存中的結(jié)果;如果不存在,則執(zhí)行方法,并將結(jié)果存入緩存。
使用示例:
@Service
publicclass UserService {
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
// 模擬數(shù)據(jù)庫訪問
simulateSlowService();
returnnew User(id, "John Doe");
}
private void simulateSlowService() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
thrownew IllegalStateException(e);
}
}
}
在上述示例中,getUserById方法被@Cacheable注解修飾,指定使用users緩存,并以方法參數(shù)id作為緩存鍵。首次調(diào)用該方法時(shí),緩存中不存在對應(yīng)的用戶信息,方法會被執(zhí)行并將結(jié)果存入緩存。后續(xù)相同的調(diào)用將直接從緩存中獲取結(jié)果,避免了重復(fù)的業(yè)務(wù)邏輯執(zhí)行。
關(guān)鍵屬性:
- value / cacheNames:指定緩存的名稱,可以有多個(gè),表示多個(gè)緩存同時(shí)生效。
- key:指定緩存的鍵,支持SpEL表達(dá)式,默認(rèn)基于方法參數(shù)生成。
- condition:緩存條件,符合條件的情況下才進(jìn)行緩存。
- unless:否決緩存條件,符合條件的情況下不緩存。
- keyGenerator:自定義鍵生成策略。
- cacheManager:指定使用的緩存管理器。
- cacheResolver:指定緩存解析器。
2. @CachePut
@CachePut注解同樣用于方法級別,但與@Cacheable不同,它總是執(zhí)行方法,并將結(jié)果存入緩存。@CachePut適用于需要更新緩存但不影響方法執(zhí)行結(jié)果的場景。
使用示例:
@Service
public class UserService {
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
// 模擬更新數(shù)據(jù)庫
return user;
}
}
在上述示例中,updateUser方法被@CachePut注解修飾,每次調(diào)用該方法時(shí),都會執(zhí)行方法邏輯(更新操作),并將返回的User對象更新到users緩存中。這樣可以確保緩存中的數(shù)據(jù)與數(shù)據(jù)庫中的數(shù)據(jù)保持一致。
關(guān)鍵屬性:與@Cacheable相同,@CachePut也支持value、cacheNames、key等屬性,用于指定緩存名稱、鍵及其他配置。
3. @CacheEvict
@CacheEvict注解用于方法級別,表示在方法執(zhí)行后,清除指定緩存中的一個(gè)或多個(gè)條目。它常用于刪除操作,以確保緩存中的數(shù)據(jù)與數(shù)據(jù)源保持一致。
使用示例:
@Service
public class UserService {
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
// 模擬刪除數(shù)據(jù)庫
}
}
在上述示例中,deleteUser方法被@CacheEvict注解修飾,指定從users緩存中移除鍵為id的條目。這樣,在用戶被刪除后,相應(yīng)的緩存數(shù)據(jù)也被清除,防止緩存中的數(shù)據(jù)不一致。
關(guān)鍵屬性:
- value / cacheNames:指定緩存的名稱。
- key:指定要清除的緩存鍵。
- allEntries:指定是否清除緩存中的所有條目,默認(rèn)為false。
- beforeInvocation:指定清除緩存的時(shí)機(jī),默認(rèn)為方法執(zhí)行成功后。
- cacheManager:指定使用的緩存管理器。
- cacheResolver:指定緩存解析器。
4. @Caching
@Caching注解用于組合多個(gè)緩存注解,使得在一個(gè)方法上可以執(zhí)行多個(gè)緩存操作。它適用于需要同時(shí)執(zhí)行多個(gè)緩存行為的復(fù)雜場景。
使用示例:
@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) {
// 模擬添加用戶到數(shù)據(jù)庫
return user;
}
}
在上述示例中,addUser方法通過@Caching注解同時(shí)執(zhí)行了兩個(gè)@CachePut操作,將用戶信息存入不同的緩存中,并且執(zhí)行了一個(gè)@CacheEvict操作,清除userCache中的所有條目。
關(guān)鍵屬性:
- @Caching主要包含以下屬性:
- cacheable:@Cacheable注解數(shù)組。
- put:@CachePut注解數(shù)組。
- evict:@CacheEvict注解數(shù)組。
通過組合不同類型的緩存注解,@Caching提供了更靈活的緩存操作能力。
5. @CacheConfig
@CacheConfig注解用于類級別,為該類中的所有緩存注解提供公共配置。例如,可以指定統(tǒng)一的緩存名稱、緩存管理器等,減少重復(fù)配置的工作量。
使用示例:
@Service
@CacheConfig(cacheNames = "users", cacheManager = "cacheManager")
publicclass UserService {
@Cacheable(key = "#id")
public User getUserById(Long id) {
// 模擬數(shù)據(jù)庫訪問
returnnew User(id, "John Doe");
}
@CachePut(key = "#user.id")
public User updateUser(User user) {
// 模擬更新數(shù)據(jù)庫
return user;
}
@CacheEvict(key = "#id")
public void deleteUser(Long id) {
// 模擬刪除數(shù)據(jù)庫
}
}
在上述示例中,@CacheConfig注解指定了默認(rèn)的緩存名稱和緩存管理器,使得類中的所有緩存注解無需重復(fù)指定這些屬性,只需關(guān)注特定的鍵或其他配置。
關(guān)鍵屬性:
- cacheNames / value:指定默認(rèn)的緩存名稱。
- cacheManager:指定默認(rèn)的緩存管理器。
- cacheResolver:指定默認(rèn)的緩存解析器。
- keyGenerator:指定默認(rèn)的鍵生成策略。
@CacheConfig通過提供類級別的緩存配置,簡化了屬性的配置和維護(hù),提高了代碼的可讀性和可維護(hù)性。
五、緩存框架
要使 Spring的緩存注解生效,必須配置一個(gè)緩存管理器(CacheManager)和相應(yīng)的緩存提供者。Spring支持多種緩存實(shí)現(xiàn),常見的包括 EhCache、Redis、Caffeine等。下面,我們介紹這 3種常用緩存提供者的配置方法。
1. EhCache
EhCache是一款常用的開源緩存庫,支持本地內(nèi)存和磁盤存儲,配置靈活,適用于單機(jī)應(yīng)用。
依賴配置(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>
配置示例:
創(chuàng)建一個(gè)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緩存數(shù)據(jù)庫,支持分布式部署,適用于大規(guī)模應(yīng)用場景。
依賴配置(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) {
// 配置默認(rèn)緩存過期時(shí)間等
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(60))
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
3. Caffeine
Caffeine是一個(gè)高性能的本地緩存庫,具有豐富的緩存策略和高并發(fā)性能。
依賴配置(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;
}
}
六、案例分析
下面我們通過一個(gè)簡單的 CRUD應(yīng)用,演示如何在 Spring Boot項(xiàng)目中集成和使用緩存注解。
1. 項(xiàng)目介紹
構(gòu)建一個(gè)用戶管理系統(tǒng),包含用戶的增刪改查功能。通過緩存優(yōu)化其中的讀取操作,以提升系統(tǒng)性能。
2. 環(huán)境搭建
技術(shù)棧:
- Spring Boot:快速構(gòu)建項(xiàng)目基礎(chǔ)。
- Spring Data JPA:數(shù)據(jù)訪問層。
- H2數(shù)據(jù)庫:內(nèi)存數(shù)據(jù)庫,方便演示。
- Spring Cache:緩存抽象。
- EhCache:作為緩存實(shí)現(xiàn)。
依賴配置(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. 緩存配置
創(chuàng)建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. 實(shí)體和倉庫
用戶實(shí)體類:
@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. 服務(wù)層與緩存注解應(yīng)用
@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); // 模擬耗時(shí)操作
} catch (InterruptedException e) {
thrownew IllegalStateException(e);
}
}
}
在上述示例中:
- getUserById和getUserByUsername方法被@Cacheable注解修飾,表示查詢用戶時(shí)會先從緩存中查找,若緩存不存在則執(zhí)行數(shù)據(jù)庫查詢并將結(jié)果緩存在users緩存中。
- updateUser方法被@CachePut注解修飾,表示更新用戶信息時(shí),會將更新后的用戶對象寫入緩存。
- deleteUser方法被@CacheEvict注解修飾,表示刪除用戶時(shí),會從緩存中移除對應(yīng)的用戶信息。
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) 啟動應(yīng)用程序。
(2) 調(diào)用GET /api/users/{id}接口:
- 首次調(diào)用會觸發(fā)數(shù)據(jù)庫查詢并緩存結(jié)果。
- 第二次調(diào)用相同的接口,將直接從緩存中獲取用戶信息,響應(yīng)速度更快。
(3) 調(diào)用POST /api/users接口更新用戶:更新操作會通過@CachePut注解將新的用戶信息更新到緩存中。
(4) 調(diào)用DELETE /api/users/{id}接口刪除用戶:刪除操作會通過@CacheEvict注解從緩存中移除用戶信息。
通過上述步驟,可以驗(yàn)證緩存的實(shí)際效果,發(fā)現(xiàn)讀取操作的響應(yīng)時(shí)間明顯降低。
七、增強(qiáng)功能
1. 自定義緩存鍵生成策略
默認(rèn)情況下,Spring根據(jù)方法的參數(shù)生成緩存鍵。對于復(fù)雜的業(yè)務(wù)場景,可能需要自定義緩存鍵生成策略。
自定義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) {
// 方法實(shí)現(xiàn)
}
2. 緩存條件與排除
通過condition和unless屬性,可以控制是否進(jìn)行緩存操作。
- condition:在滿足條件時(shí)才進(jìn)行緩存。
- unless:在滿足條件時(shí)不進(jìn)行緩存。
示例:
@Cacheable(value = "users", key = "#id", condition = "#id > 10", unless = "#result.username == 'admin'")
public User getUserById(Long id) {
// 方法實(shí)現(xiàn)
}
在上述示例中:
- 只有當(dāng)id > 10時(shí),方法執(zhí)行結(jié)果才會被緩存。
- 即使?jié)M足condition條件,如果result.username == 'admin',則不緩存結(jié)果。
3. 緩存同步與異步
在分布式系統(tǒng)中,緩存的一致性和同步性是至關(guān)重要的。Spring Cache本身不直接提供同步機(jī)制,但可以通過結(jié)合其他工具實(shí)現(xiàn)。
方案:
- 使用消息隊(duì)列(如Kafka、RabbitMQ)同步緩存更新。
- 利用分布式鎖(如Redis的RedLock)防止緩存擊穿和緩存穿透。
- 實(shí)現(xiàn)基于事件驅(qū)動的緩存更新策略。
4. 緩存與事務(wù)的結(jié)合
在涉及事務(wù)的操作中,緩存的更新需要與事務(wù)保持一致性。
方案:
- 緩存更新操作應(yīng)在事務(wù)提交后執(zhí)行,確保數(shù)據(jù)的一致性。
- 使用@CacheEvict的beforeInvocation屬性控制緩存清除的時(shí)機(jī)。
示例:
@CacheEvict(value = "users", key = "#id", beforeInvocation = false)
@Transactional
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
在上述示例中,緩存清除操作將在事務(wù)提交后執(zhí)行,確保數(shù)據(jù)成功刪除后再清除緩存。
八、總結(jié)
本文,我們分析了緩存技術(shù),它在提升應(yīng)用性能、降低數(shù)據(jù)庫壓力、改善用戶體驗(yàn)方面發(fā)揮著重要作用。
另外,我們重點(diǎn)分析了 Spring中 5個(gè)核心的緩存注解以及示例分析,Spring通過提供全面的緩存抽象和簡潔的緩存注解,使得開發(fā)者能夠輕松地集成和管理緩存機(jī)制。