本稿では、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メソッドがデータ更新時にキャッシュをクリアして整合性を保ちます。