介绍

当服务的访问量过大、并发量过高时,为了减轻数据库的压力,可以考虑使用缓存。缓存分为两种,一种是本地缓存,比如Caffeine、Ehache,另外一种是分布式缓存,最常用的就是redis。本地缓存的数据可存在于主存当中,所以速度略快于分布式缓存。
Caffeine是一款基于java8开发的本地缓存框架,拥有着较高的性能、命中率以及优秀的缓存驱逐策略。
它与ConcurrentHashMap非常相似的,但后者并不能自动的移除那些访问不频繁的数据。下面主要讲一下Caffeine的使用。

缓存的添加策略

手动加载

 private static void buildCache() {
        Cache<String, Set<String>> cache = Caffeine.newBuilder()
                //设置过期时间;向缓存中写入的数据会在1分钟之后过期
                .expireAfterWrite(1, TimeUnit.MINUTES)
                //设置最大值;最大可以放1条数据,当数据量超过最大值之后,则进行覆盖
                .maximumSize(1)
                .build();

        String cacheKey = "cacheKey";
        //从缓存中获取数据;如果数据不存在则通过第二个参数自动生成;如果生成失败则返回null
        Set<String> value0 = cache.get(cacheKey, key -> Sets.newHashSet("value1", "value2"));
        //从缓存中获取,如果不存在,则返回null
        Set<String> value1 = cache.getIfPresent(cacheKey);
        //向缓存中添加元素
        cache.put(cacheKey, Sets.newHashSet("value3", "value4"));
        //可通过asMap方法对产生的ConcurrentMap进行操作;
        cache.asMap().forEach((key, value) -> {
            if (key.equals(cacheKey)) {
                value.add("value5");
            }
        });
        //删除缓存
        cache.invalidate(cacheKey);
    }

以上是手动加载的常用方法,之所以被称为手动加载,是因为在构建之后,需要我们手动进行put等操作来生成缓存中的数据。需要注意的是,cache.get方法是一个原子操作,多线程环境下,如果key是相同的,最后的操作会覆盖之前的数据。

自动加载

private static void autoLoadCache() {
        String cacheKey = "cacheKey";
        String cacheKey1 = "cacheKey1";
        LoadingCache<String, Set<String>> cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .maximumSize(100)
                //根据key来生成value
                .build(key -> createCache(key));
        //如果未命中缓存,则通过key值生成value
        cache.get(cacheKey);
        //批量查找key对应的value;"cacheKey"会获取,"cacheKey1"返回的value为0条。
        Map<String, Set<String>> all = cache.getAll(Arrays.asList(cacheKey, cacheKey1));
    }

    private static Set<String> createCache(String key) {
        return new HashSet<String>() {{
            if (key.equals("cacheKey1")) {
                add(key.concat("value1"));
                add(key.concat("value2"));
            }
        }};
    }

自动加载相对于手动加载,是将生成缓存的步骤放在了创建缓存里,并在此基础上增加了批量查找的功能。
自动加载返回的是一个LoadingCache,我们可以自定义该接口里面的load(对应cache.get)方法和loadAll(对应cache.getAll)方法。

      LoadingCache<String, Set<String>> cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .maximumSize(100)
                //根据key来生成value
                .build(new CacheLoader<String, Set<String>>() {

                    @Override
                    public Set<String> load(String key) throws Exception {
                        //重写load方法...
                        return null;
                    }

                    @Override
                    public Map<String, Set<String>> loadAll(Iterable<? extends String> keys) throws Exception {
                        //重写loadAll方法...
                        return null;
                    }
                });

手动异步加载

   private static void buildAsyncCache() throws ExecutionException, InterruptedException {
        String cacheKey = "cacheKey";
        AsyncCache<String, Set<String>> cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .maximumSize(100)
                //表示是异步构建
                .buildAsync();

        // 查找value,没有返回null
        CompletableFuture<Set<String>> value = cache.getIfPresent(cacheKey);
        // 查找缓存元素,如果不存在,则异步生成结束后再返回
        value = cache.get(cacheKey, key -> createCache(key));
        // 添加或者更新一个缓存元素
        cache.put(cacheKey, value);
        //阻塞移除,需要等到缓存生成之后才能移除
        cache.synchronous().invalidate(cacheKey);
    }

    private static Set<String> createCache(String key) {
        //直到休眠完毕才能执行并返回
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new HashSet<String>() {{
            if (key.equals("cacheKey")) {
                add(key.concat("value1"));
                add(key.concat("value2"));
            }
        }};
    }

手动异步加载方式使用了java8的CompletableFuture,在执行cache.get方法时,会进行异步获取value,直到value返回,下面是cache.get方法的源码,里面进行了异步的处理
在这里插入图片描述
并且该方式在清除缓存的时候是同步方式,防止缓存写入操作在清除之后执行。

自动异步加载

   private static void autoAsyncCache() {
        String cacheKey = "cacheKey";
        AsyncLoadingCache<String, Set<String>> cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .maximumSize(100)
                //加载时创建自动异步生成数据的方法
                .buildAsync((String key) -> createCache(key));

        //value不存在的时候异步获取
        CompletableFuture<Set<String>> value = cache.get(cacheKey);
        //异步获取所有数据
        cache.getAll(Arrays.asList(cacheKey));
    }

自动异步加载的方式和自动加载方式很像,也可以重写CacheLoader接口中的AsyncCacheLoader.asyncLoad 和AsyncCacheLoader.asyncLoadAll方法。重写方式可参考上面的自动加载。

缓存的驱逐策略

基于容量

上面简单涉及到了缓存驱逐的策略,maximumSize方法可设置最大容量,当超过该容量之后则会进行覆盖,如下是基于缓存数量的驱逐方式:

       Cache<String, Set<String>> cache = Caffeine.newBuilder()
                //设置最大值;最大可以放1条数据,当数据量超过最大值之后,则进行覆盖
                .maximumSize(1)
                .build();

基于权重的驱逐方式:

   private static void buildCache() {
        Cache<String, Set<String>> cache = Caffeine.newBuilder()
                //最大只能存下value数量为1的值
                .maximumWeight(1)
                .weigher((String key, Set<String> value) -> value.size())
                .build();
        
        cache.get("1", key -> new HashSet<String>() {{
            add(key.concat("value"));
        }});
        cache.get("2", key -> new HashSet<String>() {{
            add(key.concat("value"));
            add(key.concat("value1"));
        }});
  }

如上设置了value的权重,意思是只能保留条数为1条的数据,则缓存中只会保留key为“1”的那条数据。

基于时间

如下是基于固定的时间方式:

       Cache<String, Set<String>> cache = Caffeine.newBuilder()
                //设置过期时间;向缓存中写入或更新的数据会在1分钟之后过期
                .expireAfterWrite(1, TimeUnit.MINUTES)
                //最大只能存下value数量为1的值
                .build();

        Cache<String, Set<String>> cache1 = Caffeine.newBuilder()
                //设置过期时间;没有被访问的时间超过了1分钟,数据会过期
                .expireAfterAccess(1, TimeUnit.MINUTES)
                //最大只能存下value数量为1的值
                .build();

基于自定义过期时间的方式:

   Cache<String, Set<String>> cache2 = Caffeine.newBuilder()
                //自定义设置过期时间
                .expireAfter(new Expiry<String, Set<String>>() {
                    @Override
                    public long expireAfterCreate(@NonNull String key, @NonNull Set<String> value, long currentTime) {
                        //创建多久过期
                        return 1;
                    }

                    @Override
                    public long expireAfterUpdate(@NonNull String key, @NonNull Set<String> value, long currentTime, @NonNegative long currentDuration) {
                        //更新多久过期
                        return 2;
                    }

                    @Override
                    public long expireAfterRead(@NonNull String key, @NonNull Set<String> value, long currentTime, @NonNegative long currentDuration) {
                        //访问多久过期
                        return 3;
                    }
                })
                .build();

基于引用

      Cache<String, Set<String>> cache2 = Caffeine.newBuilder()
                //基于软引用
                .softValues()
                .build();

      Cache<String, Set<String>> cache2 = Caffeine.newBuilder()
                //基于弱引用;当key和value都不再存在其他强引用的时候
                .weakKeys()
                .weakValues()
                .build();

各类引用的demo:

   void reference() {
        //强引用;永远不会被jvm回收
        UserModel userModel = new UserModel();
        //软引用;内存足够则不会回收,内存不够则回收
        SoftReference<UserModel> softUserModel = new SoftReference<UserModel>(userModel);
        //弱引用;内存够与不够都会被回收
        WeakReference<UserModel> weekUserModel = new WeakReference<UserModel>(userModel);
        //虚引用;随时都会被回收
        ReferenceQueue<UserModel> rq = new ReferenceQueue<UserModel>();
        PhantomReference<UserModel> prA = new PhantomReference<UserModel>(userModel, rq);
    }

感谢您的观看,欢迎一起探讨。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐