Java本地内存LoadingCache介绍

2022-03-29 09:14:34 浏览数 (1)

一、本地缓存设计思路

如果让我们去设计一个本地缓存,需要考虑以下问题:

1、数据存储结构如何设计

本地缓存应该是要能高效的查找,因此数据的存储结构很关键;

最简单的设计就是一个大的HashMap,这样在多线程写的时候会有问题,当然也可以用并发场景下高性能的ConcurrencyHashMap;当然还可以自己设计底层的存储结构;

2、如何限制本地缓存的大小

为什么要限制,因为内存是宝贵的资源,作为容错处理必须有相应的参数设置本地缓存占用内存大小,具体来说是按内存大小,还是其它维度?

3、更新问题

实际场景缓存的内容是有过期时间的,即过一段时间需要更新,具体是通过过期时间的方式还是刷新机制

4、并发问题

当缓存快要过期了,如果这个时候多线程来取数据,是有一个线程能更新还是随机?

二、LoadingCache介绍

LoadingCache是大名鼎鼎的Google的Guava包的一个本地缓存接口(Guava是一个优秀的类库,具体就不在这里介绍了,有兴趣的同学可以自己百度下),定义如下:

代码语言:javascript复制
@GwtCompatible
public interface LoadingCache<K, V> extends Cache<K, V>, Function<K, V> {
  V get(K key) throws ExecutionException;

  V getUnchecked(K key);

  ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException;
  @Deprecated
  @Override
  V apply(K key);
  
  void refresh(K key);

  @Override
  ConcurrentMap<K, V> asMap();
}

可以看到这是一个泛型接口,其中K为key的类型,V为缓存内容的类型,如果我想以String为Key,缓存内容也是String,则这样声明:

代码语言:javascript复制
LoadingCache<String,  String> actCache;

来看看LoadingCache的具体使用

1、引入依赖

代码语言:javascript复制
<dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>21.0</version>
</dependency>

2、配置

代码语言:javascript复制
LoadingCache<String, String> localCache = CacheBuilder.newBuilder()
                .concurrencyLevel(50)
                .expireAfterWrite(60, TimeUnit.SECONDS)
                .initialCapacity(10000)
                .maximumSize(50000)
                .recordStats()
                .removalListener(new RemovalListener<String, String>() {
                    @Override
                    public void onRemoval(RemovalNotification<String, String> notification) {
                        //自己处理
                    }
                })
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String cacheKey) {
                        String res = null;
                        //业务处理并设置res
                        return res;
                    }
                });

一般我们通过CacheBuilder设置相关参数,常用参数如下:

concurrencyLevel:并发等级,内部会根据这个值设置多少个hash slot,一个slot可以理解为相同hash key的数组,然后每一项又是一个链表,这就是我们书中学到的hash冲突的拉链法;

expireAfterWrite:写入多久之后过期,即被删除

maximumSize:缓存个数,注意这里是缓存多少个key,而不是缓存占用的字节;

removalListener:删除监听器,可以在被删除的时候做些处理,如更新

build:主要的方法,参数为CacheLoader,这是一个抽象类,必须实现的方法有load,即根据单个key得到缓存内容,还有个loadAll,用于一次取多个key的内容时被用到;

3、使用

使用就比较简单了,还是以上面的例子说明:

代码语言:javascript复制
String res = localCache.get("hello");

localCache的类型为LoadingCache<String, String>,我们传一个String给它,它返回一个String。

注意,如果Key是一个对象,则必须自己重写hashCode和equals方法,LoadingCache是通过这2个方法计算hash值和判断是否相等的。

三、设计分析

回到前面的几个问题,看Guava里是怎么解决的;

1、数据存储结构

LoadingCache最终是LocalCache来实现的,我们看下它的结构:

它是用一个数组Segment<K, V> segments 来存放缓存的内容的;

即它自己造的轮子,根据key算出hash得到key应该存在哪个segment,这也符合Google的习惯,Google的技术实力雄厚,对于这种很底层的东西它认为自己的代码性能可能更好;

2、限制缓存大小

前面已经说了,是通过限制缓存key的个数,即maximumSize方法来限制,这样也简单

3、更新问题

从接口声明上它已经声明了refresh方法,即支持自己刷新;

4、并发问题

代码语言:javascript复制
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
      checkNotNull(key);
      checkNotNull(loader);
      try {
        if (count != 0) { // read-volatile
          // don't call getLiveEntry, which would ignore loading values
          ReferenceEntry<K, V> e = getEntry(key, hash);
          if (e != null) {
            long now = map.ticker.read();
            V value = getLiveValue(e, now);
            if (value != null) {
              recordRead(e, now);
              statsCounter.recordHits(1);
              return scheduleRefresh(e, key, hash, value, now, loader);
            }
            ValueReference<K, V> valueReference = e.getValueReference();
            if (valueReference.isLoading()) {
              return waitForLoadingValue(e, key, valueReference);
            }
          }
        }

        // at this point e is either null or expired;
        return lockedGetOrLoad(key, hash, loader);
      } catch (ExecutionException ee) {
      //
      }

可以关注scheduleRefresh和lockedGetOrLoad方法,具体过程是缓存过期的时候是有通过锁和CAS等方式保证只有一个线程来更新,而其它线程在这个时候得到还是旧的值。

0 人点赞