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

  • 相关阅读:
    SGU 187 Twist and whirl
    伸展树---初步学习
    poj 2503 Babelfish
    sublime 3 phpfmt配置(大括号对齐)
    Linux Shell 错误: $' ': command not found错误解决
    redis 使用场景
    wireshake tcp 三次握手详解
    ip地址和子网掩码
    phpstorm 远程调式 php
    win10,ubuntu时间不对问题
  • 原文地址:https://www.cnblogs.com/gordonkong/p/7161809.html
Copyright © 2020-2023  润新知