Spring Cache用法及常見問題解決方案
Spring Cache 作為 Spring 框架提供的緩存抽象層,確實能夠顯著簡化項目中 Redis、Caffeine 等緩存技術的使用,但許多開發(fā)者在實際應用中會遇到各種"看似正確但緩存不生效"的問題。以下將系統(tǒng)性地分析這些問題的根源,并提供全面的解決方案。
這篇文章將會以一個親身經(jīng)歷者的角度,系統(tǒng)的講解SpringCache的用法,并且舉例介紹使用SpringCache遇到的常見問題。
一、介紹
1.1 基本介紹
Spring Cache 是 Spring 框架提供的一個緩存抽象層,它通過在方法上添加簡單的注解來實現(xiàn)緩存功能,從而減少重復計算,提高系統(tǒng)性能。
Spring Cache 利用了AOP,實現(xiàn)了基于注解的緩存功能,并且進行了合理的抽象,業(yè)務代碼不用關心底層是使用了什么緩存框架,只需要簡單地加一個注解,就能實現(xiàn)緩存功能了,做到了對代碼侵入性做小。
由于市面上的緩存工具實在太多,SpringCache框架還提供了CacheManager接口,可以實現(xiàn)降低對各種緩存框架的耦合。它不是具體的緩存實現(xiàn),它只提供一整套的接口和代碼規(guī)范、配置、注解等,用于整合各種緩存方案,比如Redis、Caffeine、Guava Cache、Ehcache。
1.2 核心概念
(1)緩存抽象
Spring Cache 提供了一組通用的緩存抽象接口,主要包括:
Cache- 緩存接口,定義緩存操作CacheManager- 緩存管理器,用于管理各種緩存組件
(2)主要注解
Spring Cache 通過以下注解提供聲明式緩存:
@Cacheable- 表明方法的返回值可以被緩存@CacheEvict- 表明方法會觸發(fā)緩存的清除@CachePut- 表明方法會更新緩存,但總會執(zhí)行方法@Caching- 組合多個緩存操作@CacheConfig- 類級別的共享緩存配置
二、常見坑
2.1當一個類中的方法A(帶有@Cacheable注解)被同一個類中的另一個方法B調用時,@Cacheable注解會失效,緩存機制不會起作用
(1)案例演示
package com.example.demo;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class Controller {
@GetMapping("/test")
public String get() {
return innerGet();
}
@Cacheable(value = "inner")
public String innerGet() {
System.out.println("----------1111111111111------------");
return "內部調用";
}
}

通過反復調用該接口,我們發(fā)現(xiàn)系統(tǒng)并沒有命中緩存,而是每次都重復執(zhí)行了innerGet()方法,redis緩存中也確實沒有這個inner相關的key。
其實這個問題在我的idea中已經(jīng)有警告提示了,如下圖所示:

意思是說
當一個類中的方法A(帶有@Cacheable注解)被同一個類中的另一個方法B調用時,@Cacheable注解會失效,緩存機制不會起作用。
(2)原因分析
這是由于Spring AOP(面向切面編程)的實現(xiàn)方式導致的:
- Spring的緩存功能是通過AOP代理實現(xiàn)的
- 當方法從類外部調用時,會經(jīng)過代理,緩存邏輯能正常執(zhí)行
- 但當方法從類內部調用時(自調用),會繞過代理直接調用,導致緩存邏輯被跳過
(3)解決辦法
- 將緩存方法移到另一個類?。?!(推薦)
- 自行注入自己(通過構造函數(shù)或@Autowired),然后再用這個注入的當前類的對象去調用這個方法。(不太推薦)
(4)引申
這種自調用失效的問題是Spring AOP的普遍現(xiàn)象,不僅限于@Cacheable,其他如@Transactional、@Async等注解也有同樣的問題。
@Service
public class OrderService {
public void placeOrder(Order order) {
// 這里直接調用,@Transactional會失效
updateInventory(order.getItems()); // 事務不會生效
// 其他業(yè)務邏輯...
}
@Transactional
public void updateInventory(List<Item> items) {
// 更新庫存操作
items.forEach(item -> {
inventoryRepository.reduceStock(item.getId(), item.getQuantity());
});
}
}為什么失效?
AOP 代理機制:Spring 的事務管理是通過 AOP 代理實現(xiàn)的。當 placeOrder 直接調用 updateInventory 時,調用發(fā)生在目標對象內部,繞過了 Spring 創(chuàng)建的代理對象。因此事務攔截器沒有機會介入,事務不會開啟。
解決辦法
解決辦法是一樣的,這里也是推薦拆分到不同服務類。
@Service
public class OrderService {
@Autowired
private InventoryService inventoryService;
public void placeOrder(Order order) {
inventoryService.updateInventory(order.getItems()); // 現(xiàn)在會走代理,事務生效
}
}
@Service
public class InventoryService {
@Transactional
public void updateInventory(List<Item> items) {
// 更新庫存操作
}
}2.2 json序列化問題
(1)案例演示

存在亂碼的情況,如:內部調用
(2)原因分析
- 這是因為
- Spring Cache 默認使用 JDK 序列化方式
- JDK 序列化會產(chǎn)生二進制數(shù)據(jù),導致 Redis 中顯示亂碼
- 您看到的
內部調用就是 JDK 序列化的結果
(3)解決辦法
配置 CacheManager 使用 JSON 序列化
@EnableCaching
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}這樣就可以了,不信我們再寫兩個案例試試
@RestController
@RequestMapping("/api")
public class Controller {
@Autowired
private Controller controller;
@GetMapping("/test")
public String get() {
return controller.innerGet();
}
@Cacheable(value = "inner")
public String innerGet() {
System.out.println("----------1111111111111------------");
return "內部調用";
}
@GetMapping("/cacheUser")
public User cacheUser() {
return controller.getUser();
}
@Cacheable(value = "user")
public User getUser() {
System.out.println("----------2222222222222------------");
User user = new User();
user.setName("張三");
user.setAge(18);
user.setPassword("2342a18");
user.setEmail("32432kjkj@1523.com");
return user;
}
}



(4)提醒
如果不使用springcache,想通過RedisTemplate直接編程式的操作redis的話,記得也需要配置一下RedisTemplate的序列化。否則也會有這樣的問題。當然了,你還可以使用StringRedisTemplate,這在Redis入門教程中已經(jīng)講解過了,這里不在贅述。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用 String 序列化 key
template.setKeySerializer(new StringRedisSerializer());
// 使用 Jackson 序列化 value
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}三、正確使用教程?
3.1 導入依賴
這里的緩存我們用Redis。當然如果是其他緩存,請自行引入即可。
<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>3.2 緩存配置
這一步主要是進行序列化。
防止SpringCache注解加入緩存后,出現(xiàn)亂碼的情況。因為Spring Cache 默認使用 JDK 序列化方式。另外對RedisTemplate也進行了JDK序列化自定義,除非你的項目一定不用編程式操作redis。
具體原因在第二章已經(jīng)講解了,這里只給出正確配置。
package com.example.demo;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableCaching // 一定要開啟緩存
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用 String 序列化 key
template.setKeySerializer(new StringRedisSerializer());
// 使用 Jackson 序列化 value
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}3.3 常用注解
(1)@Cacheable
用于標記方法的返回值可以被緩存。
- 常用屬性:
- value/cacheNames:指定緩存名稱(value和cacheNames是互斥的,即只能指定其中一個,二選一,必填)
- key:指定緩存的key??梢允褂肧pring Expression Language(SpEL)來編寫
key表達式,以實現(xiàn)動態(tài)鍵的生成- 默認情況下,若未顯式指定key,Spring會使用方法的??所有參數(shù)組合??作為鍵(通過
SimpleKeyGenerator生成) - 與
keyGenerator沖突??:key和keyGenerator屬性不能同時使用,需二選一
- 默認情況下,若未顯式指定key,Spring會使用方法的??所有參數(shù)組合??作為鍵(通過
- condition:指定緩存條件
- unless:否決緩存的條件
//?? 參數(shù)作為鍵
@Cacheable(value = "userCache", key = "#id") // 使用參數(shù)id作為鍵
public User getUserById(Long id) { ... }
// 若參數(shù)是對象,可訪問其屬性:
@Cacheable(value = "userCache", key = "#user.id") // 使用user對象的id屬性
public User find(User user) { ... }
// ??多參數(shù)組合鍵
@Cacheable(value = "userCache", key = "#firstName + '-' + #lastName")
public User getUserByName(String firstName, String lastName) { ... }
// ??方法信息作為鍵
@Cacheable(value = "userCache", key = "#root.methodName + #id") // 方法名+參數(shù)
public User getUserById(Long id) { ... }
/*
SpEL支持的元數(shù)據(jù)??
在SpEL表達式中,可通過以下變量生成鍵:
#root.methodName:當前方法名
#root.target:目標對象實例
#result:方法返回值(僅適用于unless或condition)
#參數(shù)名或#p0/#p1(按參數(shù)索引)
*/(2)@CachePut
- 總是執(zhí)行方法,并將結果放入緩存
- 通常用于更新操作后更新緩存
@CachePut(value="users", key="#user.id")
public User updateUser(User user) {...}(3)@CacheEvict
- 用于清除緩存
- 常用屬性:
- allEntries:是否清除所有緩存(默認false)
- beforeInvocation:是否在方法執(zhí)行前清除(默認false)
@CacheEvict(value="users", key="#userId")
public void deleteUser(String userId) {...}(4)@Caching
- 用于組合多個緩存操作
@Caching(evict = {
@CacheEvict(value="primary", key="#user.id"),
@CacheEvict(value="secondary", key="#user.username")
})
public void updateUser(User user) {...}3.4 注意事項
??方法A調用同類中帶緩存的方法B時,若沒有自行注入自己,則無法引入緩存。(Spring AOP的普遍現(xiàn)象)
??未自定義序列化操作,則可能出現(xiàn)亂碼現(xiàn)象
到此這篇關于Spring Cache用法很簡單,但你知道這中間的坑嗎?的文章就介紹到這了,更多相關Spring Cache用法內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringCloud微服務之Hystrix組件實現(xiàn)服務熔斷的方法
微服務架構特點就是多服務,多數(shù)據(jù)源,支撐系統(tǒng)應用。這樣導致微服務之間存在依賴關系。這篇文章主要介紹了SpringCloud微服務之Hystrix組件實現(xiàn)服務熔斷的方法,需要的朋友可以參考下2019-08-08
java 中JFinal getModel方法和數(shù)據(jù)庫使用出現(xiàn)問題解決辦法
這篇文章主要介紹了java 中JFinal getModel方法和數(shù)據(jù)庫使用出現(xiàn)問題解決辦法的相關資料,需要的朋友可以參考下2017-04-04
springboot如何獲取request請求的原始url與post參數(shù)
這篇文章主要介紹了springboot如何獲取request請求的原始url與post參數(shù)問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12

