缓存技术内部交流_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 中读取。

posted @ 2017-07-13 17:47  首夜盲毒预言家  阅读(1101)  评论(0编辑  收藏  举报