SpringBoot cache annotation @Cacheable custom key strategy and cache expiration time specification

The previous blog post introduced the @Cacheable @CacheEvit @CachePutbasic 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 @Cacheableannotations, 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 by SimpleKey []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 KeyGeneratorand 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 keyGeneratorto 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 RedisCacheManagerdone 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 RedisCacheConfigurationmethod 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 createRedisCachethe 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 prefix
  • key: Can be understood as a variable that caches the key, and supports SpEL expressions
  • keyGenerator: key assembly strategy
  • condition/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 by SimpleKey []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 RedisCacheConfigurationttl time is specified by setting

The other is an extension RedisCacheManagerclass, which implements custom cacheNamesextension 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

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 grey blog

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324114065&siteId=291194637