相关阅读:
【极客源码】JetCache源码(一)开篇
【极客源码】JetCache源码(二)顶层视图
JAVA基础(一)简单、透彻理解内部类和静态内部类
JAVA基础(二)内存优化-使用Java引用做缓存
JAVA基础(三)ClassLoader实现热加载
JAVA基础(四)枚举(enum)和常量定义,工厂类使用对比
JAVA基础(五)函数式接口-复用,解耦之利刃
JAVA编程思想(一)通过依赖注入增加扩展性
JAVA编程思想(二)如何面向接口编程
JAVA编程思想(三)去掉别扭的if,自注册策略模式优雅满足开闭原则
JAVA编程思想(四)Builder模式经典范式以及和工厂模式如何选?
HikariPool源码(二)设计思想借鉴
人在职场(一)IT大厂生存法则
1. 类结构
Cache相关类结构如下:
1.1. Cache
1.定义了缓存操作方法,如get,put,remove等等。
2.接口中使用了默认方法,提供默认实现。
3.接口中提供了大写的方法和小写的方法,小写为默认接口方法,大写的未实现,交由子类实现,其中小写方法调用大写方法使用了模板方法模式。
default V get(K key) throws CacheInvokeException {
CacheGetResult<V> result = GET(key); // 模板方法设计模式
if (result.isSuccess()) {
return result.getValue();
} else {
return null;
}
}
CacheGetResult<V> GET(K key); // 接口方法,待子类实现
1.2. AbstractCache
1.2.1. 实现了cache接口
实现了cache接口,但并未完全实现,只是封装了公共部分,然后再次使用模板方法设计模式交给子类实现。
@Override
public final CacheGetResult<V> GET(K key) {
long t = System.currentTimeMillis();
CacheGetResult<V> result;
if (key == null) {
result = new CacheGetResult<V>(CacheResultCode.FAIL, CacheResult.MSG_ILLEGAL_ARGUMENT, null);
} else {
result = do_GET(key); // 模板方法设计模式
}
result.future().thenRun(() -> {
CacheGetEvent event = new CacheGetEvent(this, System.currentTimeMillis() - t, key, result);
notify(event);
});
return result;
}
protected abstract CacheGetResult<V> do_GET(K key); // 模板方法,由子类实现
1.2.2. 增加了一些通用方法
// 发送缓存事件给监控者,监控者可对缓存事件监控
public void notify(CacheEvent e) {
List<CacheMonitor> monitors = config().getMonitors();
for (CacheMonitor m : monitors) {
m.afterOperation(e);
}
}
记录错误日志,方法是protected的,可以被子类重载。
protected void logError(String oper, Object key, Throwable e) {
....
}
1.2.3. 异步编程
@Override
public final CacheGetResult<V> GET(K key) {
long t = System.currentTimeMillis();
CacheGetResult<V> result;
if (key == null) {
result = new CacheGetResult<V>(CacheResultCode.FAIL, CacheResult.MSG_ILLEGAL_ARGUMENT, null);
} else {
result = do_GET(key);
}
// 这里的thenRun用法并没有特别之处,本质上就是一个异步线程,只不过利用了result中的feature对象
// 异步处理的目的是不影响GET方法的性能
result.future().thenRun(() -> {
CacheGetEvent event = new CacheGetEvent(this, System.currentTimeMillis() - t, key, result);
notify(event);
});
return result;
}
关于异步编程可参考“Java并发编程入门(十九)异步任务调度工具CompleteFeature ”,里面有各个方法的详细介绍,看这篇就够了。
1.2.4. 函数式接口的使用
在JetCache中大量使用到函数式接口,函数式接口的好处是高度抽象,灵活性高,可用于兼容遗留方法,只需要方法参数和返回值一致就可以复用,不依赖类实现的接口和方法名称;
其缺点是由于高度抽象,不能明确表达方法的具体意图。
如果没有兼容遗留方法的必要,还是定义明确的接口方法更好一些。
对于函数式接口,参考“JAVA基础(五)函数式接口-复用,解耦之利刃 ”。
// 方法参数使用了函数式接口来传递loader
static <K, V> V computeIfAbsentImpl(K key, Function<K, V> loader, boolean cacheNullWhenLoaderReturnNull,
long expireAfterWrite, TimeUnit timeUnit, Cache<K, V> cache) {
// 这里需要获取abstractCache是因为接下来要使用的方法在cache接口中没有,这种用法有那么点别扭,也就是从方法签名看
// 不出来在方法内部要转换为子类,并用到子类中的方法,如果在cache中定义此方法可能更自然一些
AbstractCache<K, V> abstractCache = CacheUtil.getAbstractCache(cache);
// abstractCache::notify就是cache中不存在的方法,这里用了函数式接口,可以将一个函数作为参数传入
// 这里使用了代理模式,创建了一个缓存load代理对象,用于记录load耗时
CacheLoader<K, V> newLoader = CacheUtil.createProxyLoader(cache, loader, abstractCache::notify);
CacheGetResult<V> r;
// 从这里判断实例可以思考接口的定义方式
if (cache instanceof RefreshCache) {
RefreshCache<K, V> refreshCache = ((RefreshCache<K, V>) cache);
r = refreshCache.GET(key);
refreshCache.addOrUpdateRefreshTask(key, newLoader);
} else {
r = cache.GET(key);
}
if (r.isSuccess()) {
return r.getValue();
} else {
// 当获取缓存失败时,使用函数式接口和匿名内部类来更新缓存
Consumer<V> cacheUpdater = (loadedValue) -> {
if(needUpdate(loadedValue, cacheNullWhenLoaderReturnNull, newLoader)) {
if (timeUnit != null) {
cache.PUT(key, loadedValue, expireAfterWrite, timeUnit).waitForResult();
} else {
cache.PUT(key, loadedValue).waitForResult();
}
}
};
V loadedValue;
// 缓存穿透保护,需要同步加载缓存数据
if (cache.config().isCachePenetrationProtect()) {
loadedValue = synchronizedLoad(cache.config(), abstractCache, key, newLoader, cacheUpdater);
} else {
loadedValue = newLoader.apply(key);
// 函数式接口的方法签名,仅仅消费数据
cacheUpdater.accept(loadedValue);
}
return loadedValue;
}
}
1.2.5. load代理对象
在上面的方法里创建了一个缓存load代理,用于记录load耗时:
// 这里使用了代理模式,创建了一个缓存load代理对象,用于记录load耗时
CacheLoader<K, V> newLoader = CacheUtil.createProxyLoader(cache, loader, abstractCache::notify);
// CacheUtil.java
public static <K, V> ProxyLoader<K, V> createProxyLoader(Cache<K, V> cache,
Function<K, V> loader,
Consumer<CacheEvent> eventConsumer) {
if (loader instanceof ProxyLoader) {
return (ProxyLoader<K, V>) loader;
}
if (loader instanceof CacheLoader) {
return createProxyLoader(cache, (CacheLoader) loader, eventConsumer);
}
return k -> {
long t = System.currentTimeMillis();
V v = null;
boolean success = false;
try {
v = loader.apply(k);
success = true;
} finally {
t = System.currentTimeMillis() - t;
CacheLoadEvent event = new CacheLoadEvent(cache, t, k, v, success);
eventConsumer.accept(event);
}
return v;
};
}
为啥不直接在原生的loader类中实现记录耗时的功能呢?
因为这里传入的loader是个函数式接口,且通过依赖注入的方式传入,它允许开发者提供不同的实现,当不同的开发者有不同的实现时,就没有必要都再去实现一遍记录load耗时的功能,因此这里统一用代理模式做了封装,不管开发者如何实现,都会具备这个公共能力。
另外从这个层面来看,代理模式的用法和模板方法模式也有相同之处,都可以将公共部分抽象,并剥离出来统一实现,避免重复实现。
关于接口注入可参考:“JAVA编程思想(一)通过依赖注入增加扩展性”。
1.2.6. 接口和类结构的定义
在上面的方法中有两个地方用到了类型强转:
AbstractCache<K, V> abstractCache = CacheUtil.getAbstractCache(cache);
if (cache instanceof RefreshCache) {
之所以用强转,是因为在cache接口中不具备子类的方法,这种处理方式其实不利于面向接口编程,但有时不同子类的行为又避免不了差异,所以就导致了这种情况存在。当子类存在不同接口中的方法时有两种处理方式:
一种就是上述的处理方式,随着类继承的层次加深,底层类和上一层出现了差异,底层增加了上一层不存在的方法,此时要么方法中传入具体的底层类,要么就是传入接口类,在方法内部做强转。
另外一种方式就是当子类有不同实现方法时,增加接口定义,通过子类实现不同的接口来处理差异,这样仍然可以面向接口编程,试比较两种实现方式的差异:
如图,左边直接在RefreshCache类中增加了一个接口和父类不存在的方法,当方法参数为cache,又需要使用子类特有方法时必然会导致强转。
而右边增加了RefreshAbility接口定义,在该接口中定义了新方法,RefreshCache同时实现了两个接口,这样,当方法的输入参数是RefreshAbility接口时,仍然可以面向接口编程,不需要强转。
当然,有的场景没有这么简单,但我们仍然需要注意考虑:如何尽可能面向接口编程,避免强转,一定有比较优雅的方式来解决这个问题。
1.2.7. 缓存穿透保护
回顾下在【极客源码】JetCache源码(一)开篇 中提到的缓存穿透的概念:
数据库没有数据,缓存中也没有数据,这样每次访问都会访问数据库,导致数据压力增大。因此,当缓存里没有数据时,要避免多个并发线程同时访问数据库,此时需要进行加锁,只允许一个线程去访问数据库,其他线程等待。
实现代码片段如下:
// 缓存穿透保护,需要同步加载缓存数据
if (cache.config().isCachePenetrationProtect()) {
loadedValue = synchronizedLoad(cache.config(), abstractCache, key, newLoader, cacheUpdater);
} else {
loadedValue = newLoader.apply(key);
// 函数式接口的方法签名,仅仅消费数据,更新缓存数据
cacheUpdater.accept(loadedValue);
}
// 避免缓存穿透的方法
static <K, V> V synchronizedLoad(CacheConfig config, AbstractCache<K,V> abstractCache,
K key, Function<K, V> newLoader, Consumer<V> cacheUpdater) {
// 利用ConcurrentHashMap来对访问key加锁
ConcurrentHashMap<Object, LoaderLock> loaderMap = abstractCache.initOrGetLoaderMap();
Object lockKey = buildLoaderLockKey(abstractCache, key);
while (true) {
boolean create[] = new boolean[1];
LoaderLock ll = loaderMap.computeIfAbsent(lockKey, (unusedKey) -> {
create[0] = true;
LoaderLock loaderLock = new LoaderLock();
// 利用CountDownLatch加锁,使用CountDownLatch的好处是支持设置锁等待超时时间
loaderLock.signal = new CountDownLatch(1);
loaderLock.loaderThread = Thread.currentThread();
return loaderLock;
});
// 如果是同一个线程,那么加载数据并更新缓存,并释放锁
if (create[0] || ll.loaderThread == Thread.currentThread()) {
try {
// 加载数据
V loadedValue = newLoader.apply(key);
ll.success = true; // 这行代码总是返回成功,看起来会导致下面无法走到continue分支。continue哪里只是加了一个保护,可能永远走不到
ll.value = loadedValue;
// 更新缓存
cacheUpdater.accept(loadedValue);
return loadedValue;
} finally {
if (create[0]) {
// 释放锁
ll.signal.countDown();
// 移除加锁key,释放内存
loaderMap.remove(lockKey);
}
}
} else { // 不同线程则等待获取锁的线程处理完,避免同时访问数据库
try {
// 有超时时间设置则等待超时时间后放弃,否则就一直等待
Duration timeout = config.getPenetrationProtectTimeout();
if (timeout == null) {
ll.signal.await();
} else {
boolean ok = ll.signal.await(timeout.toMillis(), TimeUnit.MILLISECONDS);
// 超时后锁还没有被加锁线程释放,则自行load数据,这里没有放入缓存的原因可能是避免重复放入缓存(另一个加载线程的结果不可知)
if(!ok) {
logger.info("loader wait timeout:" + timeout);
return newLoader.apply(key);
}
}
} catch (InterruptedException e) {
logger.warn("loader wait interrupted");
// 如果被中断则自行加载数据
return newLoader.apply(key);
}
// 走到这里,加锁线程的锁已经被释放,如果加载缓存成功,则返回结果,否则继续循环处理
if (ll.success) {
return (V) ll.value;
} else {
continue;
}
}
}
}
其中CountDownLatch用法参考:“Java并发编程入门(十四)CountDownLatch应用场景”.
需要注意的是,这段代码适用于进程内缓存,对分布式缓存无效。
1.2.8. 静态内部类
使用静态内部类来定义加载锁:
static class LoaderLock {
CountDownLatch signal;
Thread loaderThread;
// volatile修饰保证可见性
volatile boolean success;
volatile Object value;
}
关于静态内部类可参考“JAVA基础(一)简单、透彻理解内部类和静态内部类”.
2. 小结
本节通过JetCache的代码重温和学习了如下知识点:
1. 异步编程
2. 函数式接口
3. 设计模式中的模板方法和代理模式,为什么要使用代理模式,和模板方法的相同点
4. 接口定义如何面向接口编程
5. 如何编码解决了我们开篇中提到的缓存穿透问题。
6. CountDownLatch用法
7. 静态内部类
end.