暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Spring官方都说废掉GuavaCache用Caffeine,你还不换?

稀饭下雪 2020-12-28
752

最近来了一个实习生小张,看了我在公司项目中使用的缓存框架Caffeine,三天两头跑来找我取经,说是要把Caffeine吃透,为此无奈的也只能一个个细心解答了。

后来这件事情被总监直到了,说是后面还有新人,让我将相关问题和细节汇总成一份教程,权当共享好了,该份教程也算是全网第一份,结合了目前我司游戏中业务场景的应用和思考,以及踩过的坑。

这是Caffeine教程的第三篇,主要讲解Caffeine和二级缓存的结合使用。

实习生小张:主管说直接给Caffeine设置了最大缓存个数,会存在一个隐患,那便是当同时在线的玩家数超过最大缓存个数的情况下,会导致缓存被清,之后导致频繁读取数据库加载数据,让我在Caffeine的基础上,结合二级缓存解决这个问题。

可以的,目前来说Caffeine提供了整套机制,可以方便我们和二级缓存进行结合。

在具体给出例子前,要先引出一个CacheWriter的概念,我们可以把它当做一个回调对象,在往Caffeine的缓存put数据或者remove数据的时候回调用。

/**
 * @author xifanxiaxue
 * @date 2020/12/5 10:18
 * @desc
 */

public class CaffeineWriterTest {

    /**
     * 充当二级缓存用,生命周期仅活到下个gc
     */

    private Map<Integer, WeakReference<Integer>> secondCacheMap =
            new ConcurrentHashMap<>();

    @Test
    public void test() throws InterruptedException {
        // 设置最大缓存个数为1
        LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
                .maximumSize(1)
                // 设置put和remove的回调
                .writer(new CacheWriter<Integer, Integer>() {
                    @Override
                    public void write(@NonNull Integer key, @NonNull Integer value) {
                        secondCacheMap.put(key, new WeakReference<>(value));
                        System.out.println("触发CacheWriter.write,将key = " + key + "放入二级缓存中");
                    }

                    @Override
                    public void delete(@NonNull Integer key, @Nullable Integer value, @NonNull RemovalCause cause) {
                        switch (cause) {
                            case EXPLICIT:
                                secondCacheMap.remove(key);
                                System.out.println("触发CacheWriter" +
                                        ".delete,清除原因:主动清除,将key = " + key +
                                        "从二级缓存清除");
                                break;
                            case SIZE:
                                System.out.println("触发CacheWriter" +
                                        ".delete,清除原因:缓存个数超过上限,key = " + key);
                                break;
                            default:
                                break;
                        }
                    }
                })
                .build(new CacheLoader<Integer, Integer>() {
                    @Nullable
                    @Override
                    public Integer load(@NonNull Integer key) {
                        WeakReference<Integer> value = secondCacheMap.get(key);
                        if (value == null) {
                            return null;
                        }

                        System.out.println("触发CacheLoader.load,从二级缓存读取key = " + key);
                        return value.get();
                    }
                });

        cache.put(11);
        cache.put(22);
        // 由于清除缓存是异步的,因而睡眠1秒等待清除完成
        Thread.sleep(1000);
        
        // 缓存超上限触发清除后
        System.out.println("从Caffeine中get数据,key为1,value为"+cache.get(1));
    }
}

复制

举的这个例子稍显复杂,毕竟是要和二级缓存结合使用,不复杂点就没办法显示Caffeine的妙,先看下secondCacheMap对象,这是我用来充当二级缓存用的,由于value值我设置成为WeakReference弱引用,因而生命周期仅活到下个gc。

稀饭:小张,这个例子就可以解决你的二级缓存如何结合的问题,你给我说说看最终打印结果值是null还是非null?

小张:肯定是null 啊,因为key为1的缓存因为缓存个数超过上限被清除了呀。

对Caffeine的运行机制不够熟悉的人很容易犯了小张这样的错误,产生了对结果的误判。

为了理清楚程序运行的逻辑,我将程序运行结果打印了出来

触发CacheWriter.write,将key = 1放入二级缓存中 触发CacheWriter.write,将key = 2放入二级缓存中 触发CacheWriter.delete,清除原因:缓存个数超过上限,key = 1 触发CacheLoader.load,从二级缓存读取key = 1 从Caffeine中get数据,key为1,value为1 触发CacheWriter.delete,清除原因:缓存个数超过上限,key = 2

结合代码,我们可以看到CacheWriter.delete中,我判断了RemovalCause,也就是清除缓存理由的意思,如果是缓存超上限,那么并不清除二级缓存的数据,而CacheLoader.load会从二级缓存中读取数据,所以在最终从Caffeine中加载key为1的数据的时候并不为null,而是从二级缓存拿到了数据。

实习生小张:那最后的打印 ” 触发CacheWriter.delete,清除原因:缓存个数超过上限,key = 2 “ 又是什么情况呢?

那是因为Caffeine在调用CacheLoader.load拿到非null的数据后会重新放入缓存中,这样便导致缓存个数又超过了最大的上限了,所以清除了key为2的缓存。

实习生小张:稀饭稀饭,我这边想具体看到缓存命中率如何,有没有什么方法呢?

有的有的,看源码的话就可以看到,Caffeine内部有挺多打点记录的,不过需要我们在构建缓存的时候开启记录。

/**
 * @author xifanxiaxue
 * @date 2020/12/1 23:12
 * @desc
 */

public class CaffeineRecordTest {

    /**
     * 模拟从数据库中读取数据
     *
     * @param key
     * @return
     */

    private int getInDB(int key) {
        return key;
    }

    @Test
    public void test() {
        LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
                // 开启记录
                .recordStats()
                .build(new CacheLoader<Integer, Integer>() {
                    @Override
                    public @Nullable Integer load(@NonNull Integer key) {
                        return getInDB(key);
                    }
                });
        cache.get(1);

        // 命中率
        System.out.println(cache.stats().hitRate());
        // 被剔除的数量
        System.out.println(cache.stats().evictionCount());
        // 加载新值所花费的平均时间[纳秒]
        System.out.println(cache.stats().averageLoadPenalty() );
    }
}

复制

「实际应用:上次在游戏中引入Caffeine的时候便用来record的机制,只不过是在测试的时候用,一般不建议生产环境用这个。具体用法我是开了条线程定时的打印命中率、被剔除的数量以及加载新值所花费的平均时间,进而判断引入Caffeine是否具备一定的价值。」

实习生小张:Caffeine我已经用上了,可是会有个问题,如果数据忘记保存入库,然后被淘汰掉了,玩家数据就丢失了,Caffeine有没有提供什么方法可以在淘汰的事情让开发者做点什么?

稀饭:还真的有,Caffeine对外提供了淘汰监听,我们只需要在监听器内进行保存就可以了。

/**
 * @author xifanxiaxue
 * @date 2020/11/19 22:34
 * @desc 淘汰通知
 */

public class CaffeineRemovalListenerTest {

    @Test
    public void test() throws InterruptedException {
        LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfterAccess(1, TimeUnit.SECONDS)
                .scheduler(Scheduler.systemScheduler())
             // 增加了淘汰监听
                .removalListener(((key, value, cause) -> {
                    System.out.println("淘汰通知,key:" + key + ",原因:" + cause);
                }))
                .build(new CacheLoader<Integer, Integer>() {
                    @Override
                    public @Nullable Integer load(@NonNull Integer key) throws Exception {
                        return key;
                    }
                });

        cache.put(12);

        Thread.currentThread().sleep(2000);
    }

复制

可以看到我这边使用removalListener提供了淘汰监听,因此可以看到以下的打印结果:

淘汰通知,key:1,原因:EXPIRED

实习生小张:我看到数据淘汰的时候是有提供了几个cause的,也就是原因,分别对应着什么呢?

目前数据被淘汰的原因不外有以下几个:

  • EXPLICIT:如果原因是这个,那么意味着数据被我们手动的remove掉了。

  • REPLACED:就是替换了,也就是put数据的时候旧的数据被覆盖导致的移除。

  • COLLECTED:这个有歧义点,其实就是收集,也就是垃圾回收导致的,一般是用弱引用或者软引用会导致这个情况。

  • EXPIRED:数据过期,无需解释的原因。

  • SIZE:个数超过限制导致的移除。

以上这几个就是数据淘汰时会携带的原因了,如果有需要,我们可以根据不同的原因处理不同的业务。

「实际应用:目前我们项目中就有在数据被淘汰的时候做了缓存入库的处理,毕竟确实有开发人员忘记在逻辑处理完后手动save入库了,所以只能做个兜底机制,避免数据丢失。」


第四篇将分享Caffeine结合db实现的定时持久化的缓存组件



文章转载自稀饭下雪,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论