05.Guava Cache

一、cache应具备的逐出策略(缓存元素剔除策略)

      1)数量逐出:通过LRU算法(最近最少使用)可以实现超过limit时顶替掉最近最少使用的元素

      2)重量逐出:大小限制(通过softreference实现),一般是对象如果实现Serializable接口,则通过计算对象的字节大小作为重量逐出参考。

      3)时间逐出:比如限制cache元素只能存活30秒

二、Guava cache:LoadingCache

1、逐出策略说明

数量逐出:CacheBuilder的maximumSize接口

时间逐出:CacheBuilder的expireAfterAccess、expireAfterWrite接口

  • expireAfterAccess:接口中access time会根据Write/Update/Read操作进行改变
  • expireAfterWrite:接口中write time会根据Write/Update操作进行改变
     

重量逐出:CacheBuilder的maximumWeight(以int类型代表所有缓存元素的最大重量)

实体类:Employee.java

package com.mzj.guava.cache.guava;

import com.google.common.base.MoreObjects;

public class Employee {

    private String name;
    private String dept;
    private String empID;

    public Employee(String name, String dept, String empID) {
        this.name = name;
        this.dept = dept;
        this.empID = empID;
    }

    public String getName() {
        return name;
    }

    public String getDept() {
        return dept;
    }

    public String getEmpID() {
        return empID;
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
                .add("name", name)
                .add("dept", dept)
                .add("empID", empID)
                .toString();
    }
}

1、测试数量逐出、重量逐出:CacheLoaderTest.java

package com.mzj.guava.cache.guava;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.Weigher;
import org.junit.Test;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;

/**
 * +
 * 基本用法
 * <p>
 * 查询到Employee放入cache,下次从cache直接拿
 */
public class CacheLoaderTest {

    /**
     * 方便进行断言的变量:
     * 
     * true:从数据库获取数据
     * false:从缓存获取数据
     */
    private boolean isTrue = false;

    @Test
    public void testBasic() {
        LoadingCache<String, Employee> cache = CacheBuilder.newBuilder().maximumSize(10)//缓存数量
                .expireAfterAccess(2, TimeUnit.SECONDS)//缓存元素过期时间(30s过期)
                .build(new CacheLoader<String, Employee>() {//告诉cache,数据从哪里获取
                    @Override
                    public Employee load(String key) throws Exception {
                        return findEmployeeByName(key);
                    }
                });

        try {
            Employee employee = cache.get("mazhongjia");
            assertThat(employee, notNullValue());
            assertLoadFromDBThenReset();//走DB
            System.out.println(employee);
            employee = cache.get("mazhongjia");
            assertLoadFromCache();//走cache
            System.out.println(employee);

            TimeUnit.SECONDS.sleep(3);//休眠3秒,让cache中元素过期
            employee = cache.get("mazhongjia");
            assertLoadFromDBThenReset();//走DB
            System.out.println(employee);
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testEvictionByWeigh() {

        /**
         * 定义一个:秤
         */
        Weigher<String, Employee> weigher = ((key, employee) -> employee.getName().length() + employee.getEmpID().length() + employee.getDept().length());

        LoadingCache<String, Employee> cache = CacheBuilder.newBuilder().maximumWeight(45)//缓存重量(所有缓存元素的最大重量是:45)
                .concurrencyLevel(1)// 设置并发级别为1,并发级别是指可以同时写缓存的线程数(划分多个segment)
                .weigher(weigher)
                .build(new CacheLoader<String, Employee>() {
                    @Override
                    public Employee load(String key) throws Exception {
                        return findEmployeeByName(key);
                    }
                });

        cache.getUnchecked("gavin");
        assertLoadFromDBThenReset();//走DB
        cache.getUnchecked("kevin");
        assertLoadFromDBThenReset();//走DB
        cache.getUnchecked("allen");
        assertLoadFromDBThenReset();//走DB

        assertThat(cache.size(),equalTo(3L));//断言缓存元素为3个

        assertThat(cache.getIfPresent("gavin"),notNullValue());//getIfPresent:如果存在则返回,如果不存在也不会去DB中拿

        cache.getUnchecked("mamam");
        assertThat(cache.getIfPresent("kevin"),nullValue());//getIfPresent:如果存在则返回,如果不存在也不会去DB中拿;这里已经访问过gavin了,所以目前kevin是最老的未被使用的
        assertThat(cache.size(),equalTo(3L));//断言缓存元素为3个

    }

    /**
     * 断言去DB获取数据
     */
    private void assertLoadFromDBThenReset() {
        assertThat(true, equalTo(this.isTrue));
        this.isTrue = false;
    }

    /**
     * 断言去cache获取数据
     */
    private void assertLoadFromCache() {
        assertThat(false, equalTo(this.isTrue));
    }

    /**
     * 去数据库中通过名称查询Employee
     *
     * @param name
     * @return
     */
    private Employee findEmployeeByName(final String name) {
        this.isTrue = true;
        return new Employee(name, name, name);
    }
}

说明1:CacheBuilder是创建LoadingCache的构建器模式的Builder类

说明2:创建CacheLoader除了直接new以外,还可以如下三种方式:

2、测试时间逐出:CacheLoaderTest2.java

package com.mzj.guava.cache.guava;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.junit.Test;

import java.util.concurrent.TimeUnit;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;

public class CacheLoaderTest2 {

    /**
     * CacheBuilder的expireAfterAccess接口测试
     *
     * 接口中access time会根据Write/Update/Read操作进行改变
     *
     * @throws InterruptedException
     */
    @Test
    public void testEvictionByAccessTime() throws InterruptedException {
        LoadingCache<String,Employee> cache = CacheBuilder.newBuilder()
                .expireAfterAccess(2, TimeUnit.SECONDS)
                .build(this.createCacheLoader());

        assertThat(cache.getUnchecked("Alex"),notNullValue());
        assertThat(cache.size(),equalTo(1L));

        TimeUnit.SECONDS.sleep(3);
        assertThat(cache.getIfPresent("Alex"),nullValue());

        assertThat(cache.getUnchecked("Guava"),notNullValue());

        TimeUnit.SECONDS.sleep(1);
        assertThat(cache.getIfPresent("Guava"),notNullValue());//read操作
        TimeUnit.MILLISECONDS.sleep(990);
        assertThat(cache.getIfPresent("Guava"),notNullValue());//read操作
        TimeUnit.SECONDS.sleep(1);
        assertThat(cache.getIfPresent("Guava"),notNullValue());//测试Read操作会修改access time
    }

    /**
     * CacheBuilder的expireAfterWrite接口测试
     *
     * 接口中write time会根据Write/Update操作进行改变
     *
     * @throws InterruptedException
     */
    @Test
    public void testEvictionByWriteTime() throws InterruptedException {
        LoadingCache<String,Employee> cache = CacheBuilder.newBuilder()
                .expireAfterWrite(2, TimeUnit.SECONDS)
                .build(this.createCacheLoader());

        assertThat(cache.getUnchecked("Alex"),notNullValue());
        assertThat(cache.size(),equalTo(1L));

        assertThat(cache.getUnchecked("Guava"),notNullValue());

        TimeUnit.SECONDS.sleep(1);
        assertThat(cache.getIfPresent("Guava"),notNullValue());//read操作
        TimeUnit.MILLISECONDS.sleep(990);
        assertThat(cache.getIfPresent("Guava"),notNullValue());//read操作
        TimeUnit.SECONDS.sleep(1);
        assertThat(cache.getIfPresent("Guava"),nullValue());//测试Read操作不会修改write time

    }

    private final CacheLoader<String,Employee> createCacheLoader(){
        return CacheLoader.from(key -> new Employee(key,key,key));
    }
}

3、CacheBuilder的weakKeys()与weakValues()

weakKeys:

weakValues:

也就是构建LoadingCache时如果调用CacheBuilder的上面两个方法,将使得key或者value被wrapped,by WeakReference,会在Minor GC、Major GC和Full GC时释放其占用内存。

package com.mzj.guava.cache.guava;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.junit.Test;

import java.util.concurrent.TimeUnit;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;

public class WeakKeyTest {

    /**
     * 在JVM发生Minor GC、Major GC和Full GC时会将所有weak reference全部释放掉
     */
    @Test
    public void testWeakKey() throws InterruptedException {
        LoadingCache<String,Employee> cache = CacheBuilder.newBuilder()
                .expireAfterAccess(2, TimeUnit.SECONDS)
                .weakKeys()
                .weakValues()
                .build(this.createCacheLoader());

        assertThat(cache.getUnchecked("Alex"),notNullValue());
        assertThat(cache.getUnchecked("Guava"),notNullValue());

        /**
         *  active method
         *
         *  Thread active design pattern
         */
        System.gc();
        TimeUnit.MILLISECONDS.sleep(100);

        assertThat(cache.getIfPresent("Alex"),nullValue());
    }

    private final CacheLoader<String,Employee> createCacheLoader(){
        return CacheLoader.from(key -> new Employee(key,key,key));
    }
}

4、CacheBuilder的softValues()

softValues:

也就是构建LoadingCache时如果调用CacheBuilder的上面方法,将使得value被wrapped,by SoftReference,会在JVM在内存快不够时(快要OOM时)有可能~~会尝试~~~将所有soft reference释放掉

因此,测试本示例,需要修改JVM参数:-Xmx128M -Xms64M -XX:+PrintGCDetails

package com.mzj.guava.cache.guava;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.junit.Test;

import java.util.concurrent.TimeUnit;

import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;

public class SoftKeyTest {

    /**
     * 在JVM在内存快不够时(快要OOM时)有可能~~会尝试~~~将所有soft reference释放掉
     *
     * 测试本示例,最好修改JVM参数:-Xmx128M -Xms64M -XX:+PrintGCDetails
     */
    @Test
    public void testSoftValues() throws InterruptedException {
        LoadingCache<String, EmployeeBig> cache = CacheBuilder.newBuilder()
                .expireAfterWrite(2, TimeUnit.SECONDS)
                .softValues()
                .build(this.createCacheLoader());

        int i = 0;
        for (; ; ) {
            cache.put("Alex" + i, new EmployeeBig("Alex" + 1, "Alex" + 1, "Alex" + 1));
            System.out.println("The Empolyee [" + (i++) + "] is store into cache.");
            TimeUnit.MILLISECONDS.sleep(600);
        }
    }

    private final CacheLoader<String, EmployeeBig> createCacheLoader() {
        return CacheLoader.from(key -> new EmployeeBig(key, key, key));
    }
}

CacheBuilder无softKeys()方法

2、cache中空元素的处理

注意,这里的空元素是通过CacheLoader的load方法返回的null,而不是缓存过期变为null

没有经过任何处理时cacheLoader的load方法如果返回null值,会抛出异常

@Test
    public void testLoadNullValue() {

        /**
         * cacheLoader的load方法如果返回null值,会抛出异常
         */
        CacheLoader<String, Employee> cacheLoader = CacheLoader.from(k -> k.equals("null") ? null : new Employee(k, k, k));

        LoadingCache<String,Employee> cache = CacheBuilder.newBuilder().build(cacheLoader);

        Employee alax = cache.getUnchecked("Alex");

        assertThat(alax,notNullValue());
        assertThat(alax.getName(),equalTo("Alex"));

        try{
            Employee employeeNull = cache.getUnchecked("null");//此行会抛出异常,由于cacheLoader的load方法由于参数"null"而返回null
            assertThat(employeeNull,nullValue());
           fail("should not process to here.");
        }catch (Exception e){
            e.printStackTrace();
            assertThat(e instanceof CacheLoader.InvalidCacheLoadException,equalTo(true));
        }
    }
通过使用guava的Optional包装了缓存中元素,使得cacheLoader的load方法不可能返回null,而解决了上面测试用例中异常
@Test
    public void testLoadNullValueUseOptional(){
        CacheLoader<String, Optional<Employee>> loader = new CacheLoader<String, Optional<Employee>>() {
            @Override
            public Optional<Employee> load(String key) throws Exception {
                if (key.equals("null"))
                    return Optional.fromNullable(null);
                else
                    return Optional.fromNullable(new Employee(key,key,key));
            }
        };
        LoadingCache<String,Optional<Employee>> cache = CacheBuilder.newBuilder().build(loader);

        Optional<Employee> alax = cache.getUnchecked("Alex");
        assertThat(alax.get(),notNullValue());

        /**
         * 1、此行断言体现了从cache获取到到Optional为Absent的实现(代表空的Optional)但是并未返回null
         */
        assertThat(cache.getUnchecked("null").orNull(),nullValue());//不能调用cache.getUnchecked("null")的get会抛异常(返回Absent的实现)

        /**
         * 2、此行断言体现了更进一步,如果为”null的Optional,则使用默认值
         *
         * 好处是不再需要进行判断:从cache中获取的内容是否为null,没有则返回默认值
         */
        Employee def = cache.getUnchecked("null").or(new Employee("default","default","default"));
        assertThat(def,notNullValue());
    }

3、cache的refresh

作用:在缓存对应DB中数据被修改后,需要将缓存数据逐出

方案一:修改完DB数据后,令缓存中数据实效,下次再请求时重新写入缓存(此方案适用于对数据要求精确的场景)

方案二:让缓存数据定期过期,比如固定2秒就过期(此方案适用于对数据要求不精确的场景,比如评论等)

guava中的cache的refresh只是方案二的实现

CacheBuilder的refreshAfterWrite方法:

设置写入后刷新时间(刷新个人理解为过期)
实现上,guava并不是内部有一个线程不断检查过期元素,而是只记录这个的刷新时间,请求时判断,如果超过这个时间,则不去缓存中拿
package com.mzj.guava.cache.guava;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.junit.Test;

import java.util.concurrent.TimeUnit;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;

public class CacheLoaderTest4 {

    @Test
    public void testCacheeRefresh() throws InterruptedException {

        CacheLoader<String, Long> cacheLoader = CacheLoader.from(k -> System.currentTimeMillis());

        LoadingCache<String,Long> cache = CacheBuilder.newBuilder()
                .refreshAfterWrite(2,TimeUnit.SECONDS)//设置写入后刷新时间(刷新个人理解为过期),如果把这个设置注释掉,会测试失败,实现上,guava并不是内部有一个线程不断检查过期元素,而是只记录这个的刷新时间,请求时判断,如果超过这个时间,则不去缓存中拿
                .build(cacheLoader);

        Long result1 = cache.getUnchecked("Alex");
        TimeUnit.SECONDS.sleep(3);
        Long result2 = cache.getUnchecked("Alex");

        assertThat(result1.longValue() != result2.longValue(),equalTo(true));
    }


}

4、cache的preload(预加载)

作用:cache启动时,加载某些元素入缓存,但是机制存在一个缺陷:预加载的缓存元素与缓存本身指定的规则如果使用不当,会出现不一致的地方,需要注意。

package com.mzj.guava.cache.guava;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.junit.Test;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;

/**
 * cache的preload(预加载)
 */
public class CacheLoaderTest5 {

    /**
     * 方案:
     *
     * 此方案存在问题:在创建CacheLoader时,限制(规定)了cache的value时key的uppercase
     *
     * 但是预加载的时候,可以不按照此规则向cache中放入数据
     *
     * 在进行业务逻辑处理时依然按照这个规则
     *
     * @throws InterruptedException
     */
    @Test
    public void testCachePreLoad() throws InterruptedException {

        CacheLoader<String, String> cacheLoader = CacheLoader.from(String::toUpperCase);//这里限制(规定)了cache的value时key的uppercase

        LoadingCache<String,String> cache = CacheBuilder.newBuilder()
                .build(cacheLoader);

        Map<String,String> preData = new HashMap<String,String>(){
            {
                put("alex","ALEX");
                put("mazhongjia","MAZHONGJIA");
                put("hello","hello");//没有按照规则向缓存放入数据
            }
        };

        cache.putAll(preData);//预加载

        assertThat(cache.size(),equalTo(2L));
        assertThat(cache.getUnchecked("alex"),equalTo("ALEX"));
    }


}

5、Removal通知

作用:缓存数据在删除时,进行通知

自定义RemovalListener并加入CacheBuilder

package com.mzj.guava.cache.guava;

import com.google.common.cache.*;
import org.hamcrest.core.Is;
import org.junit.Test;

import java.util.HashMap;
import java.util.Map;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;

/**
 * cache的removal通知(缓存中元素删除后通知)
 */
public class CacheLoaderTest6 {

    @Test
    public void testCacheRemovedNotification() throws InterruptedException {

        CacheLoader<String, String> cacheLoader = CacheLoader.from(String::toUpperCase);//这里限制(规定)了cache的value时key的uppercase

        RemovalListener<String, String> listener = notification -> {
            //如果是被逐出的
            if (notification.wasEvicted()) {
                /**
                 * RemovalCause:逐出原因枚举
                 *      EXPLICIT:手工逐出(wasEvicted方法返回false,不会进入此if判断)
                 *      REPLACED:被替换了(wasEvicted方法返回false,不会进入此if判断)
                 *      COLLECTED:垃圾回收,GC的时候被逐出
                 *      EXPIRED:时间过期,被逐出
                 *      SIZE:数量逐出
                 */
                RemovalCause removalCause = notification.getCause();
                assertThat(removalCause, Is.is(RemovalCause.SIZE));
                assertThat(notification.getKey(), equalTo("Alex"));
            }
        };

        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(3)//设置缓存数量为3
                .removalListener(listener)//添加remove listener
                .build(cacheLoader);

        cache.getUnchecked("Alex");
        cache.getUnchecked("mazhongjia");
        cache.getUnchecked("huna");
        cache.getUnchecked("maxiaotang");
    }


}

6、CacheStats

1、作用:统计,cache的命中率、命中数、miss率、miss数量

package com.mzj.guava.cache.guava;

import com.google.common.cache.*;
import org.hamcrest.core.Is;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;

/**
 * cache的stat
 */
public class CacheLoaderTest7 {

    @Test
    public void testCacheStat() throws InterruptedException {

        CacheLoader<String, String> cacheLoader = CacheLoader.from(String::toUpperCase);
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .recordStats()//通过构造CacheBuilder时调用recordStats还开启记录CacheStats
                .build(cacheLoader);

        assertThat(cache.getUnchecked("alex"),equalTo("ALEX"));

        /**
         * 获取cache的状态对象,CacheStats是不可变对象,把CacheStats设计成final的好处是:如果有多个线程访问cache,不同线程获取CacheStats之间互不影响
         */
        CacheStats stats = cache.stats();

        //目前还没有请求从cache中获取到数据
        assertThat(stats.hitCount(),equalTo(0L));//命中数
        assertThat(stats.missCount(),equalTo(1L));//miss数
        assertThat(stats.hitRate(),equalTo(0.0D));//命中率
        assertThat(stats.missRate(),equalTo(1.0D));//miss率
    }


}

7、CacheBuilderSpec

1、作用:之前小节创建LoadingCache的方式都是通过CacheBuilder在代码中创建,以构造器的方式设置各种属性,除此之外,也可通过配置文件的方式创建,这种创建方式常用在spring ioc容器中创建时使用。

原理:实际上,CacheBuilderSpec就是通过规定的一个字符串格式(属性名1=属性值1,属性名2=属性值2)解析得到各部分属性、值来创建CacheBuilder。这样就可以将这个属性字符串在外部进行配置了。

package com.mzj.guava.cache.guava;

import com.google.common.cache.*;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;

/**
 * cache的CacheBuilderSpec
 */
public class CacheLoaderTest8 {

    @Test
    public void testacheBuilderSpec() throws InterruptedException {

        String spec = "maximumSize=5,recordStats";
        CacheBuilderSpec cacheBuilderSpec = CacheBuilderSpec.parse(spec);

        CacheLoader<String, String> cacheLoader = CacheLoader.from(String::toUpperCase);
        LoadingCache<String, String> cache = CacheBuilder.from(cacheBuilderSpec)
                .build(cacheLoader);

        assertThat(cache.getUnchecked("alex"),equalTo("ALEX"));

        CacheStats stats = cache.stats();

        //目前还没有请求从cache中获取到数据
        assertThat(stats.hitCount(),equalTo(0L));//命中数
        assertThat(stats.missCount(),equalTo(1L));//miss数
        assertThat(stats.hitRate(),equalTo(0.0D));//命中率
        assertThat(stats.missRate(),equalTo(1.0D));//miss率
    }
}

完整代码示例:

https://github.com/mazhongjia/googleguava/tree/master/src/main/java/com/mzj/guava/cache/guava

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值