10

CaffeineCache 慎用weakKeys

 4 years ago
source link: http://www.zhyea.com/2019/05/06/caffeinecache-weakkeys-flaw.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前两天在一个Spring项目里使用了Caffine缓存,在application.yml中的配置如下:

  cache:
    type: CAFFEINE
    caffeine:
      spec: initialCapacity=1048576,maximumSize=1073741824,weakKeys,weakValues,expireAfterAccess=10m

为了避免缓存占用过多内存导致频繁GC,使用了weakKeys和weakValues选项。

不过测试时发现缓存不能命中,仍然会查询数据库。

通过debug发现,caffine使用WeakKeyReference将缓存的key做了封装。WeakKeyReference的结构如下:

static class WeakKeyReference<K> 
    extends WeakReference<K> implements InternalReference<K> {
    
    private final int hashCode;
 
    public WeakKeyReference(@Nullable K key, @Nullable ReferenceQueue<K> queue) {
      super(key, queue);
      hashCode = System.identityHashCode(key);
    }
 
    @Override
    public Object getKeyReference() {
      return this;
    }
 
    @Override
    public boolean equals(Object object) {
      return referenceEquals(object);
    }
 
    @Override
    public int hashCode() {
      return hashCode;
    }
  }

需要注意这里的equals()和hashCode()方法。

equals()调用的referenceEquals()方法是接口InternalReference的default方法,具体为:

    default boolean referenceEquals(@Nullable Object object) {
      if (object == this) {
        return true;
      } else if (object instanceof InternalReference<?>) {
        InternalReference<?> referent = (InternalReference<?>) object;
        return (get() == referent.get());
      }
      return false;
    }

referenceEquals()方法中调用的get()方法在WeakKeyReference类中获取的是key的原始值。在方法中对两个key是否一致的判定使用的是 == ,而非是equals()。也就是说需要两个key指向同一个对象才能被认为是一致的。

hashCode()的实现也与equals()方法呼应。生成hashCode使用的是 System . identityHashCode ( ) 。identityHashCode方法是jre的一个native方法,这个方法的注释如下:

    /**
     * Returns the same hash code for the given object as
     * would be returned by the default method hashCode(),
     * whether or not the given object's class overrides
     * hashCode().
     * The hash code for the null reference is zero.
     */

注释说明这个方法对于指定的对象会返回相同的hashCode。即这个方法是针对对象进行操作的,比如两个字符串对象,即使其字符序列相同,通过identityHashCode方法生成的hashCode也不会相同。 看一个示例程序:

    public static void main(String[] args) throws IOException {
        System.out.println(System.identityHashCode(new String("zhyea")));
        System.out.println(System.identityHashCode(new String("zhyea")));
    }

示例程序输出了相通字符序列“zhyea”的两个字符串对象的identityHashCode执行结果,结果为:

可以看到最终结果是不同的。

到现在缓存不能命中的原因应该是找到了:因为使用了weakKeys选项,caffine使用WeakKeyReference封装了缓存key,导致相同字符序列的不同String对象的key被视为是不同的缓存主键。

果然在去掉weakKeys和weakValues配置项后,测试发现缓存能够命中了。

后来在 Caffeine的文档 中找到了如下说明:

Caffeine . weakKeys ( ) stores keys using weak references. This allows entries to be garbage-collected if there are no other strong references to the keys. Since garbage collection depends only on identity equality, this causes the whole cache to use identity (==) equality to compare keys, instead of equals ( ) .

文档中提到因为GC的限制,需要对weakKey使用“==”替换equals()。

原因算是找到了,不过回过头来想想,在Spring中Caffeine的weakKeys选项确实有些鸡肋:Spring的CacheKey生成方式导致weakKey必然指向不同的对象,结果就是缓存注定不能命中,并且每次调用都会在缓存中插入一条新的记录。这样尽管使用weakKey不会造成内存泄漏,可是也会增加GC负担。因此在SpringBoot中使用Caffeine时需要慎用weakKeys。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK