The previous blog post introduced the @Cacheable
@CacheEvit
@CachePut
basic use of cache annotations in Spring, and then we will look at more advanced knowledge points
- key generation strategy
- Timeout time specification
<!-- more -->
I. Project Environment
1. Project dependencies
This project is developed with the help of SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
+redis5.0
Open a web service for testing
<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. Expanding knowledge points
1. Key generation strategy
For @Cacheable
annotations, there are two parameters used to assemble the cache key
- cacheNames/value: similar to cache prefix
- key: SpEL expression, usually generates the final cache key according to the passed parameters
Default redisKey = cacheNames::key
(note the two colons in the middle)
Such as
/**
* 没有指定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;
}
The cache key is generated by default SimpleKeyGenerator
, such as the above call, if id=1
, then the corresponding cache key isk1::1
What if there are no parameters, or multiple parameters?
/**
* 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;
}
Then write a test 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;
}
}
After accessing, the output is as follows
{
"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 []"
]
}
Summary
- Single parameter:
cacheNames::arg
- No parameters:
cacheNames::SimpleKey []
, followed bySimpleKey []
to fill in - Multiple parameters:
cacheNames::SimpleKey [arg1, arg2...]
- Non-base objects:
cacheNames::obj.toString()
2. Custom key generation strategy
If you want to use a custom key generation strategy, just inherit KeyGenerator
and declare it as a 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) + ")";
}
}
Then in the place of use, use the annotation keyGenerator
to specify the key generation strategy
/**
* 对应的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();
}
test case
@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;
}
The cache key is placed in the returned result keys
, and the output is as follows, as expected
{
"keys": [
"vv::ExtendDemo#selfKey([1])"
],
"self": "selfKey:1 --> f5f8aa2a-0823-42ee-99ec-2c40fb0b9338"
}
3. Cache invalidation time
All of the above caches do not have an expiration time set. In actual business scenarios, there are scenarios where the expiration time is not set; but more need to set a ttl. For Spring's cache annotations, there is no additional configuration for specifying ttl. If we wish to specify ttl, it can be RedisCacheManager
done by
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;
}
The above is a set RedisCacheConfiguration
method with two points
- Serialization method: use json to serialize the cached content
- Expiration time: Set the expiration time according to the parameters passed
If you want to customize the configuration for a specific key, you can do the following
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(8);
// 自定义设置缓存时间
// 这个k0 表示的是缓存注解中的 cacheNames/value
redisCacheConfigurationMap.put("k0", this.getRedisCacheConfigurationWithTtl(60 * 60));
return redisCacheConfigurationMap;
}
The last thing is to define what we needRedisCacheManager
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return new RedisCacheManager(
RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
// 默认策略,未配置的 key 会使用这个
this.getRedisCacheConfigurationWithTtl(60),
// 指定 key 策略
this.getRedisCacheConfigurationMap()
);
}
On the basis of the previous test case, add the information that returns 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;
}
The returned result is as follows, pay attention to the returned ttl expiration time
4. Custom Expiration Time Extension
Although the invalidation time can be specified above, it is still not very comfortable to use. Either the global setting is a unified invalidation time, or it is hardcoded in the code and the invalidation time is isolated from the place where the cache is defined, which is very unintuitive.
Next, we will introduce a case that sets the expiration time directly in the annotation.
Use case as below
/**
* 通过自定义的RedisCacheManager, 对value进行解析,=后面的表示失效时间
* @param key
* @return
*/
@Cacheable(value = "ttl=30")
public String ttl(String key) {
return "k_" + key;
}
The custom strategy is as follows:
- In value, the left side of the equal sign is the cacheName, and the right side of the equal sign is the invalidation time
To implement this logic, you can extend a custom RedisCacheManager
, such as
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);
}
}
Rewrite createRedisCache
the logic and parse the expiration time according to the name;
The registration and use method is the same as above, declared as a Spring bean object
@Primary
@Bean
public RedisCacheManager ttlCacheManager(RedisConnectionFactory redisConnectionFactory) {
return new TtlRedisCacheManager(RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory),
// 默认缓存配置
this.getRedisCacheConfigurationWithTtl(60));
}
The test case is as follows
@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;
}
The verification results are as follows
5. Summary
So far, I have basically introduced the common postures of caching annotations in Spring, whether it is the use case of several annotations, or a custom key strategy, and the specified expiration time. From the point of view of use, it can basically meet our needs. daily needs scenarios
The following is a knowledge point abstraction for cache annotations
Cache annotations
@Cacheable
: If the cache exists, take it from the cache; otherwise, execute the method and write the return result to the cache@CacheEvit
: invalidate cache@CachePut
: refresh cache@Caching
: both annotation combinations
Configuration parameters
cacheNames/value
: can be understood as a cache prefixkey
: Can be understood as a variable that caches the key, and supports SpEL expressionskeyGenerator
: key assembly strategycondition/unless
: Conditions for whether the cache is available or not
Default cache policy y
The following cacheNames is the cache prefix defined in the annotation, and the two semicolons are fixed
- Single parameter:
cacheNames::arg
- No parameters:
cacheNames::SimpleKey []
, followed bySimpleKey []
to fill in - Multiple parameters:
cacheNames::SimpleKey [arg1, arg2...]
- Non-base objects:
cacheNames::obj.toString()
cache invalidation time
Expiration time, this article introduces two ways, one is centralized configuration, and the RedisCacheConfiguration
ttl time is specified by setting
The other is an extension RedisCacheManager
class, which implements custom cacheNames
extension parsing
Spring cache annotation knowledge point comes to an end, I am a gray, welcome to pay attention to the public account of Changcao一灰灰blog
III. Source code and related knowledge points that cannot be missed
0. Project
Blog series
source code
- Engineering: https://github.com/liuyueyi/spring-boot-demo
- Source code: https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/
1. A gray blog
It is not as good as a letter. The above content is purely from the family. Due to limited personal ability, it is inevitable that there will be omissions and mistakes. If you find bugs or have better suggestions, you are welcome to criticize and correct them.
The following is a gray personal blog, recording all blog posts in study and work, welcome everyone to visit
- A Hui Hui Blog Personal blog https://blog.hhui.top
- A gray blog-Spring special blog http://spring.hhui.top