相关推荐recommended
SpringBoot缓存注解@Cacheable之自定义key策略及缓存失效时间指定原创
作者:mmseoamin日期:2024-02-20

SpringBoot缓存注解@Cacheable之自定义key策略及缓存失效时间指定原创,第1张

I. 项目环境

1. 项目依赖

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA + redis5.0进行开发

开一个 web 服务用于测试


    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.boot
        spring-boot-starter-data-redis
    

II. 扩展知识点

1. key 生成策略

对于@Cacheable注解,有两个参数用于组装缓存的 key

  • cacheNames/value: 类似于缓存前缀

  • key: SpEL 表达式,通常根据传参来生成最终的缓存 key

    默认的redisKey = cacheNames::key (注意中间的两个冒号)

    /**
     * 没有指定key时,采用默认策略 {@link org.springframework.cache.interceptor.SimpleKeyGenerator } 生成key
     * 

     * 对应的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 key(int id) {
            Map 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 keys = (Set) redisTemplate.execute((RedisCallback>) connection -> {
                Set sets = connection.keys("k*".getBytes());
                Set 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 self(int id) {
          Map res = new HashMap<>();
          res.put("self", extendDemo.selfKey(id));
          Set keys = (Set) redisTemplate.execute((RedisCallback>) connection -> {
              Set sets = connection.keys("vv*".getBytes());
              Set 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 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 getRedisCacheConfigurationMap() {
            Map 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 key(int id) {
            Map 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 keys = (Set) redisTemplate.execute((RedisCallback>) connection -> {
                Set sets = connection.keys("k*".getBytes());
                Set ans = new HashSet<>();
                for (byte[] b : sets) {
                    ans.add(new String(b));
                }
                return ans;
            });
            res.put("keys", keys);
            Map ttl = new HashMap<>(8);
            for (String key : keys) {
                ttl.put(key, getTtl(key));
            }
            res.put("ttl", ttl);
            return res;
        }

        返回结果如下,注意返回的 ttl 失效时间

        SpringBoot缓存注解@Cacheable之自定义key策略及缓存失效时间指定原创,第2张 

        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 res = new HashMap<>();
              res.put("execute", extendDemo.ttl(k));
              res.put("ttl", getTtl("ttl::" + k));
              return res;
          }

          SpringBoot缓存注解@Cacheable之自定义key策略及缓存失效时间指定原创,第3张

          5. 小结

          到此基本上将 Spring 中缓存注解的常用姿势都介绍了一下,无论是几个注解的使用 case,还是自定义的 key 策略,失效时间指定,单纯从使用的角度来看,基本能满足我们的日常需求场景

          下面是针对缓存注解的一个知识点抽象

          缓存注解