20191109 guava构建本地缓存

多级缓存设计

缓存分为本地缓存和分布式缓存(远程缓存)。

以java为例,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着jvm的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

本地缓存----->远程缓存---->mysql数据库

使用本地缓存的好处:

1)减少和redis的交互。

2)速度上,本地缓存是最快的(数据无变化,就算有并发也没关系)

3)本地缓存随着当前实例的销毁而销毁。

代码落地:在Controller中,使用全局变量做本地缓存,比如机等级,机构类型等(不需要每次都跨服务查询)

本地缓存的实现方案有多种:使用纯java的ehcache作为本地缓存等

/**
 * Created on 2019/12/27 14:15
 * author:crs
 * Description:测试guava的基本功能
 */
public class TestGuavaCache {
    public static void main(String[] args) {


        //缓存工具对象,而非缓存本身
        //CacheBuilder类,Cache类,LoadingCache类,CacheLoader类
        //方法:getIfPresent()
        Cache<String, Object> cacheUtils = CacheBuilder.newBuilder().build();
        cacheUtils.put("key-001", "hello");
        //如果存在就返回value,如果不存在就返回null
        System.out.println(cacheUtils.getIfPresent("key-001"));

        //存取对象
        RedisEntity redisEntity = new RedisEntity("crs", 22);
        cacheUtils.put("key-002", redisEntity);
        try {
            //todo:取对象时,这个回调是做什么用的?
            RedisEntity result = (RedisEntity) cacheUtils.get("key-003", new Callable<RedisEntity>() {

                @Override
                public RedisEntity call() throws Exception {
                    //如果Key对应的value值不存在,则调用call方法进行填充获取。
                    return new RedisEntity("key-003", 88);
                }
            });
            System.out.println(result.toString());
        } catch (ExecutionException e) {
            e.printStackTrace();
        }


        //存取集合
        ArrayList<RedisEntity> list = new ArrayList<>();
        list.add(redisEntity);
        Cache<String, List> cacheList = CacheBuilder.newBuilder().build();
        cacheList.put("list", list);
        try {
            List<RedisEntity> listResult = cacheList.get("list", new Callable<List>() {
                @Override
                public List call() throws Exception {
                    return null;
                }
            });
            for (RedisEntity item : listResult) {
                System.out.println("存取list集合" + item);

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

        //存取Map
        //首先存数据前,首先要指定存入数据的类型,然后创建不同的Cache对象.
        //不同的数据类型,需要不同的Cache对象。
        Cache<String, Map<String, RedisEntity>> cacheMap = CacheBuilder.newBuilder().build();
        HashMap<String, RedisEntity> map = new HashMap<>();
        map.put("map", redisEntity);
        cacheMap.put("map", map);
        try {
            Map<String, RedisEntity> mapResult = cacheMap.get("map", new Callable<Map<String, RedisEntity>>() {
                @Override
                public Map<String, RedisEntity> call() throws Exception {
                    return null;
                }
            });
            System.out.println("存取map集合" + mapResult.get("map"));
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        //设置过期时间,多种过期策略
        //,单个删除,批量删除,全部删除。
        cacheUtils.invalidate("key");

        //todo:写入缓存后三秒过期
        Cache<String, String> cache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.SECONDS).build();
        cache.put("cache-001", "value-001");
        System.out.println(cache.getIfPresent("cache-001"));
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(cache.getIfPresent("cache-001"));

        //todo:访问后,如果三秒没再次访问就过期
        Cache<String, Object> accessCache = CacheBuilder.newBuilder().expireAfterAccess(3, TimeUnit.SECONDS).build();
        accessCache.put("cache-002", "value-002");
        //获取字符串不是get方法,而是getIfPresent()
        System.out.println(accessCache.getIfPresent("cache-002"));
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("三秒后再次访问" + accessCache.getIfPresent("cache-002"));

        //todo:设置过期时间,写入后每隔三秒刷新一次。
        LoadingCache<String, Object> refreshCache = CacheBuilder.newBuilder().refreshAfterWrite(3, TimeUnit.SECONDS)
                .recordStats()
                .build(new CacheLoader<String, Object>() {
                    public Object load(String key) {
                        return UUID.randomUUID().toString();
                    }
                });
        refreshCache.put("cache-003", "value-003");
        for (int i = 0; i < 10; i++) {
            System.out.println("每隔三秒刷新一次" + refreshCache.getIfPresent("cache-003"));
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //打印缓存的命中率信息
        System.out.println(refreshCache.stats().toString());

        System.out.println("cpu的核数:" + Runtime.getRuntime().availableProcessors());


    }

    /**
     * 多核cpu测试
     */
    @Test
    public void testProcessors() {
        //多核cpu可以多个线程同时执行。Cache创建的时候,可以设置多少个线程执行?
        System.out.println("cpu的核数:" + Runtime.getRuntime().availableProcessors());
    }


    /**
     * 测试缓存的创建
     */
    @Test
    public void testCreateCache() {
        Cache<String, String> cache = CacheBuilder.newBuilder()
                .weakKeys() //防止OOM
                .weakValues() //防止OOM
                .maximumSize(200)
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .concurrencyLevel(Runtime.getRuntime().availableProcessors())
                .recordStats()
                .build();
        cache.put("testCreateCache", "testCreateCache");
        System.out.println(cache.getIfPresent("testCreateCache"));
    }

}

如果Key对应的value值不存在,则调用call方法进行填充获取。

Callable只有在缓存值不存在时,才会调用。

 

在guava中数据的移除分为被动移除和主动移除两种。

被动移除数据的方式,guava默认提供了三种方式:

1.基于大小的移除:看字面意思就知道就是按照缓存的大小来移除,如果即将到达指定的大小,那就会把不常用的键值对从cache中移除。

定义的方式一般为 CacheBuilder.maximumSize(long),注意点,

其一,这个size指的是cache中的条目数(本地缓存的个数),不是内存大小或是其他;

其二,并不是完全到了指定的size系统才开始移除不常用的数据的,而是接近这个size的时候系统就会开始做移除的动作;

其三,如果一个键值对已经从缓存中被移除了,你再次请求访问的时候,如果cachebuild是使用cacheloader方式的,那依然还是会从cacheloader中再取一次值,如果这样还没有,就会抛出异常

2.基于时间的移除:guava提供了两个基于时间移除的方法

expireAfterAccess(long, TimeUnit)  这个方法是根据某个键值对最后一次访问之后多少时间后移除

expireAfterWrite(long, TimeUnit)  这个方法是根据某个键值对被创建或值被替换后多少时间移除

3.基于引用的移除:这种移除方式主要是基于java的垃圾回收机制,根据键或者值的引用关系决定移除(java中的四种引用类型)。

 

主动移除数据方式,主动移除有三种方法:

1.单独移除用 Cache.invalidate(key)

2.批量移除用 Cache.invalidateAll(keys)

3.移除所有用 Cache.invalidateAll()

 

数据移除方式什么时候使用? 创建Cache对象的时候使用。

 

JVM 是根据可达性分析算法找出需要回收的对象,判断对象的存活状态都和引用有关。

四种引用关系。可达性分析算法和四种个引用类型。

虽然对象可达,但是由于四种引用关系的存在,仍有可能被回收。比如若引用和软引用。

 

事件回调其实是一种常见的设计模式,在 Java 中利用接口来实现回调,所以需要定义一个接口。

 

 

缓存的淘汰策略:FIFO,LRU,LFU等;

缓存的回收策略:推荐基于数量和容量的回收。基于软/弱引用的回收。

缓存的过期策略分为固定时间和相对时间。expireAfterWrite() expireAfterAccess()

缓存的刷新策略支持定时刷新和显式刷新两种方式。

 

使用缓存合理性问题

1、热点数据,缓存才有价值;冷数据 根据内存淘汰策略,很容易被移除内存。冷数据不仅占用内存,而且价值不大。

2、频繁修改的数据,看情况考虑使用缓存。数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

3、数据不一致性一般会对缓存设置失效时间,一旦超过失效时间,就要从数据库重新加载,因此应用要容忍一定时间的数据不一致。

4、缓存更新机制:使用缓存过程中,我们经常会遇到缓存数据的不一致性和与脏读现象,我们有什么解决策略呢?一般情况下,我们采取缓存双淘汰机制,在更新数据库的时候淘汰缓存。此外,设定超时时间,例如30分钟。极限场景下,即使有脏数据入cache,这个脏数据也最多存在三十分钟。

(缓存双淘汰机制:更新数据库时淘汰(主动删除 ),设定超时时间)

5、缓存可用性。缓存是提高数据读取性能的,缓存数据丢失和缓存不可用不会影响应用程序的处理。因此,一般的操作手段是,如果Redis出现异常,我们手动捕获这个异常,记录日志,并且去数据库查询数据返回给用户。

6、缓存预热。在新启动的缓存系统中,如果没有任何数据,在重建缓存数据过程中,系统的性能和数据库复制都不太好,那么最好的缓存系统启动时就把热点数据加载好,例如对于缓存信息,在启动缓存加载数据库中全部数据进行预热。一般情况下,我们会开通一个同步数据的接口,进行缓存预热。(缓存预热和重建缓存数据的过程中)

 

使用Guava cache构建本地缓存

1、当前应用都是多线程的,缓存需要支持并发的写入。

2、缓存的过期策略(固定时间过期和相对时间过期,10分钟内未访问则使缓存过期)。在java中甚至可以使用软引用,弱引用的过期策略。

3、淘汰策略;由于本地缓存是存放在内存中,我们往往需要设置一个容量上限和淘汰策略来防止出现内存溢出的情况。内存溢出。缓存的最大容量------->防止内存溢出。

4、由于本地缓存是将计算结果缓存到内存中,所以我们往往需要设置一个最大容量来防止出现内存溢出的情况。这个容量可以是缓存对象的数量,也可以是一个具体的内存大小。在Guva中仅支持设置缓存对象的数量。(缓存的最大容量:缓存对象的数量,具体的内存大小)

由于缓存的最大容量恒定,为了提高缓存的命中率

为什么会出现缓存淘汰策略?因为缓存的最大容量是恒定的,提高缓存的命中率。

缓存淘汰策略

1)FIFO:First In First Out,先进先出。一般采用队列的方式实现。这种淘汰策略仅仅是保证了缓存数量不超过我们所设置的阈值,而完全没有考虑缓存的命中率。所以在这种策略极少被使用。

2)LRU:Least Recently Used,最近最少使用; 最近最久使用(时间角度)

该算法其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

所以该算法是淘汰最后一次使用时间离当前最久的缓存数据保留最近访问的数据。所以该种算法非常适合缓存“热点数据”。但是该算法在缓存周期性数据时,就会出现缓存污染,也就是淘汰了即将访问的数据,反而把不常用的数据读取到缓存中。

3)LFU:Least Frequently Used,最不经常使用。(使用次数)

该算法的核心思想是“如果数据在以前被访问的次数最多,那么将来被访问的几率就会更高”。所以该算法淘汰的是历史访问次数最少的数据。

一般情况下,LFU效率要优于LRU,且能够避免周期性或者偶发性的操作导致缓存命中率下降的问题。但LFU需要记录数据的历史访问记录,一旦数据访问模式改变,LFU需要更长时间来适用新的访问模式,即:LFU存在历史数据影响将来数据的“缓存污染”效用。

合理的使用淘汰算法能够很明显的提升缓存命中率,但是也不应该一味的追求命中率,而是应在命中率和资源消耗中找到一个平衡。(缓存命中率和资源消耗之间的平衡点)。在guava中默认使用LRU淘汰算法。

Guva是google开源的一个公共java库。

当缓存不存在时,会通过CacheLoader自动加载,该方法会抛出ExecutionException异常;

如何计算缓存容量?

key.getBytes().length + value.getBytes().length; 计算一个缓存占用的内存大小。当前字符串占用了多少内存大小,占用了多少字节。

在java中有对象自动回收机制,依据程序员创建对象的方式不同,将对象由强到弱分为强引用、软引用、弱引用、虚引用。

相对时间一般是相对于访问时间,也就是每次访问后,会重新刷新该缓存的过期时间。

 

如何限制内存占用(缓存对内存的)?通常都设定为自动回收元素。

你愿意消耗一些内存空间来提升速度(以空间换效率,或者说以空间换性能

本地缓存的使用场景:

1)你愿意消耗一些内存空间来提升速度。

2)你预料到某些键会被查询一次以上。

3)缓存中存放的数据总量不会超出内存容量。

如何实现缓存同步?

缓存过期策略?

缓存对内存的占用。

本地缓存:

1)十分钟过期,存储十分钟后,就会被回收,释放内存;如果有新的访问,在进行存储;

2)对缓存数量的显示,只能缓存1000个键值对;缓存个数达到一定值后就会按照某种策略回收内存中的缓存;

3)重新启动微服务后,本地缓存中的数据被清空;也能到达释放缓存的目的;

这三种都能回收内存。本地缓存占用的是当前应用程序的内存空间?运行时内存区域?

 

java中的引用类型

1、强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,垃圾回收器不会自动回收一个被引用的强引用对象,而是会直接抛出OutOfMemoryError错误,使程序异常终止。

2、当内存充足时,GC不会主动回收软引用对象,而当内存不足时软引用对象就会被回收。

3、因为无论内存是否充足,弱引用对象都有可能被回收;

 

使用CacheLoader加载新值,并存入缓存中。

显式清除:可以实现缓存同步;个别清除:Cache.invalidate(key)

缓存命中率。

缓存穿透、缓存击穿、缓存雪崩

 

相对时间一般是相对于访问时间,也就是每次访问后,会重新刷新该缓存的过期时间。

 

项目中为何要用缓存?(缓存层,存储层)

最直接的表现就是减轻数据库的压力。避免因为数据读取频繁或过大而影响数据库性能,降低程序宕机的可能性。

https://my.oschina.net/u/2270476/blog/1805749

https://blog.csdn.net/liuxiao723846/article/details/80067275

https://www.cnblogs.com/chanshuyi/p/how_to_deal_with_massive_request_in_redis.html

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Guava Cache是Google Guava库中提供的一种本地缓存解决方案。它是一个基于内存的缓存,可以在应用程序内部存储数据,提高应用程序性能。 Guava Cache提供了以下特性: 1. 自动加载:当缓存中不存在某个键的值时,可以自动加载生成该值。 2. 自动移除:缓存中的某些条目可以在一定时间内自动过期,或者可以使用大小限制来限制缓存中的条目数。 3. 针对不同的缓存数据设置不同的过期时间、存活时间、最大值、最小值等。 4. 支持同步和异步缓存。 使用Guava Cache非常简单,只需要按以下步骤操作: 1. 引入Guava库。 2. 创建一个CacheBuilder对象,用于配置缓存。 3. 调用build()方法创建一个Cache对象。 4. 使用put()方法向缓存中添加数据。 5. 使用get()方法从缓存中读取数据,如果缓存中不存在该键对应的值,则可以自动加载。 6. 使用invalidate()方法从缓存中移除数据。 下面是一个简单的示例: ```java import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; public class GuavaCacheExample { public static void main(String[] args) throws ExecutionException { // 创建一个CacheBuilder对象 CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder() .maximumSize(100) // 设置缓存最大条目数 .expireAfterWrite(10, TimeUnit.MINUTES); // 设置缓存过期时间 // 创建一个Cache对象 LoadingCache<String, String> cache = cacheBuilder.build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { System.out.println("loading " + key); // 自动加载数据 return "value-" + key; } }); // 添加数据到缓存中 cache.put("key1", "value1"); cache.put("key2", "value2"); // 从缓存中读取数据 System.out.println(cache.get("key1")); // 输出"value1" System.out.println(cache.get("key3")); // 输出"loading key3"和"value-key3" // 移除缓存中的数据 cache.invalidate("key1"); System.out.println(cache.get("key1", () -> "default")); // 输出"default" } } ``` 在这个示例中,我们使用CacheBuilder对象配置了缓存的最大条目数和过期时间。我们还使用CacheLoader对象创建了一个自动加载的缓存,当缓存中不存在某个键的值时,可以自动加载生成该值。我们使用put()方法向缓存中添加了两个数据,使用get()方法从缓存中读取了两个数据,并使用invalidate()方法从缓存中移除了一个数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值