缓存技术内部交流_05_Cache Through

参考资料:

http://www.ehcache.org/documentation/3.2/caching-patterns.html

http://www.ehcache.org/documentation/3.2/writers.html

示例代码:

https://github.com/gordonklg/study,cache module

A. Cache-As-SoR

前文提过 Cache-As-SoR 模式。老外抽象概念的能力比较泛滥,简而言之,Cache-As-SoR 意味着使用者把缓存层当做系统数据层用,为了同步数据,读模式有 read-through,写模式有 write-through 和 write-behind,读写模式中各取一个组合成 Cache-As-SoR 模式。而文章标题中的 Cache Through 是指 read-through 和 write-through 的组合。

在 Read-Through 模式中,缓存拥有一个 loader 模块,当应用系统访问缓存层获取数据时,如果数据当下不在缓存中,则由 loader 负责从 SoR 获取数据,放入缓存,并返回给应用系统。

在 Write-Through 模式中,当应用系统更新缓存层数据时,由 writer 模块将数据写入 SoR,并更新缓存。

在 Write-Behind 模式中,更新的数据会先存放在本地队列,然后批量写入 SoR。

B. Ehcache3 实现 Cache Through 模式

Ehcache3 通过 CacheLoaderWriter 来实现 Cache-As-SoR 模式,相当于把 loader 和 writer 模块合到了一起。见下例:

gordon.study.cache.ehcache3.pattern.CacheThroughUserService.java

    private UserManagedCache<String, UserModel> cache;

    public CacheThroughUserService() {
cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(String.class, UserModel.class).withLoaderWriter(new CustomLoaderWriter())
.build(true);
} public UserModel findUser(String id) {
UserModel result = cache.get(id);
if (result == null) {
System.out.println("can't find user in cache!");
} else {
System.out.println("get user from cache: " + id);
}
return result;
} public UserModel updateUser(String id, String info) {
UserModel user = new UserModel(id, info);
cache.put(id, user);
return user;
} public boolean deleteUser(String id) {
cache.remove(id);
return true;
} public static void main(String[] args) throws Exception {
final CacheThroughUserService service = new CacheThroughUserService();
ExecutorService executorService = Executors.newFixedThreadPool(30);
for (int i = 0; i < 3; i++) {
executorService.execute(new Runnable() {
public void run() {
service.findUser("1");
}
});
executorService.execute(new Runnable() {
public void run() {
service.findUser("2");
}
});
executorService.execute(new Runnable() {
public void run() {
service.findUser("3");
}
});
}
executorService.shutdown();
executorService.awaitTermination(5, TimeUnit.SECONDS);
System.out.println("---------------------------------");
service.updateUser("1", "new info ...");
service.findUser("1");
service.deleteUser("1");
service.findUser("1");
} private static class CustomLoaderWriter implements CacheLoaderWriter<String, UserModel> { @Override
public UserModel load(String key) throws Exception {
System.out.println("::load user by id: " + key);
Thread.sleep(1000);
if (key == null || key.equals("1")) {
return new NullUser();
}
return new UserModel(key, "info ...");
} @Override
public Map<String, UserModel> loadAll(Iterable<? extends String> keys) throws BulkCacheLoadingException, Exception {
System.out.println("::load user by ids: " + keys);
return null;
} @Override
public void write(String key, UserModel value) throws Exception {
System.out.println("::update user by id: " + key);
} @Override
public void writeAll(Iterable<? extends Entry<? extends String, ? extends UserModel>> entries)
throws BulkCacheWritingException, Exception {
System.out.println("::update user by ids");
} @Override
public void delete(String key) throws Exception {
System.out.println("::delete user by id: " + key);
} @Override
public void deleteAll(Iterable<? extends String> keys) throws BulkCacheWritingException, Exception {
System.out.println("::delete user by ids");
}
}

代码第58行定义的 CustomLoaderWriter 实现了 CacheLoaderWriter 接口。其 load、write、delete 方法分别用于从 SoR 层读取、写入、删除数据。

代码第4行通过 withLoaderWriter 方法设置 loaderWriter,激活 Cache-As-SoR 模式。

Service 层得到极大的精简,只需要无脑操作缓存即可。

从控制台输出可以看出,Cache-As-SoR 比较好的解决了并发读的问题,多个线程有机会同时读取后端数据。

C. 源码分析

CacheLoaderWriter 是如何解决并发读的问题的呢?

首先,要了解 ConcurrentHashMap 有个 compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) 方法,该方法用参数传入的 BiFunction 为指定的 key 设置一个计算出的值,同时保证整个计算过程的原子性:其它线程如果也想更新目标 key 的值,会被阻塞直到 compute 方法完成计算。(实现细节有点难读,需要了解 CAS 原理和 ConcurrentHashMap 设计细节,只看了个大概,以后有机会专门深入研究一下)

剩下的实现就简单了,当 cache builder 指定了 cacheLoaderWriter 时,最终构建的内部 cache 为 EhcacheWithLoaderWriter。EhcacheWithLoaderWriter 的 get 方法最终会调用底层 ConcurrentHashMap 的 compute 方法,通过 CacheLoaderWriter 的 load 方法从 SoR 中获取数据。

为了防止多次调用 CacheLoaderWriter 的 load 方法从 SoR 中获取数据,本来应该要调用 ConcurrentHashMap 的 computeIfAbsent 方法才对。Ehcache3 中把判断 if absent then compute 的逻辑放到了 OnHeapStore 的 computeIfAbsent 方法中,这样做的最大目的应该是为了保证 get 操作会触发过期逻辑。

  public ValueHolder<V> computeIfAbsent(final K key, final Function<? super K, ? extends V> mappingFunction) throws StoreAccessException {
computeIfAbsentObserver.begin();
checkKey(key); final StoreEventSink<K, V> eventSink = storeEventDispatcher.eventSink();
try {
final long now = timeSource.getTimeMillis(); final AtomicReference<OnHeapValueHolder<V>> previousValue = new AtomicReference<OnHeapValueHolder<V>>();
final AtomicReference<StoreOperationOutcomes.ComputeIfAbsentOutcome> outcome =
new AtomicReference<StoreOperationOutcomes.ComputeIfAbsentOutcome>(StoreOperationOutcomes.ComputeIfAbsentOutcome.NOOP);
OnHeapValueHolder<V> computeResult = map.compute(key, new BiFunction<K, OnHeapValueHolder<V>, OnHeapValueHolder<V>>() {
@Override
public OnHeapValueHolder<V> apply(K mappedKey, OnHeapValueHolder<V> mappedValue) {
if (mappedValue == null || mappedValue.isExpired(now, TimeUnit.MILLISECONDS)) {
if (mappedValue != null) {
updateUsageInBytesIfRequired(- mappedValue.size());
fireOnExpirationEvent(mappedKey, mappedValue, eventSink);
}
V computedValue = mappingFunction.apply(mappedKey);
if (computedValue == null) {
return null;
} checkValue(computedValue);
OnHeapValueHolder<V> holder = newCreateValueHolder(key, computedValue, now, eventSink);
if (holder != null) {
outcome.set(StoreOperationOutcomes.ComputeIfAbsentOutcome.PUT);
updateUsageInBytesIfRequired(holder.size());
}
return holder;
} else {
previousValue.set(mappedValue);
outcome.set(StoreOperationOutcomes.ComputeIfAbsentOutcome.HIT);
OnHeapValueHolder<V> holder = setAccessTimeAndExpiryThenReturnMappingUnderLock(key, mappedValue, now, eventSink);
if (holder == null) {
updateUsageInBytesIfRequired(- mappedValue.size());
}
return holder;
}
}
});

上面第15行代码保证了如果底层 ConcurrentHashMap 中的缓存项已过期,会重新从 SoR 中读取。

上一篇:MySQL 5.7--复制延迟监控


下一篇:JVM 专题十:运行时数据区(五)堆