上一篇博文介紹了Spring中緩存注解@Cacheable
@CacheEvit
@CachePut
的基本使用,接下來我們將看一下更高級一點的知識點
- key生成策略
- 超時時間指定
I. 項目環境
1. 項目依賴
本項目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
+ redis5.0
進行開發
開一個web服務用於測試
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
II. 擴展知識點
1. key生成策略
對於@Cacheable
注解,有兩個參數用於組裝緩存的key
- cacheNames/value: 類似於緩存前綴
- key: SpEL表達式,通常根據傳參來生成最終的緩存key
默認的redisKey = cacheNames::key
(注意中間的兩個冒號)
如
/**
* 沒有指定key時,采用默認策略 {@link org.springframework.cache.interceptor.SimpleKeyGenerator } 生成key
* <p>
* 對應的key為: k1::id
* value --> 等同於 cacheNames
* @param id
* @return
*/
@Cacheable(value = "k1")
public String key1(int id) {
return "defaultKey:" + id;
}
緩存key默認采用SimpleKeyGenerator
來生成,比如上面的調用,如果id=1
, 那么對應的緩存key為 k1::1
如果沒有參數,或者多個參數呢?
/**
* redis_key : k2::SimpleKey[]
*
* @return
*/
@Cacheable(value = "k0")
public String key0() {
return "key0";
}
/**
* redis_key : k2::SimpleKey[id,id2]
*
* @param id
* @param id2
* @return
*/
@Cacheable(value = "k2")
public String key2(Integer id, Integer id2) {
return "key1" + id + "_" + id2;
}
@Cacheable(value = "k3")
public String key3(Map map) {
return "key3" + map;
}
然后寫一個測試case
@RestController
@RequestMapping(path = "extend")
public class ExtendRest {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ExtendDemo extendDemo;
@GetMapping(path = "default")
public Map<String, Object> key(int id) {
Map<String, Object> res = new HashMap<>();
res.put("key0", extendDemo.key0());
res.put("key1", extendDemo.key1(id));
res.put("key2", extendDemo.key2(id, id));
res.put("key3", extendDemo.key3(res));
// 這里將緩存key都撈出來
Set<String> keys = (Set<String>) redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
Set<byte[]> sets = connection.keys("k*".getBytes());
Set<String> ans = new HashSet<>();
for (byte[] b : sets) {
ans.add(new String(b));
}
return ans;
});
res.put("keys", keys);
return res;
}
}
訪問之后,輸出結果如下
{
"key1": "defaultKey:1",
"key2": "key11_1",
"key0": "key0",
"key3": "key3{key1=defaultKey:1, key2=key11_1, key0=key0}",
"keys": [
"k2::SimpleKey [1,1]",
"k1::1",
"k3::{key1=defaultKey:1, key2=key11_1, key0=key0}",
"k0::SimpleKey []"
]
}
小結一下
- 單參數:
cacheNames::arg
- 無參數:
cacheNames::SimpleKey []
, 后面使用SimpleKey []
來補齊 - 多參數:
cacheNames::SimpleKey [arg1, arg2...]
- 非基礎對象:
cacheNames::obj.toString()
2. 自定義key生成策略
如果希望使用自定義的key生成策略,只需繼承KeyGenerator
,並聲明為一個bean
@Component("selfKeyGenerate")
public static class SelfKeyGenerate implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return target.getClass().getSimpleName() + "#" + method.getName() + "(" + JSON.toJSONString(params) + ")";
}
}
然后在使用的地方,利用注解中的keyGenerator
來指定key生成策略
/**
* 對應的redisKey 為: get vv::ExtendDemo#selfKey([id])
*
* @param id
* @return
*/
@Cacheable(value = "vv", keyGenerator = "selfKeyGenerate")
public String selfKey(int id) {
return "selfKey:" + id + " --> " + UUID.randomUUID().toString();
}
測試用例
@GetMapping(path = "self")
public Map<String, Object> self(int id) {
Map<String, Object> res = new HashMap<>();
res.put("self", extendDemo.selfKey(id));
Set<String> keys = (Set<String>) redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
Set<byte[]> sets = connection.keys("vv*".getBytes());
Set<String> ans = new HashSet<>();
for (byte[] b : sets) {
ans.add(new String(b));
}
return ans;
});
res.put("keys", keys);
return res;
}
緩存key放在了返回結果的keys
中,輸出如下,和預期的一致
{
"keys": [
"vv::ExtendDemo#selfKey([1])"
],
"self": "selfKey:1 --> f5f8aa2a-0823-42ee-99ec-2c40fb0b9338"
}
3. 緩存失效時間
以上所有的緩存都沒有設置失效時間,實際的業務場景中,不設置失效時間的場景有;但更多的都需要設置一個ttl,對於Spring的緩存注解,原生沒有額外提供一個指定ttl的配置,如果我們希望指定ttl,可以通過RedisCacheManager
來完成
private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
// 設置 json 序列化
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).
// 設置過期時間
entryTtl(Duration.ofSeconds(seconds));
return redisCacheConfiguration;
}
上面是一個設置RedisCacheConfiguration
的方法,其中有兩個點
- 序列化方式:采用json對緩存內容進行序列化
- 失效時間:根據傳參來設置失效時間
如果希望針對特定的key進行定制化的配置的話,可以如下操作
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(8);
// 自定義設置緩存時間
// 這個k0 表示的是緩存注解中的 cacheNames/value
redisCacheConfigurationMap.put("k0", this.getRedisCacheConfigurationWithTtl(60 * 60));
return redisCacheConfigurationMap;
}
最后就是定義我們需要的RedisCacheManager
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return new RedisCacheManager(
RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
// 默認策略,未配置的 key 會使用這個
this.getRedisCacheConfigurationWithTtl(60),
// 指定 key 策略
this.getRedisCacheConfigurationMap()
);
}
在前面的測試case基礎上,添加返回ttl的信息
private Object getTtl(String key) {
return redisTemplate.execute(new RedisCallback() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
return connection.ttl(key.getBytes());
}
});
}
@GetMapping(path = "default")
public Map<String, Object> key(int id) {
Map<String, Object> res = new HashMap<>();
res.put("key0", extendDemo.key0());
res.put("key1", extendDemo.key1(id));
res.put("key2", extendDemo.key2(id, id));
res.put("key3", extendDemo.key3(res));
Set<String> keys = (Set<String>) redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
Set<byte[]> sets = connection.keys("k*".getBytes());
Set<String> ans = new HashSet<>();
for (byte[] b : sets) {
ans.add(new String(b));
}
return ans;
});
res.put("keys", keys);
Map<String, Object> ttl = new HashMap<>(8);
for (String key : keys) {
ttl.put(key, getTtl(key));
}
res.put("ttl", ttl);
return res;
}
返回結果如下,注意返回的ttl失效時間
4. 自定義失效時間擴展
雖然上面可以實現失效時間指定,但是用起來依然不是很爽,要么是全局設置為統一的失效時間;要么就是在代碼里面硬編碼指定,失效時間與緩存定義的地方隔離,這就很不直觀了
接下來介紹一種,直接在注解中,設置失效時間的case
如下面的使用case
/**
* 通過自定義的RedisCacheManager, 對value進行解析,=后面的表示失效時間
* @param key
* @return
*/
@Cacheable(value = "ttl=30")
public String ttl(String key) {
return "k_" + key;
}
自定義的策略如下:
- value中,等號左邊的為cacheName, 等號右邊的為失效時間
要實現這個邏輯,可以擴展一個自定義的RedisCacheManager
,如
public class TtlRedisCacheManager extends RedisCacheManager {
public TtlRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
String[] cells = StringUtils.delimitedListToStringArray(name, "=");
name = cells[0];
if (cells.length > 1) {
long ttl = Long.parseLong(cells[1]);
// 根據傳參設置緩存失效時間
cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));
}
return super.createRedisCache(name, cacheConfig);
}
}
重寫createRedisCache
邏輯, 根據name解析出失效時間;
注冊使用方式與上面一致,聲明為Spring的bean對象
@Primary
@Bean
public RedisCacheManager ttlCacheManager(RedisConnectionFactory redisConnectionFactory) {
return new TtlRedisCacheManager(RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory),
// 默認緩存配置
this.getRedisCacheConfigurationWithTtl(60));
}
測試case如下
@GetMapping(path = "ttl")
public Map ttl(String k) {
Map<String, Object> res = new HashMap<>();
res.put("execute", extendDemo.ttl(k));
res.put("ttl", getTtl("ttl::" + k));
return res;
}
驗證結果如下
5. 小結
到此基本上將Spring中緩存注解的常用姿勢都介紹了一下,無論是幾個注解的使用case,還是自定義的key策略,失效時間指定,單純從使用的角度來看,基本能滿足我們的日常需求場景
下面是針對緩存注解的一個知識點抽象
緩存注解
@Cacheable
: 緩存存在,則從緩存取;否則執行方法,並將返回結果寫入緩存@CacheEvit
: 失效緩存@CachePut
: 更新緩存@Caching
: 都注解組合
配置參數
cacheNames/value
: 可以理解為緩存前綴key
: 可以理解為緩存key的變量,支持SpEL表達式keyGenerator
: key組裝策略condition/unless
: 緩存是否可用的條件
默認緩存ke策略y
下面的cacheNames為注解中定義的緩存前綴,兩個分號固定
- 單參數:
cacheNames::arg
- 無參數:
cacheNames::SimpleKey []
, 后面使用SimpleKey []
來補齊 - 多參數:
cacheNames::SimpleKey [arg1, arg2...]
- 非基礎對象:
cacheNames::obj.toString()
緩存失效時間
失效時間,本文介紹了兩種方式,一個是集中式的配置,通過設置RedisCacheConfiguration
來指定ttl時間
另外一個是擴展RedisCacheManager
類,實現自定義的cacheNames
擴展解析
Spring緩存注解知識點到此告一段落,我是一灰灰,歡迎關注長草的公眾號一灰灰blog
III. 不能錯過的源碼和相關知識點
0. 項目
系列博文
源碼
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 源碼:https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/
1. 一灰灰Blog
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛
- 一灰灰Blog個人博客 https://blog.hhui.top
- 一灰灰Blog-Spring專題博客 http://spring.hhui.top