前两天在一个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。