CaffeineCache 慎用weakKeys

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

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

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

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

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

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

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

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

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

示例程序输出了相通字符序列“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。

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据