SpringBoot缓存注解@Cacheable之自定义key策略及缓存失效时间指定

本文涉及的产品
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Redis 版,经济版 1GB 1个月
简介: 上一篇博文介绍了Spring中缓存注解@Cacheable @CacheEvit @CachePut的基本使用,接下来我们将看一下更高级一点的知识点 (Spring系列缓存注解@Cacheable @CacheEvit @CachePut 使用姿势介绍)

image.png


上一篇博文介绍了Spring中缓存注解@Cacheable@CacheEvit@CachePut的基本使用,接下来我们将看一下更高级一点的知识点 (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失效时间


image.png


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;
}
复制代码


验证结果如下

image.png


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缓存注解知识点到此告一段落,我是一灰灰


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3天前
|
Java 数据安全/隐私保护 Spring
揭秘Spring Boot自定义注解的魔法:三个实用场景让你的代码更加优雅高效
揭秘Spring Boot自定义注解的魔法:三个实用场景让你的代码更加优雅高效
|
1月前
|
canal 缓存 NoSQL
Redis常见面试题(一):Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;先删除缓存还是先修改数据库,双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
Redis常见面试题(一):Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
|
24天前
|
缓存 Python
在Python中,`functools`模块提供了一个非常有用的装饰器`lru_cache()`,它实现了最近最少使用(Least Recently Used, LRU)缓存策略。
在Python中,`functools`模块提供了一个非常有用的装饰器`lru_cache()`,它实现了最近最少使用(Least Recently Used, LRU)缓存策略。
|
23天前
|
Java Spring
springBoot 使用 @NotEmpty,@NotBlank,@NotNull 及@Valid注解校验请求参数
springBoot 使用 @NotEmpty,@NotBlank,@NotNull 及@Valid注解校验请求参数
41 7
|
3天前
|
设计模式 Java 测试技术
公司为何禁止在SpringBoot中使用@Autowired注解?
【8月更文挑战第15天】在Spring Boot的广泛应用中,@Autowired注解作为依赖注入的核心机制之一,极大地简化了Bean之间的装配过程。然而,在某些企业环境下,我们可能会遇到公司政策明确禁止或限制使用@Autowired注解的情况。这一决策背后,往往蕴含着对代码质量、可维护性、测试便利性以及团队开发效率等多方面的考量。以下将从几个方面深入探讨这一决定的合理性及替代方案。
9 0
|
4天前
|
存储 缓存 Java
Java本地高性能缓存实践问题之使用@CachePut注解来更新缓存中的数据的问题如何解决
Java本地高性能缓存实践问题之使用@CachePut注解来更新缓存中的数据的问题如何解决
|
29天前
|
存储 缓存 分布式计算
高并发架构设计三大利器:缓存、限流和降级问题之缓存的应对策略问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之缓存的应对策略问题如何解决
|
1月前
|
存储 设计模式 缓存
探索微服务架构下的缓存策略
【7月更文挑战第18天】在微服务架构的海洋中,缓存策略如同指南针,指引着系统性能的优化方向。本文将深入探讨微服务环境下缓存的有效应用,从缓存的基本概念出发,到微服务架构中缓存的特殊需求,再到实际案例分析,最后讨论缓存一致性与失效策略,旨在为开发者提供一套完整的缓存解决方案框架。
|
11天前
|
缓存 监控 Go
[go 面试] 缓存策略与应对数据库压力的良方
[go 面试] 缓存策略与应对数据库压力的良方
|
1月前
|
SQL Java 调度
实时计算 Flink版产品使用问题之使用Spring Boot启动Flink处理任务时,使用Spring Boot的@Scheduled注解进行定时任务调度,出现内存占用过高,该怎么办
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。