Guava + Redis 二段階キャッシュユーティリティの実装

本稿では、GuavaローカルキャッシュとRedis分散キャッシュを組み合わせた二段階キャッシュユーティリティクラスを実装します。この設計は、データ読み取り頻度が高く書き込み頻度が低いシナリオに適しており、リスト型データを効率的にキャッシュするための汎用的なソリューションを提供します。

package com.example.cache.util;

import com.alibaba.fastjson.JSON;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Component
public class TwoTierCacheUtil<K, V> {

    private final Cache<String, String> localCache;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    public TwoTierCacheUtil() {
        this.localCache = CacheBuilder.newBuilder()
                .maximumSize(10000)
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .recordStats()
                .build();
    }

    public List<V> fetchTwoTierCache(String cacheKey, Class<V> clazz,
                                      Function<String, List<V>> dataLoader, long redisTtl) {
        List<V> data = loadFromLocal(cacheKey, clazz);
        if (!CollectionUtils.isEmpty(data)) {
            return data;
        }

        data = loadFromRedis(cacheKey, clazz);
        if (!CollectionUtils.isEmpty(data)) {
            localCache.put(cacheKey, JSON.toJSONString(data));
            return data;
        }

        data = dataLoader.apply(cacheKey);
        if (!CollectionUtils.isEmpty(data)) {
            updateRedis(cacheKey, data, redisTtl);
            localCache.put(cacheKey, JSON.toJSONString(data));
        }
        return data;
    }

    private List<V> loadFromLocal(String cacheKey, Class<V> clazz) {
        String cachedValue = localCache.getIfPresent(cacheKey);
        if (StringUtils.isNotBlank(cachedValue)) {
            return JSON.parseArray(cachedValue, clazz);
        }
        return new ArrayList<>();
    }

    private List<V> loadFromRedis(String cacheKey, Class<V> clazz) {
        Object cachedValue = redisTemplate.opsForValue().get(cacheKey);
        if (cachedValue != null) {
            String jsonStr = cachedValue instanceof String ?
                    (String) cachedValue : JSON.toJSONString(cachedValue);
            return JSON.parseArray(jsonStr, clazz);
        }
        return new ArrayList<>();
    }

    private void updateRedis(String cacheKey, List<V> data, long ttl) {
        if (CollectionUtils.isEmpty(data) || ttl < 0) {
            return;
        }
        redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(data), ttl, TimeUnit.SECONDS);
    }

    public void evictCache(String cacheKey) {
        localCache.invalidate(cacheKey);
        redisTemplate.delete(cacheKey);
    }

    public String displayCacheStats() {
        return localCache.stats().toString();
    }
}

1. アーキテクチャ概要

本ユーティリティは、二層構造のキャッシュシステムを採用しています。第一層はGuavaベースのローカルキャッシュで、アプリケーションのヒープメモリ内に存在し、マイクロ秒単位の高速アクセスを提供します。第二層はRedisを利用した分散キャッシュで、複数のサービスインスタンス間でデータを共有可能にし、より大容量のデータ保存を実現します。

2. 主要メソッドの動作フロー

中心メソッドであるfetchTwoTierCacheは、以下の三段階の探索を行います:

  • まず、Guavaローカルキャッシュからキーに対応するデータを検索します。
  • 見つからない場合、Redisキャッシュを確認します。
  • 両方で見つからない場合のみ、dataLoader関数を実行してデータソース(例:データベース)からデータを取得し、両方のキャッシュに格納します。

この設計により、キャッシュのヒット率を最大化し、データソースへの負荷を最小限に抑えます。

3. キャッシュの無効化と監視

evictCacheメソッドは、データ更新があった場合に両方のキャッシュをクリアし、古いデータが読み取られるのを防ぎます。displayCacheStatsメソッドは、ローカルキャッシュのヒット率やミス率などの統計情報を提供し、パフォーマンスの監視と最適化に役立ちます。

4. 利用例:商品リストのキャッシュ

@Service
public class ProductService {

    @Autowired
    private TwoTierCacheUtil<String, Product> cacheUtil;
    @Autowired
    private ProductRepository productRepo;

    public List<Product> fetchProductsByCategory(Long categoryId) {
        String key = "product:category:" + categoryId;
        return cacheUtil.fetchTwoTierCache(
            key,
            Product.class,
            k -> productRepo.findByCategoryId(categoryId),
            3600
        );
    }

    public void updateProduct(Product product) {
        productRepo.save(product);
        cacheUtil.evictCache("product:category:" + product.getCategoryId());
    }
}

この例では、fetchProductsByCategoryメソッドがキャッシュを介して商品リストを取得し、updateProductメソッドがデータ更新時にキャッシュをクリアして整合性を保ちます。

タグ: Guava redis キャッシュ Spring Boot Java

5月26日 03:28 投稿