SpringBoot緩存註解@Cacheable之自定義key策略及緩存失效時間指定

上一篇博文介紹了Spring中緩存註解@Cacheable @CacheEvit @CachePut的基本使用,接下來咱們將看一下更高級一點的知識點java

  • key生成策略
  • 超時時間指定

<!-- more -->git

I. 項目環境

1. 項目依賴

本項目藉助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA + redis5.0進行開發github

開一個web服務用於測試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註解,有兩個參數用於組裝緩存的keyredis

  • cacheNames/value: 相似於緩存前綴
  • key: SpEL表達式,一般根據傳參來生成最終的緩存key

默認的redisKey = cacheNames::key (注意中間的兩個冒號)spring

json

/**
 * 沒有指定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緩存

若是沒有參數,或者多個參數呢?app

/**
 * 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;
}

而後寫一個測試caseless

@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. 項目

系列博文

源碼

1. 一灰灰Blog

盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛

一灰灰blog

相關文章
相關標籤/搜索