jetcache快取使用

QiaoZhi發表於2024-08-12

1、簡介

​ jetcache 是阿里開源的一個快取框架,支援像guava的cache、caffeine的Cache 用法,也可以整合springboot,像spring的@Cache 註解一樣進行使用。

​ jetcache的快取類似於map,提供了get、put、putAll、computeIfAbsent等方法, 另外還提供了單機鎖、分散式鎖機制,一般也不用這個做鎖就先不研究了。

2、簡單使用

1、直接cache 用法

1、pom

<!-- https://mvnrepository.com/artifact/com.alicp.jetcache/jetcache-core -->
        <dependency>
            <groupId>com.alicp.jetcache</groupId>
            <artifactId>jetcache-core</artifactId>
            <version>2.6.7</version>
        </dependency>

2、簡單用法

1》建立快取的操作類似guava/caffeine cache,例如下面的程式碼建立基於記憶體的LinkedHashMapCache

package qz.jetcache;

import com.alicp.jetcache.Cache;
import com.alicp.jetcache.CacheConfig;
import com.alicp.jetcache.embedded.EmbeddedCacheConfig;
import com.alicp.jetcache.embedded.LinkedHashMapCacheBuilder;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class CacheClient {

    public static void main(String[] args) throws InterruptedException {
        Cache<String, String> cache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
                .limit(20)
                .expireAfterWrite(10, TimeUnit.SECONDS)
                .buildCache();

        // 每個cache 都有個配置,包括快取的大小、過期時間、重新整理時間、重新整理載入開關、重新整理載入執行緒池、重新整理載入策略、key/value轉換器等
        CacheConfig<String, String> config = cache.config();

        // 第三個參數列示是否要快取null值
        log.info(cache.computeIfAbsent("1", k -> doGetFromDB(k), true));
        log.info(cache.computeIfAbsent("1", k -> doGetFromDB(k), true));
        log.info(cache.computeIfAbsent("2", k -> doGetFromDB(k), true));
        log.info(cache.computeIfAbsent("2", k -> doGetFromDB(k), true));
        log.info(cache.computeIfAbsent("3", k -> doGetFromDB(k), false));
        log.info(cache.computeIfAbsent("3", k -> doGetFromDB(k), false));
        /**
         * 2024-08-01 16:35:36 [main] [qz.jetcache.CacheClient]-[INFO] doGetFromDB, key: 1
         * 2024-08-01 16:35:36 [main] [qz.jetcache.CacheClient]-[INFO] 1_1
         * 2024-08-01 16:35:36 [main] [qz.jetcache.CacheClient]-[INFO] 1_1
         * 2024-08-01 16:35:36 [main] [qz.jetcache.CacheClient]-[INFO] doGetFromDB, key: 2
         * 2024-08-01 16:35:36 [main] [qz.jetcache.CacheClient]-[INFO]
         * 2024-08-01 16:35:36 [main] [qz.jetcache.CacheClient]-[INFO]
         * 2024-08-01 16:35:36 [main] [qz.jetcache.CacheClient]-[INFO] doGetFromDB, key: 3
         * 2024-08-01 16:35:36 [main] [qz.jetcache.CacheClient]-[INFO]
         * 2024-08-01 16:35:36 [main] [qz.jetcache.CacheClient]-[INFO] doGetFromDB, key: 3
         * 2024-08-01 16:35:36 [main] [qz.jetcache.CacheClient]-[INFO]
         */
    }

    private static String doGetFromDB(String k) {
        log.info("doGetFromDB, key: {}", k);
        if ("1".equals(k)) {
            return "1_1";
        }

        return null;
    }
}

2》也能用caffeine, jetcache 做了整合

        Cache<String, String> cache = CaffeineCacheBuilder.createCaffeineCacheBuilder()
                .limit(20)
                .buildCache();

2、整合springboot 使用

可以作為本地快取使用,也可以作為分散式快取使用,也可以本地和遠端同時寫。

1、pom依賴

				<dependency>
            <groupId>com.alicp.jetcache</groupId>
            <artifactId>jetcache-starter-redis-lettuce</artifactId>
            <version>2.6.0</version>
        </dependency>

2、增加配置類

package cn.qz.cloud.alibaba.config;

import com.alicp.jetcache.CacheBuilder;
import com.alicp.jetcache.RefreshPolicy;
import com.alicp.jetcache.anno.CacheConsts;
import com.alicp.jetcache.anno.support.GlobalCacheConfig;
import com.alicp.jetcache.anno.support.SpringConfigProvider;
import com.alicp.jetcache.embedded.EmbeddedCacheBuilder;
import com.alicp.jetcache.embedded.LinkedHashMapCacheBuilder;
import com.alicp.jetcache.redis.lettuce.RedisLettuceCacheBuilder;
import com.alicp.jetcache.support.FastjsonKeyConvertor;
import com.alicp.jetcache.support.JetCacheExecutor;
import com.alicp.jetcache.support.KryoValueDecoder;
import com.alicp.jetcache.support.KryoValueEncoder;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

@Configuration
@Slf4j
public class JetcacheConfig {

    @Bean
    public RedisClient redisClient() {
        // 建立 RedisClient
        RedisClient redisClient = RedisClient.create(RedisURI.create("redis://127.0.0.1:6379"));
        return redisClient;
    }

    @Bean
    public SpringConfigProvider springConfigProvider() {
        return new SpringConfigProvider();
    }

    @Bean
    public GlobalCacheConfig config(RedisClient redisClient, @Qualifier("cache-reader") ScheduledExecutorService reader, @Qualifier("cache-refresh") ScheduledExecutorService refresh) {
        JetCacheExecutor.setDefaultExecutor(reader);
        JetCacheExecutor.setHeavyIOExecutor(refresh);
        Map<String, CacheBuilder> localBuilders = new HashMap<>();
        EmbeddedCacheBuilder localBuilder = LinkedHashMapCacheBuilder
                .createLinkedHashMapCacheBuilder()
                .keyConvertor(FastjsonKeyConvertor.INSTANCE)
                .limit(100)
                .expireAfterWrite(30, TimeUnit.SECONDS);
        localBuilders.put(CacheConsts.DEFAULT_AREA, localBuilder);

        Map<String, CacheBuilder> remoteBuilders = new HashMap<>();
        RefreshPolicy refreshPolicy = RefreshPolicy.newPolicy(1, TimeUnit.HOURS);
        RedisLettuceCacheBuilder remoteCacheBuilder = RedisLettuceCacheBuilder.createRedisLettuceCacheBuilder()
                .keyConvertor(FastjsonKeyConvertor.INSTANCE)
                .valueEncoder(KryoValueEncoder.INSTANCE)
                .valueDecoder(KryoValueDecoder.INSTANCE)
                .refreshPolicy(refreshPolicy)
                .expireAfterWrite(10, TimeUnit.SECONDS)
                .redisClient(redisClient);
        remoteBuilders.put(CacheConsts.DEFAULT_AREA, remoteCacheBuilder);

        GlobalCacheConfig globalCacheConfig = new GlobalCacheConfig();
        globalCacheConfig.setLocalCacheBuilders(localBuilders);
        globalCacheConfig.setRemoteCacheBuilders(remoteBuilders);
        globalCacheConfig.setStatIntervalMinutes(15);
        globalCacheConfig.setAreaInCacheName(false);

        return globalCacheConfig;
    }

    @Bean("cache-reader")
    public ScheduledExecutorService cacheReaderExecutor() {
        log.info("cacheReaderExecutor");
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("cache-reader-thread-pool-%d").setDaemon(true).build();
        return new ScheduledThreadPoolExecutor(10, threadFactory, (r, executor) -> {
            log.info("cacheReaderExecutor executorActiveCount={} Task={} rejected from{}", executor.getActiveCount(), r.toString(), executor.toString());
        });
    }

    @Bean("cache-refresh")
    public ScheduledExecutorService cacheRefreshExecutor() {
        log.info("cacheRefreshExecutor");
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("cache-refresh-thread-pool-%d").setDaemon(true).build();
        return new ScheduledThreadPoolExecutor(10, threadFactory, (r, executor) -> {
            log.info("cacheRefreshExecutor executorActiveCount={} Task={} rejected from{}", executor.getActiveCount(), r.toString(), executor.toString());
        });
    }
}

3、測試類:

package cn.qz.cloud.alibaba.controller;

import cn.qz.cloud.alibaba.bean.User;
import com.alicp.jetcache.anno.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@RequestMapping("/user")
@RestController
@Slf4j
public class UserController {

    private static final String cacheKeyPrefix = "user:cache:";

    private Map<String, User> userMap = new HashMap() {
        {
            put("qz", new User("qz", 18));
            put("zs", new User("zs", 18));
            put("ww", new User("ww", 18));
        }
    };

    /**
     * 檢視所有,預設是存在遠端快取remote 種
     *
     * @return
     */
    @RequestMapping("/lists")
    @Cached(name = cacheKeyPrefix + "listUser", expire = 60, timeUnit = TimeUnit.MINUTES)
    public List<User> listUser() {
        log.info("獲取使用者集合listUser");
        return new ArrayList<>(userMap.values());
    }

    /**
     * 快取到本地
     *
     * @return
     */
    @RequestMapping("/lists2")
    @Cached(name = cacheKeyPrefix + "listUser2", expire = 60, timeUnit = TimeUnit.MINUTES, cacheType = CacheType.LOCAL)
    public List<User> listUser2() {
        log.info("獲取使用者集合listUser2");
        return new ArrayList<>(userMap.values());
    }

    @RequestMapping("/find")
    @CachePenetrationProtect // 防止快取擊穿,透過在多個併發請求中只有一個請求去載入資料,其他請求等待載入完成後直接從快取獲取 (引數相同的時候只有一個執行緒能獲取到)
    @Cached(name = cacheKeyPrefix + "find:", key = "#name", expire = 120, timeUnit = TimeUnit.SECONDS, postCondition = "T(cn.qz.cloud.alibaba.config.CacheCondition).checkNeedCache(#result)")
    // cacheNullValue = true 標識是否結果為null 的情況
    @CacheRefresh(refresh = 60, stopRefreshAfterLastAccess = 300, timeUnit = TimeUnit.SECONDS)
    //60秒重新整理一次,如果300秒內沒有訪問,則停止重新整理。重新整理機制: 框架自己非同步呼叫該方法進行重新整理快取
    public User find(@RequestParam String name) throws InterruptedException {
        log.info("查詢使用者, name: {}", name);
        Thread.sleep(2 * 1000);
        return userMap.get(name);
    }

    /**
     * 清除多個快取,拼接固定的key和帶引數的使用者詳情
     *
     * @param name
     */
    @RequestMapping("/del")
    @CacheInvalidate(name = cacheKeyPrefix + "listUser", key = "'[]'")
    @CacheInvalidate(name = cacheKeyPrefix + "find:", key = "#name")
    @CacheInvalidate(name = cacheKeyPrefix + "listUser2", key = "'[]'")
    public void del(@RequestParam String name) {
        log.info("清除快取");
    }

}

4、工具類

package cn.qz.cloud.alibaba.config;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CacheCondition {

    /**
     * 檢查是否需要快取
     * <p>
     * 可以根據返回的結果進行判斷
     *
     * @param object
     * @return
     */
    public static boolean checkNeedCache(Object object) {
        log.info("checkNeedCache: {}", object);
        if (object == null) {
            return false;
        }
        return true;
    }
}

5、相關注解解釋:

1、@Cached(name = cacheKeyPrefix + "find:", key = "#name", expire = 120, timeUnit = TimeUnit.SECONDS, postCondition = "T(cn.qz.cloud.alibaba.config.CacheCondition).checkNeedCache(#result)")
快取註解,可以設定快取的key(可以加引數)、時間、單位、postCondition 指定檢查的方法,可以設定是否對結果進行快取
2、@CachePenetrationProtect // 防止快取擊穿,透過在多個併發請求中只有一個請求去載入資料,其他請求等待載入完成後直接從快取獲取 (引數相同的時候只有一個執行緒能獲取到)
3、@CacheRefresh(refresh = 60, stopRefreshAfterLastAccess = 300, timeUnit = TimeUnit.SECONDS)
	60秒重新整理一次,如果300秒內沒有訪問,則停止重新整理。重新整理機制: 框架自己非同步呼叫該方法進行重新整理快取
	cacheType為REMOTE或者BOTH的時候,重新整理行為是全域性唯一的,也就是說,即使應用伺服器是一個叢集,也不會出現多個伺服器同時去重新整理一個key的情況。
	一個key的重新整理任務,自該key首次被訪問後初始化,如果該key長時間不被訪問,在stopRefreshAfterLastAccess指定的時間後,相關的重新整理任務就會被自動移除,這樣就避免了浪費資源去進行沒有意義的重新整理。
4、CacheInvalidate
清除快取,拼接固定的key和帶引數的使用者詳情


rediskey的結構:
1) "user:cache:listUser[]"
2) "user:cache:find:zs"

相關文章