• GUAVA本地缓存01_概述、优缺点、创建方式、回收机制、监听器、统计、异步锁定


    ①. 本地缓存 - 背景

    • ①. 在高性能的服务架构设计中,缓存是一个不可或缺的环节。在实际的项目中,我们通常会将一些热点数据存储到Redis或Memcached 这类缓存中间件中,只有当缓存的访问没有命中时再查询数据库。在提升访问速度的同时,也能降低数据库的压力

    • ②. 随着不断的发展,这一架构也产生了改进,在一些场景下可能单纯使用Redis类的远程缓存已经不够了,还需要进一步配合本地缓存使用,例如Guava cache或Caffeine,从而再次提升程序的响应速度与服务性能。于是,就产生了使用本地缓存作为一级缓存,再加上远程缓存作为二级缓存的两级缓存架构

    • ③. 在先不考虑并发等复杂问题的情况下,两级缓存的访问流程可以用下面这张图来表示:
      在这里插入图片描述

    @Service
    public class PositionServiceImpl implements PositionService {
    
        @Autowired
        PositionMapper positionMapper;
    
        @Autowired
        RedisTemplate redisTemplate;
    
        private static Cache<Object, Object> cache = CacheBuilder.newBuilder().expireAfterWrite(5,TimeUnit.SECONDS).build();
    
        @Override
        public List<Position> getHotPosition() throws ExecutionException {
    		//从guava本地缓存中获取数据,如果没有则从redis中回源
            Object value = cache.get("position", new Callable() {
                @Override
                public Object call() throws Exception {
    
                    return getHotPositionListFromRedis();
                }
            });
    
            if(value != null){
                return (List<Position>)value;
            }
    
            return null;
        }
    
        @Override
        public List<Position> getHotPositionListFromRedis()  {
    
            Object position = redisTemplate.opsForValue().get("position");
            System.out.println("从redis中获取数据");
    
            if(position == null) {
                //从mysql中获取数据
                List<Position> positionList = positionMapper.select(null);
                System.out.println("从mysql中获取数据");
                //同步至redis
                redisTemplate.opsForValue().set("position",positionList);
                System.out.println("同步至redis");
                redisTemplate.expire("position",5, TimeUnit.SECONDS);
    			//getHotPositionListFromRedis在guava不存在的时候调用,这里会将数据写入到guava本地缓存中
                return positionList;
            }
    
            return (List<Position>)position;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    ②. 本地缓存 - 优缺点

    • ①. 优点:
    1. 查询频率高的场景(重复查,并且耗时的情况)
    2. 数量少,不会超过内存总量(缓存中存放的数据不会超过内存空间)
    3. 以空间换取时间,就是你愿意用内存的消耗来换取读取性能的提升
    • ②. 不足
    1. 数据存放在本机的内存,未持久化到硬盘,机器重启会丢失
    2. 单机缓存,受机器容量限制
    3. 多个应用实例出现缓存数据不一致的问题

    ③. Guava Cache介绍

    • ①. JVM缓存,是堆缓存。其实就是创建一些全局容器,比如List、Set、Map等。这些容器用来做数据存储,这样做的问题:
    1. 不能按照一定的规则淘汰数据,如 LRU,LFU,FIFO
    2. 清除数据时的回调通知
    3. 并发处理能力差,针对并发可以使用CurrentHashMap,但缓存的其他功能需要自行实现缓存过期处理,缓存数据加载刷新等都需要手工实现
    • ②. Guava是Google提供的一套Java工具包,而Guava Cache是一套非常完善的本地缓存机制(JVM缓存)
      Guava cache的设计来源于CurrentHashMap,可以按照多种策略来清理存储在其中的缓存值且保持很高的并发读写性能。

    • ③. Guava Cache的优势:

    1. 缓存过期和淘汰机制
      在GuavaCache中可以设置Key的过期时间,包括访问过期和创建过期
      GuavaCache在缓存容量达到指定大小时,采用LRU的方式,将不常使用的键值从Cache中删除
    2. 并发处理能力
      GuavaCache类似CurrentHashMap,是线程安全的。
      提供了设置并发级别的api,使得缓存支持并发的写入和读取
      采用分离锁机制,分离锁能够减小锁力度,提升并发能力分离锁是分拆锁定,把一个集合看分成若干partition, 每个partiton一把锁。ConcurrentHashMap就是分了16个区域,这16个区域之间是可以并发的。GuavaCache采用Segment做分区
    3. 更新锁定
      一般情况下,在缓存中查询某个key,如果不存在,则查源数据,并回填缓存(Cache AsidePattern)
      在高并发下会出现,多次查源并重复回填缓存,可能会造成源的宕机(DB),性能下降
      GuavaCache可以在CacheLoader的load方法中加以控制,对同一个key,只让一个请求去读源并回填缓存,其他请求阻塞等待

    ④. Guava - 三种创建方式

    • ①. 导入依赖
    	
    	<dependency>
    		<groupId>com.google.guavagroupId>
    		<artifactId>guavaartifactId>
    		<version>25.0-jreversion>
    	dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • ②. 方式一:使用CacheLoader插件
    1. 使用场景:首先问自己一个问题:有没有合理的默认方法来加载或计算与键关联的值?如果有的话,你应当使用CacheLoader。首选使用,因为它可以更容易地推断所有缓存内容的一致性
    2. LoadingCache是附带CacheLoader构建而成的缓存实现。创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法
        private static void method1() throws ExecutionException, InterruptedException {
            LoadingCache<String, String> cacheLoadInit = CacheBuilder
                    .newBuilder()
                    .initialCapacity(5)
                    .maximumSize(100)
                    .expireAfterWrite(5000, TimeUnit.MILLISECONDS) //
                    .removalListener(new RemovalListener<String, String>() {
                        @Override
                        public void onRemoval(RemovalNotification<String, String> removalNotification) {
                            System.out.println(removalNotification.getKey() + ":remove==> :" + removalNotification.getValue());
                        }
                    })
                    .build(
                            new CacheLoader<String, String>() {
                                @Override
                                public String load(String key) throws Exception {
                                    System.out.println("first init.....");
                                    return key;
                                }
                            });
            //由于CacheLoader可能抛出异常,LoadingCache.get(K)也声明为抛出ExecutionException异常
            String key = cacheLoadInit.get("first-name");
            String key2 = cacheLoadInit.get("first-name");
            System.out.println(key);
            TimeUnit.MILLISECONDS.sleep(5000);
            System.out.println("==========");
            //设置了5s的过期时间,5s后会重新加载缓存
            String keySecond = cacheLoadInit.get("first-name");
            System.out.println("5s后重新加载缓存数据" + keySecond);
            /**
             * first init.....
             * first-name
             * ==========
             * first init.....
             * 5s后重新加载缓存数据first-name
             */
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • ③. 方式二:Cache.put方法直接插入
        private static void method2(){
            Cache<String, String> cache = CacheBuilder.newBuilder()
                    .expireAfterWrite(3, TimeUnit.SECONDS)
                    .initialCapacity(1)
                    .maximumSize(2)
                    .removalListener(new RemovalListener<String, String>() {
                        @Override
                        public void onRemoval(RemovalNotification<String, String> notification) {
                            System.out.println(notification.getKey()+"移除了,value:"+notification.getValue());
                        }
                    })
                    .build();
            // getIfPresent(key):从现有的缓存中获取,如果缓存中有key,则返回value,如果没有则返回null
            String key1 = cache.getIfPresent("java金融1");
            System.out.println(key1);//null
            if(StringUtils.isEmpty(key1)){
                /**
                 * null
                 * value - java金融2
                 * value - java金融3
                 */
                cache.put("java金融1","value - java金融1");
                cache.put("java金融2","value - java金融2");
                cache.put("java金融3","value - java金融3");
                System.out.println(cache.getIfPresent("java金融1"));
                System.out.println(cache.getIfPresent("java金融2"));
                System.out.println(cache.getIfPresent("java金融3"));
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • ④. 方式三:调用get时传入一个Callable实例
    /**
         所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable)方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式"如果有缓存则返回;否则运算、缓存、然后返回"。
         Cache cache = CacheBuilder.newBuilder()
         .maximumSize(1000)
         .build(); // look Ma, no CacheLoader
         ...
         try {
         // If the key wasn't in the "easy to compute" group, we need to
         // do things the hard way.
         cache.get(key, new Callable() {
        @Override
        public Value call() throws AnyException {
        return doThingsTheHardWay(key);
        }
        });
         } catch (ExecutionException e) {
         throw new OtherException(e.getCause());
         }
         * @throws ExecutionException
         */
        private static void method3() throws ExecutionException {
            Cache<String, String> cache = CacheBuilder.newBuilder()
                    .maximumSize(3)
                    .initialCapacity(1)
                    .expireAfterWrite(1,TimeUnit.MINUTES)
                    .build();
            cache.get("key1",new Callable<String>() {
                @Override
                public String call() throws Exception {
                    return "value1";
                }
            });
            /**
             * value1
             * null
             */
            System.out.println(cache.getIfPresent("key1"));
            System.out.println("手动清除缓存");
            cache.invalidateAll();
            System.out.println(cache.getIfPresent("key1"));
            System.out.println(cache.getIfPresent("key2"));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • ⑤. cache.get方法的两个参数:key,Callable对象
    1. Cache的get方法有两个参数,第一个参数是要从Cache中获取记录的key,第二个记录是一个Callable对象
    2. 当缓存中已经存在key对应的记录时,get方法直接返回key对应的记录。如果缓存中不包含key对应的记录,Guava会启动一个线程执行Callable对象中的call方法,call方法的返回值会作为key对应的值被存储到缓存中,并且被get方法返回
    3. Guava可以保证当有多个线程同时访问Cache中的一个key时,如果key对应的记录不存在,Guava只会启动一个线程执行get方法中Callable参数对应的任务加载数据存到缓存。当加载完数据后,任何线程中的get方法都会获取到key对应的值

    ⑤. Guava - 如何回收缓存

    • ①. 基于容量的回收size-based eviction(maximumSize)
       private static void maximumSizeMethod() throws ExecutionException {
            LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                    .initialCapacity(1)
                    .maximumSize(3)
                    .build(new CacheLoader<String, String>() {
                        @Override
                        public String load(String key) throws Exception {
                            return key + " - value";
                        }
                    });
            cache.get("1");
            cache.get("2");
            cache.get("3");
            cache.get("4");
            // 最大maximumSize=3 这里添加了四个元素进来了
            // 这里是采用的URL 和 FIFO的方式去进行移除元素
            /**
             * null
             * 2 - value
             * 3 - value
             * 4 - value
             */
            System.out.println(cache.getIfPresent("1"));
            System.out.println(cache.getIfPresent("2"));
            System.out.println(cache.getIfPresent("3"));
            System.out.println(cache.getIfPresent("4"));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • ②. 定时回收Timed Eviction:CacheBuilder提供两种定时回收的方法:
    1. expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样
    2. expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问创建或覆盖,则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的
    	private static void expireAfterAccessMethod() throws InterruptedException {
            // 隔多长时间后没有被访问过的key被删除
            Cache<String, String> build = CacheBuilder.newBuilder()
                    //缓存中的数据 如果3秒内没有访问则删除
                    .expireAfterAccess(3000, TimeUnit.MILLISECONDS)
                    .build();
            build.put("1","1 - value");
            build.put("2","2 - value");
            build.put("3","3 - value");
            Thread.sleep(1000);
            build.getIfPresent("1");
            Thread.sleep(2100);
            // 这里在停顿了1s后,重新获取了,这个时候再次停顿2s,2,3没有发访问,过期了
            System.out.println(build.getIfPresent("1"));
            // 停顿3s后,这里也过期了
            Thread.sleep(3000);
            System.out.println("这里已经过了3s了");
            System.out.println(build.getIfPresent("1"));
        }
    	    private static void expireAfterWriteDemo() throws ExecutionException, InterruptedException {
            LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                    // 等同于expire ttl 缓存中对象的生命周期就是3秒
                    .expireAfterWrite(3, TimeUnit.SECONDS)
                    .initialCapacity(1)
                    .maximumSize(5)
                    .removalListener(new RemovalListener<String, String>() {
                        @Override
                        public void onRemoval(RemovalNotification<String, String> removalNotification) {
                            System.out.println("onRemoval ==> key:" + removalNotification.getKey() + "value:" + removalNotification.getValue());
                        }
                    })
                    .build(new CacheLoader<String, String>() {
                        @Override
                        public String load(String s) throws Exception {
                            return s + " - value";
                        }
                    });
            cache.get("1");
            cache.get("2");
            Thread.sleep(1000);
            cache.getIfPresent("1");
            Thread.sleep(2100);
            // 虽然前面有访问,expireAfterWrite相当于ttl,这里返回的数据是null
            System.out.println(cache.getIfPresent("1"));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • ③. 基于引用的回收Reference-based Eviction(通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收。关于软引用个弱引用的概念可以参考强引用、弱引用、软引用、虚引用)
    1. CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它强或软引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式,使用弱引用键的缓存用而不是equals比较键。
    2. CacheBuilder.weakValues():使用弱引用存储值。当值没有其它强或软引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式,使用弱引用值的缓存用而不是equals比较值。
    3. CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定见上文,基于容量回收。使用软引用值的缓存同样用==而不是equals比较值。
    	public class GuavaExpireDemo {
    	    public static void main(String[] args) throws InterruptedException, ExecutionException {
    	        Cache<String, Object> cache = CacheBuilder.newBuilder()
    	                .maximumSize(3)
    	                .weakValues()
    	                .build();
    	        Person value = new Person(1,"tang");
    	        cache.put("1",value);
    	        // Person(id=1, name=tang)
    	        System.out.println(cache.getIfPresent("1"));
    	        value=new Person(2,"yang");
    	        // 强制垃圾回收
    	        System.gc();
    	        // null
    	        System.out.println(cache.getIfPresent("1"));
    	    }
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • ④. 显式清除:任何时候,你都可以显式地清除缓存项,而不是等到它被回收:
    1. 清除单个key:Cache.invalidate(key) //将key=1 删除 cache.invalidate(“1”);
    2. 批量清除key:Cache.invalidateAll(keys)//将key=1和2的删除 cache.invalidateAll(Arrays.asList(“1”,“2”));
    3. 清除所有缓存项:Cache.invalidateAll()//清空缓存 cache.invalidateAll();
    • ⑤. GuavaCache构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制
      GuavaCache是在每次进行缓存操作的时候,惰性删除 如get()或者put()的时候,判断缓存是否过期

    ⑥. Guava - 移除监听器

    • ①. 通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作

    • ②. 缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值

    	public class removalListenerDemo {
    	    public static void main(String[] args) {
    	        Cache cache = CacheBuilder.newBuilder()
    	                .initialCapacity(1)
    	                .maximumSize(3)
    	                .concurrencyLevel(1)
    	                .removalListener(new RemovalListener() {
    	                    @Override
    	                    public void onRemoval(RemovalNotification removalNotification) {
    	                        System.out.println("removalListener => key:" + removalNotification.getKey() + ",value:" + removalNotification.getValue());
    	                    }
    	                })
    	                .build();
    	        cache.put("key - 1","value - 1");
    	        System.out.println(cache.getIfPresent("key - 1"));
    	        cache.invalidate("key - 1");
    	        System.out.println(cache.getIfPresent("key - 1"));
    	        /**
    	         value - 1
    	         removalListener => key:key - 1,value:value - 1
    	         null
    	         */
    	    }
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    ⑦. Guava - 统计功能

    • ①. 统计功能是Guava cache一个非常实用的特性。可以通过CacheBuilder.recordStats() 方法启动了 cache的数据收集:
    1. Cache.stats(): 返回了一个CacheStats对象, 提供一些数据方法
    2. hitRate(): 请求点击率
    3. evictionCount(): 清除的个数
    	public class recordStatsDemo {
    	    public static void main(String[] args) throws ExecutionException {
    	        // 创建1块缓存,key和value都是integer类型,最大缓存个数是5,开启缓存统计功能
    	        // 使用LoadingCache,如果数据不存在就使用CacheLoader加载数据
    	        LoadingCache<Integer, Integer> cache = CacheBuilder.newBuilder().recordStats().maximumSize(5).
    	                build(new CacheLoader<Integer, Integer>() {
    	                    @Override
    	                    public Integer load(Integer id) throws Exception {
    	                        System.out.println("mock query db....");
    	                        if (id % 2 == 0) {
    	                            Thread.sleep(100);
    	                            throw new RuntimeException();
    	                        } else {
    	                            Thread.sleep(200);
    	                            return id * 10;
    	                        }
    	                    }
    	                });
    	        // 预先添加一条缓存数据
    	        cache.put(1, 100);
    	        //....CacheStats{hitCount=0, missCount=0, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=0}
    	        System.out.println("...." + cache.stats());
    	
    	        // 缓存命中,hitCount加1
    	        System.out.println("get data====" + cache.get(1));
    	        //....CacheStats{hitCount=1, missCount=0, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=0}
    	        System.out.println("...." + cache.stats());
    	
    	        // 没有命中缓存, missCount和loadSuccessCount加1,并增加totalLoadTime(纳秒为单位)
    	        System.out.println("get data====" + cache.get(3));
    	        //....CacheStats{hitCount=1, missCount=1, loadSuccessCount=1, loadExceptionCount=0, totalLoadTime=214873400, evictionCount=0}
    	        System.out.println("...." + cache.stats());
    	
    	        // 没有命中缓存, missCount和loadExceptionCount加1,并增加totalLoadTime(纳秒为单位)
    	        try {
    	            System.out.println("get data====" + cache.get(4));
    	        } catch (Exception e) {
    	            // ....CacheStats{hitCount=1, missCount=2, loadSuccessCount=1, loadExceptionCount=1, totalLoadTime=318706100, evictionCount=0}
    	            System.out.println("...." + cache.stats());
    	        }
    	
    	        // 手动清除缓存数据,或者是直接操作缓存底层数据,不会影响统计信息
    	        System.out.println("get data====" + cache.asMap().get(1));// 通过缓存底层数据结构,读取数据
    	        cache.invalidateAll();// 清空缓存
    	        //....CacheStats{hitCount=1, missCount=2, loadSuccessCount=1, loadExceptionCount=1, totalLoadTime=317725800, evictionCount=0}
    	        System.out.println("...." + cache.stats());
    	
    	        // 添加7条缓存数据,由于最大数目是5,所以evictionCount=2
    	        System.out.println("size===" + cache.size());
    	        cache.put(1, 100);
    	        cache.put(2, 100);
    	        cache.put(3, 100);
    	        cache.put(4, 100);
    	        cache.put(5, 100);
    	        cache.put(6, 100);
    	        cache.put(7, 100);
    	        //....CacheStats{hitCount=1, missCount=2, loadSuccessCount=1, loadExceptionCount=1, totalLoadTime=317725800, evictionCount=1}
    	        System.out.println("...." + cache.stats());
    	    }
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • ②. 有一点需要注意下:可以通过修改底层map给缓存添加数据,也可以获取数据。通过map获取数据,不会影响缓存统计;通过map添加数据,会影响evictionCount
    	private static void recordStatusMethod2() {
    	        LoadingCache cache = CacheBuilder.newBuilder().recordStats().maximumSize(5).
    	                build(new CacheLoader<Integer, Integer>() {
    	                    @Override
    	                    public Integer load(Integer id) throws Exception {
    	                        System.out.println("mock query db....");
    	                        if (id % 2 == 0) {
    	                            Thread.sleep(100);
    	                            throw new RuntimeException();
    	                        } else {
    	                            Thread.sleep(200);
    	                            return id * 10;
    	                        }
    	                    }
    	                });
    	        // 修改底层的map也会导致cache发生变化
    	        ConcurrentMap underlingMap = cache.asMap();
    	        underlingMap.put(1, 1);
    	        underlingMap.put(2, 2);
    	        underlingMap.put(3, 3);
    	        underlingMap.put(4, 4);
    	        underlingMap.put(5, 5);
    	        underlingMap.put(6, 6);
    	        underlingMap.put(7, 6);
    	        // underlingMap....{3=3, 6=6, 5=5, 7=6, 4=4} cache....5
    	        System.out.println("underlingMap...." + underlingMap+"cache...." + cache.size());
    	        // stats....CacheStats {hitCount=0, missCount=0, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=2}
    	        System.out.println("stats...." + cache.stats());
    	    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    ⑧. Guava - asMap视图

    • ①. asMap视图提供了缓存的ConcurrentMap形式,但asMap视图与缓存的交互需要注意:
    1. cache.asMap()包含当前所有加载到缓存的项。因此相应地,cache.asMap().keySet()包含当前所有已加载键;
    2. asMap().get(key)实质上等同于cache.getIfPresent(key),而且不会引起缓存项的加载。这和Map的语义约定一致。
    3. 所有读写操作都会重置相关缓存项的访问时间,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合视图上的操作。比如,遍历Cache.asMap().entrySet()不会重置缓存项的读取时间
    	public class GuavaAsMapDemo {
    	    public static void main(String[] args) throws ExecutionException {
    	        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
    	                .build(new CacheLoader<String, String>() {
    	                    @Override
    	                    public String load(String key) throws Exception {
    	                        return "value:" + key;
    	                    }
    	                });
    	        cache.get("1");
    	        cache.get("2");
    	        cache.get("3");
    	        ConcurrentMap<String, String> map = cache.asMap();
    	        Set<String> strings = map.keySet();
    	        System.out.println("获取到cache所有的key:"+strings);
    	        Collection<String> values = map.values();
    	        System.out.println("获取到cache所有的value:"+values);
    	        Set<Map.Entry<String, String>> set = map.entrySet();
    	        for (Map.Entry<String, String> stringStringEntry : set) {
    	            System.out.println(stringStringEntry.getKey()+"<====>"+stringStringEntry.getValue());
    	        }
    	        map.get("4");
    	        System.out.println("从缓存中获取key4:"+cache.getIfPresent("4"));
    	        /**
    	         * 获取到cache所有的key:[1, 3, 2]
    	         * 获取到cache所有的value:[value:1, value:3, value:2]
    	         * 1<====>value:1
    	         * 3<====>value:3
    	         * 2<====>value:2
    	         */
    	    }
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    ⑨. 异步锁定 - refreshAfterWrites

    • ①. refreshAfterWrite:当缓存项上一次更新操作之后的多久会被刷新。第一个请求进来,执行load把数据加载到内存中同步过程,指定的过期时间内比如10秒,都是从cache里读取数据。过了10秒后,没有请求进来,不会移除key。再有请求过来,才则执行reload,在后台异步刷新的过程中,如果当前是刷新状态,访问到的是旧值。刷新过程中只有一个线程在执行刷新操作,不会出现多个线程同时刷新同一个key的缓存。在吞吐量很低的情况下,如很长一段时间内没有请求,再次请求有可能会得到一个旧值这个旧值可能来自于很长时间之前,这将会引发问题。可以使用expireAfterWrite和refreshAfterWrite搭配使用解决这个问题
    	/**
    	 refreshAfterWrites — 失效后异步刷新缓存
    	 使用refreshAfterWrites后,需要实现CacheLoader的reload方法。需要在方法中创建一个ListenableFutureTask,然后将这个task提交给线程池去异步执行。这样的话,缓存失效后重新加载就变成了异步,加载期间尝试获取取缓存的线程也不会被阻塞。而是获取到加载之前的值。加载完毕之后,各个线程就能取到最新的值。
    	 总结:refreshAfterWrites是异步去刷新缓存的方法,使用过期的旧值快速响应。而expireAfterWrites缓存失效后线程需要同步等待加载结果,可能会造成请求大量堆积的问题。
    	 **/
    	@SuppressWarnings("all")
    	public class TestGuavaCache {
    	    private static ExecutorService executorService = Executors.newFixedThreadPool(5);
    	
    	
    	    static LoadingCache<String, String> cache = CacheBuilder.
    	            newBuilder().
    	            refreshAfterWrite(2, TimeUnit.SECONDS).
    	            build(new CacheLoader<String, String>() {
    	                //同步加载缓存
    	                @Override
    	                public String load(String key) throws Exception {
    	                    System.out.println("============");
    	                    return "";
    	                }
    	
    	                //异步加载缓存
    	                @Override
    	                public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
    	                    //定义任务。
    	                    ListenableFutureTask<String> futureTask = ListenableFutureTask.create(() -> {
    	                        try {
    	                            Thread.sleep(1000);
    	                        } catch (InterruptedException e) {
    	                            e.printStackTrace();
    	                        }
    	                        System.out.println("================");
    	                        return "曹操";
    	                    });
    	                    //异步执行任务
    	                    executorService.execute(futureTask);
    	                    return futureTask;
    	                }
    	
    	            });
    	
    	    public static void main(String[] args) throws ExecutionException, InterruptedException {
    	        cache.put("name", "李白");
    	        //第一次获取缓存:李白
    	        System.out.println(cache.get("name"));
    	        //睡眠2s后,再次获取,此时缓存失效,异步的加载缓存。但是线程是立即返回“旧结果”。
    	        Thread.sleep(2000);
    	        System.out.println(cache.get("name"));
    	        Thread.sleep(1000);
    	        System.out.println(cache.get("name"));
    	    }
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • ②. expireAfterWrite和refreshAfterWrite搭配使用
    	 CacheBuilder.newBuilder()
         .refreshAfterWrite(20, TimeUnit.MINUTES)
         .expireAfterWrite(30, TimeUnit.MINUTES)
         .maximumSize(1)
         .build(new CacheLoader<String, List<Map<String, Long>>>() {
             @Override
             public List<Map<String, Long>> load(String s) throws Exception {
                 return queryData();
             }
    
             @Override
             public ListenableFuture<List<Map<String, Long>>> reload(String key, List<Map<String, Long>> oldValue)
                     throws Exception {
                 ListenableFutureTask<List<Map<String, Long>>> task = ListenableFutureTask
                         .create(() -> queryData());
                 executorService.execute(task);
                 return task;
             }
         });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ⑩. 核心原理之数据结构

    • ①. Guava Cache的数据结构跟ConcurrentHashMap类似,但也不完全一样。最基本的区别是
      ConcurrentMap会一直保存所有添加的元素,直到显式地移除

    • ②. 相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。其数据结构图如下:
      在这里插入图片描述

    • ③. LocalCache为Guava Cache的核心类,包含一个Segment数组组成。

    1. Segement数组的长度决定了cache的并发数
    2. 每一个Segment使用了单独的锁,其实每个Segment继承了ReentrantLock,对Segment的写操
      作需要先拿到锁
    • ④. 每个Segment由一个table和5个队列组成
    1. ReferenceQueue keyReferenceQueue: 已经被GC,需要内部清理的键引用队列
    2. ReferenceQueue valueReferenceQueue: 已经被GC,需要内部清理的值引用队列
    3. ConcurrentlinkedQueue> recencyQueue : LRU队列,当segment上达到
      临界值发生写操作时该队列会移除数据
    4. Queue> writeQueue:写队列,按照写入时间进行排序的元素队列,写入
      一个元素时会把它加入到队列尾部
    5. Queue> accessQueue:访问队列,按照访问时间进行排序的元素队列,
      访问(包括写入)一个元素时会把它加入到队列尾部
    6. 1个table:AtomicReferenceArray> table:AtomicReferenceArray可以用原子方式更新其元素的对象引用数组
    • ⑤. ReferenceEntry
    1. ReferenceEntry是Guava Cache中对一个键值对节点的抽象,每个ReferenceEntry数组项都是一
      条ReferenceEntry链。并且一个ReferenceEntry包含key、hash、valueReference、next字段
      (单链)
    2. Guava Cache使用ReferenceEntry接口来封装一个键值对,而用ValueReference来封装Value值
  • 相关阅读:
    前端开发学习指南
    箭头函数和普通函数有什么区别
    6_sleuth-zipkin-spring_boot_admin 链路追踪
    SSM在线车队货车管理系统
    在Windows系统上实现电脑IP更改
    CentOS7.9+Kubernetes1.28.3+Docker24.0.6高可用集群二进制部署
    【探索Spring底层】5.Aware 接口及 InitializingBean 接口
    ssm冬奥会志愿者报名系统毕业设计源码241154
    小柏实战学习Liunx(图文教程二十二)
    element 当prop动态时失效问题 添加key值即可
  • 原文地址:https://blog.csdn.net/TZ845195485/article/details/126493436