多级缓存之Google Guava的实现方案

背景

数据库扛不住了可以使用Redis来分担读请求,在大访问量的系统中Redis集中式缓存方案,会成为大型系统的瓶颈。有什么方案解决呢?
可以在增加一层缓存层,即JVM进程的内存中进行本地缓存,分摊Redis的压力。Guava cache的设计来源于CurrentHashMap,可以按照多种策略来清理存储在其中的缓存值且保持很高的并发读写性能。以下场景适合于做本地缓存:

  • 内存占用较小
  • 数据极少变化
  • 需要访问整个集合
  • 数据实时性要求不高

如何创建?

创建缓存方式有两种。

pom引入依赖

在项目中添加如下的依赖即可

<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>30.0-jre</version>
</dependency>

CacheLoader方式

何时使用?

是否存在一个默认函数来加载或计算与键关联的值?如果是这样,则应使用CacheLoader。

案例

package com.linfanchen.springboot.lab.guava;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;

import static com.alibaba.druid.sql.ast.SQLPartitionValue.Operator.List;

@RunWith(SpringRunner.class)
@SpringBootTest
public class CacheTest {

    LoadingCache<String, String> cache = CacheBuilder.newBuilder()
            .build(new CacheLoader<String, String>() {
                /**
                 * 获取单条数据
                 */
                @Override
                public String load(final String key) {
                    return getCar(key);
                }

                /**
                 * 获取所有数据
                 */
                @Override
                public Map<String, String> loadAll(final Iterable<? extends String> keys) throws Exception {
                    // 此包完整路径位于: com.google.common.collect.Lists
                    ArrayList<String> keysList = Lists.newArrayList(keys);
                    return getCars(keysList);
                }

            });

    /**
     * Mock 读取DB中的单条数据
     */
    private static String getCar(String key) {
        return "Lexus";
    }

    /**
     * Mock 读取DB中的多条数据
     */
    private static Map<String, String> getCars(List<String> keys) {
        Map<String,String> map = new HashMap<>();
        map.put("bmw", "BMW 530Li");
        map.put("benz", "E300L");
        map.put("audi", "Audi A6L");
        return map;
    }


    @Test
    public void firstTest() {
        java.util.List<String> keys = new ArrayList<>();
        keys.add("bmw");
        keys.add("benz");

        // 从本地缓存读取数据
        try {
            System.out.println(cache.getAll(keys)); // 输出 {bmw=BMW 530Li, benz=E300L}

        } catch (ExecutionException e1) {
            e1.printStackTrace();
        }
    }



}


CacheBuilder 的详细方法

LoadingCache<Object, Object> userCache = CacheBuilder.newBuilder()
         // 基于容量回收。缓存的最大数量。超过就取MAXIMUM_CAPACITY = 1 << 30。依靠LRU队列recencyQueue来进行容量淘汰
        .maximumSize(1000)
        
         // 基于容量回收。但这是统计占用内存大小,maximumWeight与maximumSize不能同时使用。设置最大总权重
        .maximumWeight(1000)
        
         // 设置权重(可当成每个缓存占用的大小)
        .weigher((o, o2) -> 5)
        
         // 软弱引用(引用强度顺序:强软弱虚)
         // -- 弱引用key
        .weakKeys()
        
         // -- 弱引用value
        .weakValues()
        
         // -- 软引用value
        .softValues()
        
         // 过期失效回收
         // -- 没读写访问下,超过5秒会失效(非自动失效,需有任意getput方法才会扫描过期失效数据)
        .expireAfterAccess(5L, TimeUnit.SECONDS)
        
         // -- 没写访问下,超过5秒会失效(非自动失效,需有任意putget方法才会扫描过期失效数据)
        .expireAfterWrite(5L, TimeUnit.SECONDS)
        
         // 没写访问下,超过5秒会失效(非自动失效,需有任意putget方法才会扫描过期失效数据。但区别是会开一个异步线程进行刷新,刷新过程中访问返回旧数据)
        .refreshAfterWrite(5L, TimeUnit.SECONDS)
        
         // 移除监听事件
        .removalListener(removal -> {
             // 可做一些删除后动作,比如上报删除数据用于统计
             System.out.printf("触发删除动作,删除的key=%s%n", removal);
        })
        
         // 并行等级。决定segment数量的参数,concurrencyLevel与maxWeight共同决定
        .concurrencyLevel(16)
        
         // 开启缓存统计。比如命中次数、未命中次数等
        .recordStats()
        
         // 所有segment的初始总容量大小
        .initialCapacity(512)
        
         // 用于测试,可任意改变当前时间。
        .ticker(new Ticker() {
             @Override
             public long read() {
                     return 0;
                    }
        })
        // 开始构造
        .build(new CacheLoader<Object, Object>() {
             @Override
             public Object load(Object name) {
                     // 在cache找不到就取数据
                     return String.format("重新load(%s):%s", System.currentTimeMillis(), name);
                    }
        });
        
 // 简单使用
 userCache.put("car", "BMW 530Li 行政版");
 System.out.println(userCache.get("car"));
 

Callable方式

何时使用?

希望使用原子的“ get-if-absent-compute”语义,则应将Callable传递给get调用。

案例

package com.linfanchen.springboot.lab.guava;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

@RunWith(SpringRunner.class)
@SpringBootTest
public class CacheCallableTest {

    LoadingCache<String, String> cache = CacheBuilder.newBuilder()
            .build(new CacheLoader<String, String>() {
                /**
                 * 获取单条数据
                 */
                @Override
                public String load(final String key) {
                    return getCar(key);
                }

            });

    /**
     * Mock 读取DB中的单条数据
     */
    private static String getCar(String key) {
        return "Lexus" + key;
    }

    /**
     * Mock 读取DB中的多条数据
     */
    private static Map<String, String> getCars(List<String> keys) {
        Map<String,String> map = new HashMap<>();
        map.put("bmw", "BMW 530Li");
        map.put("benz", "E300L");
        map.put("audi", "Audi A6L");
        return map;
    }


    @Test
    public void secondTest() {

        try {
            // 从本地缓存读取数据
            String value = cache.get("es300h", new Callable<String>() {
                @Override
                public String call() throws Exception {
                    System.out.println("Now invoking callable code...");
                    return "callable code...";
                }
            });

            System.out.println(value);  // 输出: callable code...

        } catch (ExecutionException e1) {
            e1.printStackTrace();
        }
    }



}

如何删除?

被动

基于数据大小的删除

当数据的个数多于 maximumSize所设置的值时候,会根据 LRU+FIFO 策略进行淘汰。

基于过期时间的删除

隔多长时间后没有被访问过的key被删除,时间标准参照 expireAfterAccess所设置的时间。

基于引用的删除

可以通过weakKeys和weakValues方法指定Cache只保存对缓存记录key和value的弱引用。这样当没有其他强引用指向key和value时,key和value对象就会被垃圾回收器回收。

主动

删除单条

cache.invalidate("bmw");

批量删除

cache.invalidateAll(Arrays.asList("bmw","benz"));

清空缓存

cache.invalidateAll();

总结

guava cache 的异步 reload 策略可以有效实现容错、节约调用耗时的目的,但有一个致命的缺陷:主线程返回的数据有可能是已过期的。
通常,我们对于缓存中数据的实际失效时间并不敏感,在这样的情况下,即使 guava cache 返回了已失效数据,也并不会造成任何业务问题,而由此带来的性能提升与容错的好处是显而易见的。

参考文档:

官方网址

官方使用案例

Google Guava Cache高效本地缓存

Guava Cache简介、应用场景分析、代码实现以及核心的原理

guava cache详细介绍

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值