在现代应用中,缓存是提升性能和降低外部系统压力的关键组件。Google 提供的 Guava 是一款强大的 Java 工具库,其中的 Guava Cache 模块提供了灵活的本地缓存功能。
本文将通过封装 Guava Cache,实现一个通用的缓存模板,帮助开发者快速构建可复用的缓存逻辑。
Guava Cache 功能强大,但如果每次使用都需要手动配置,可能会显得繁琐。通过封装一个抽象类,我们可以将常用的配置和逻辑提取出来,让子类只需实现数据加载逻辑即可,简化代码,提高复用性。
以下是优化后的 GuavaAbstractLoadingCache 实现:
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 通用的 Guava 缓存模板。
* 子类只需实现 fetchData(key) 方法,从外部数据源获取数据。
* 调用 getValue(key) 即可从缓存中获取数据,缓存失效时会自动加载。
*
* @param <K> 缓存键类型(必须实现 Serializable)
* @param <V> 缓存值类型
*/
public abstract class GuavaAbstractLoadingCache<K extends Serializable, V> {
private static final Logger logger = Logger.getLogger(GuavaAbstractLoadingCache.class.getName());
private final int maximumSize;
private final int expireAfterWriteDuration;
private final TimeUnit timeUnit;
private volatile LoadingCache<K, V> cache;
/**
* 默认构造函数,使用默认配置。
*/
protected GuavaAbstractLoadingCache() {
this(1000, 60, TimeUnit.MINUTES);
}
/**
* 自定义配置构造函数。
*
* @param maximumSize 最大缓存条数
* @param expireAfterWriteDuration 缓存过期时长
* @param timeUnit 时间单位
*/
protected GuavaAbstractLoadingCache(int maximumSize, int expireAfterWriteDuration, TimeUnit timeUnit) {
this.maximumSize = maximumSize;
this.expireAfterWriteDuration = expireAfterWriteDuration;
this.timeUnit = timeUnit;
}
/**
* 获取缓存实例。如果缓存未初始化,则进行初始化。
*
* @return 缓存实例
*/
private LoadingCache<K, V> getCache() {
if (cache == null) {
synchronized (this) {
if (cache == null) {
cache = CacheBuilder.newBuilder()
.maximumSize(maximumSize)
.expireAfterWrite(expireAfterWriteDuration, timeUnit)
.build(new CacheLoader<K, V>() {
@Override
public V load(K key) throws Exception {
return fetchData(key);
}
});
logger.info("缓存初始化成功:最大大小 = " + maximumSize + ", 过期时长 = " + expireAfterWriteDuration + " " + timeUnit); }
}
}
return cache;
}
/**
* 子类实现:从外部数据源获取数据。
*
* @param key 缓存键
* @return 数据值
* @throws Exception 如果获取数据失败
*/
protected abstract V fetchData(K key) throws Exception;
/**
* 从缓存中获取数据。若缓存失效或不存在,会调用 fetchData 自动加载。
*
* @param key 缓存键
* @return 数据值
*/
public V getValue(K key) {
try {
return getCache().get(key);
} catch (ExecutionException e) {
logger.log(Level.SEVERE, MessageFormat.format("缓存加载失败, key: {0}", key), e);
throw new RuntimeException("缓存加载失败", e);
}
}
/**
* 清空缓存并重新初始化。
*/
public synchronized void resetCache() {
if (cache != null) {
cache.invalidateAll();
cache = null;
logger.info("缓存已清空并重置");
}
}
}highestSize 和 highestTime。getValue 方法中,避免调用方处理过多异常。机制 | 作用 | 是否防多次查 DB |
|---|---|---|
synchronized的 DCL | 确保 | ❌ 否 |
Guava 的 LoadingCache.get(key) | Guava 内部会对 每个 key 维护一个状态(类似 Map<K, Future<V>>):第一个线程调用 get(key) → 发现没有缓存 → 启动一个 FutureTask 执行 load(key)(即你的 fetchData)→ 查 MySQL。后续线程调用 get(key) → 发现已有 Future 在执行 → 直接等待这个 Future 的结果,不会启动新的 load。 | ✅ 是 |
通过继承 GuavaAbstractLoadingCache,我们可以快速实现具体的缓存逻辑。以下是一个简单的用户信息缓存示例。
import java.util.HashMap;
import java.util.Map;
/**
* 示例子类:实现一个用户信息缓存。
*/
public class UserCache extends GuavaAbstractLoadingCache<String, String> {
private static final Map<String, String> DATABASE = new HashMap<>();
static {
DATABASE.put("1", "Alice");
DATABASE.put("2", "Bob");
DATABASE.put("3", "Charlie");
}
@Override
protected String fetchData(String key) {
System.out.println("从外部数据源加载数据,key: " + key);
return DATABASE.getOrDefault(key, "Unknown User");
}
}public class CacheExample {
public static void main(String[] args) {
// 创建缓存实例
UserCache userCache = new UserCache();
// 测试获取数据
System.out.println(userCache.getValue("1")); // 输出:Alice
System.out.println(userCache.getValue("2")); // 输出:Bob
System.out.println(userCache.getValue("4")); // 输出:Unknown User
// 测试缓存功能(第二次获取不需要从数据源加载)
System.out.println(userCache.getValue("1")); // 输出:Alice (缓存命中)
// 清空缓存并重新获取数据
userCache.resetCache();
System.out.println(userCache.getValue("1")); // 输出:Alice (重新加载)
}
}从外部数据源加载数据,key: 1
Alice
从外部数据源加载数据,key: 2
Bob
从外部数据源加载数据,key: 4
Unknown User
Alice
缓存已清空并重置
从外部数据源加载数据,key: 1
Alice示例代码,如果数据库没有值。使用熔断避免缓存击穿。
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeCountExtractor;
import java.util.Collections;
import java.util.List;
public class UserCache extends GuavaAbstractLoadingCache<String, User> {
private static final String RESOURCE_NAME = "userCache_fetchData";
public UserCache() {
// 初始化 Sentinel 熔断规则
initDegradeRule();
}
/**
* 配置 Sentinel 熔断规则:基于异常比例
*/
private void initDegradeRule() {
DegradeRule rule = new DegradeRule();
rule.setResource(RESOURCE_NAME); // 资源名,必须与 SphU.entry 一致
rule.setGrade(com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager.DEGRADE_GRADE_EXCEPTION_RATIO);
rule.setCount(0.6); // 异常比例阈值:60%
rule.setTimeWindow(10); // 熔断时长:10 秒
rule.setMinRequestAmount(5); // 最小请求数:5 次才触发统计
rule.setStatIntervalMs(10000); // 统计窗口:10 秒
DegradeRuleManager.loadRules(Collections.singletonList(rule));
System.out.println("Sentinel 熔断规则已加载: " + rule);
}
@Override
protected User fetchData(String userId) throws Exception {
Entry entry = null;
try {
// 1. 尝试进入 Sentinel 保护资源
entry = SphU.entry(RESOURCE_NAME);
// 2. 实际查询数据库(可能抛出异常)
User user = userDao.findById(userId);
if (user == null) {
// 注意:数据不存在 ≠ 系统异常!不要抛异常,否则会被计入熔断统计
return null;
}
return user;
} catch (BlockException ex) {
// 3. 触发熔断/限流,直接降级
System.err.println("【Sentinel 熔断】拒绝请求,不查 DB!key=" + userId);
return User.EMPTY; // 或 throw new ServiceUnavailableException("服务暂时不可用");
} finally {
// 4. 退出资源
if (entry != null) {
entry.exit();
}
}
}
}在 pom.xml 中添加以下依赖即可使用 Guava 和日志功能:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。