1. 程式人生 > >實現簡單的JAVA多級快取(Caffeine + redis)

實現簡單的JAVA多級快取(Caffeine + redis)

需求


好久沒寫文章啦,之前寫的文章到現在也沒有收尾,沒辦法,時間不多啊,舊坑沒有填完就開始開新坑,最近專案組長說實現一個多級快取,通常我們喜歡把cache放到redis裡,可以把訪問速度提升,但是redis也算是遠端伺服器,會有IO時間的開銷,如果我們把快取放在本地記憶體,效能能進一步提升,這也就帶出了二級快取概念。有人說為什麼不把cache直接放到本地,如果是單機沒問題,但是叢集環境下還是需要兩級快取的配合。

快取的獲取與更新


在這裡插入圖片描述

隨便畫的,簡單來說,工具先從 一級快取取起,也就是本地快取,如果快取命中,就可以直接返回;如果一級快取沒有,就會去redis找,再不行就走傳統業務邏輯。這種快取比單一的快取工具比起來具有以下特點:

  • 適應叢集環境
  • 比單一Redis快取效能更高
  • 設計了三級資料層(包括業務直接取資料)分攤了請求量,降低資料庫壓力

但跟所有快取框架一樣,快取只適合非關鍵資料,因為快取更新多少具有延遲性。

快取的更新比獲取更復雜一點,它存在多種情況:

  • 當一級快取失效時(獲取不到),得益於Caffeine本身提供的功能,你能指定方法去redis獲取並更新到一級快取中。
  • 當業務資料發生改變,呼叫delete方法直接清除一/二級快取。(這種方法現在比較暴力,後期可以完善)
  • 當新建快取時,先Redis 存入快取,再通過Redis 的訊息訂閱機制 讓本地每臺機器接收最新的cache。

程式碼實現


環境的話比較通用的 spring + redission + Caffeine

  1. redission 要先在spring配置一下,裡面的配置檔案根據實際情況自己生成填入:
    @Bean
    RedissonClient redissonClient() {
        Config config = new Config();
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress("redis://" + redissonProperties.getHost() + ":" + redissonProperties.getPort())
                .setTimeout(redissonProperties.getTimeout())
                .setConnectionPoolSize(redissonProperties.getConnectionPoolSize())
                .setConnectionMinimumIdleSize(redissonProperties.getConnectionMinimumIdleSize());

        if (StringUtils.isNotBlank(redissonProperties.getPassword())) {
            serverConfig.setPassword(redissonProperties.getPassword());
        }
        return Redisson.create(config);
    }
  1. 快取工具類
@Component
@Slf4j
public class SecondLevelCacheUtil {
//為避免key衝突,key的設定應該規範
    public static final String BRAINTRAIN = "braintrain:";
    public static final String REDIS_TOPIC_PUT = BRAINTRAIN + "putTopic:";
    public static final String REDIS_TOPIC_DELETE = BRAINTRAIN + "deleteTopic:";

    @Autowired
    CustomsProperties customsProperties;

    @Autowired
    RedissonClient redissonClient;

    private Cache<String, String> cache;

    @PostConstruct
    void init() {
        log.info("SecondLevelCacheUtil init");
        cache = Caffeine.newBuilder()
                .expireAfterWrite(customsProperties.getRedisFirstCacheTime(), TimeUnit.MILLISECONDS)
                .build();
// 監聽刪除快取事件,及時清除本地一級快取
        RTopic<String> deleteTopic = redissonClient.getTopic(REDIS_TOPIC_DELETE + customsProperties.getAppName());
        deleteTopic.addListener((channel, message) -> {
            log.info("first cache delete {}", message);
            cache.invalidate(message);
        });
// 監聽新增快取事件,及時新增本地一級快取
        RTopic<String> putTopic = redissonClient.getTopic(REDIS_TOPIC_PUT + customsProperties.getAppName());
        putTopic.addListener((channel, message) -> {
            if (StringUtils.isNotBlank(message)) {
                log.info("first cache put {}", message);
                String[] split = message.split("\\|\\|");
                cache.put(split[0], split[1]);
            }
        });
        log.info("SecondLevelCacheUtil done");
    }


    public <T> T get(String key, Class<T> clazz) {
        try {
            if (StringUtils.isBlank(key) || !key.startsWith(BRAINTRAIN)) {
                return null;
            }
            //一級快取取不到時,呼叫getByRedis()取二級快取,由Caffeine原生提供機制
            String json = cache.get(key, k -> getByRedis(k));
            if (StringUtils.isNotBlank(json)) {
                return JSON.parseObject(json, clazz);
            }
        } catch (Exception e) {
            log.warn("SecondLevelCacheUtil get e={}", e);
        }
        return null;
    }

    public void delete(String key) {
        try {
            if (StringUtils.isBlank(key) || !key.startsWith(BRAINTRAIN)) {
                return;
            }
            RBucket<Object> bucket = redissonClient.getBucket(key);
            bucket.deleteAsync();
            // 分發"刪除"主題,讓本地一級快取接收通知
            RTopic<String> topic = redissonClient.getTopic(REDIS_TOPIC_DELETE + customsProperties.getAppName());
            long clientsReceivedMessage = topic.publish(key);
            log.info("delete first/second cache ,key{}, {}個例項接收到資訊", key, clientsReceivedMessage);
        } catch (Exception e) {
            log.warn("SecondLevelCacheUtil delete e={}", e);
        }
    }

    public void set(String key, Object value) {
        try {
            if (StringUtils.isNotBlank(key) && !Objects.isNull(value)) {
                if (!key.startsWith(BRAINTRAIN)) {
                    return;
                }
                RBucket<String> bucket = redissonClient.getBucket(key);
                String valueStr = JSONObject.toJSONString(value);
                bucket.setAsync(valueStr, customsProperties.getRedisSecondCacheTime(), TimeUnit.MILLISECONDS);
                RTopic<String> topic = redissonClient.getTopic(REDIS_TOPIC_PUT + customsProperties.getAppName());
                   // 分發"新增"主題,讓本地一級快取接收通知
                long clientsReceivedMessage = topic.publish(key + "||" + valueStr);
                log.info("after set , key: {} value: {}, {}個例項接收到資訊", key, valueStr, clientsReceivedMessage);
            }
        } catch (Exception e) {
            log.warn("SecondLevelCacheUtil set e={}", e);
        }
    }

    public void setIfAbsent(String key, Object value, Class clazz) {
        if (null == get(key, clazz)) {
            set(key, value);
        }
    }

//取二級快取的方法
    private String getByRedis(String key) {
        try {
            log.info("快取不存在或過期,呼叫了redis獲取快取key的值");
            if (StringUtils.isNotBlank(key)) {
                RBucket<String> bucket = redissonClient.getBucket(key);
                String result = bucket.get();
                RTopic<String> topic = redissonClient.getTopic(REDIS_TOPIC_PUT + customsProperties.getAppName());
                long clientsReceivedMessage = topic.publish(key + "||" + result);
                log.info("first cache null, key: {} value: {}, {}個例項接收到資訊", key, result, clientsReceivedMessage);
                return result;
            }
        } catch (Exception e) {
            log.warn("SecondLevelCacheUtil getByRedis e={}", e);
        }
        return null;
    }


}

3.部分設定項

@Component
@Data
public class CustomsProperties {
//一級快取失效時間
    @Value("${braintrain.redisFirstCacheTime: 180000}")
    Long redisFirstCacheTime;
//二級快取失效時間
    @Value("${braintrain.redisSecondCacheTime: 2592000000}")
    Long redisSecondCacheTime;
}

不足與改進

  1. 因為剛開始寫,這個工具類能滿足基本的二級快取需求,但其實改進的地方還有很多,比如根據實際情況利用Caffeine本身的淘汰策略進行cache更新與刪除,而不是直接設定失效時間,但這種改進要考慮一/二級快取的一致性,以免快取出現問題
  2. 應用重啟後一級快取處於全部失效狀態,如果全部從redis取會有讀取壓力;現有一級快取也是被動接收新cache,一級快取的命中率較低,這裡可以考慮redis的空間訊息通知。