关于Google的Cache使用

因博主最近需要使用本地缓存保存常用但不重要的数据,查询资料后,Google的Guava很适合,记录一下.

GitHub官网:

https://github.com/google/guava/wiki/CachesExplained

中文资料:

https://ifeve.com/google-guava/

参考资料:

https://www.jianshu.com/p/afe7b2dccee0

https://my.oschina.net/chkui/blog/726442

https://www.bbsmax.com/A/MyJxP0jXJn/

1Cache的概述

Guava 是由Google开发的基于Java的开源库,包含许多Google核心库,它有助于最佳编码实践,并有助于减少编码错误。它为集合 collections 、缓存 caching、原生类型支持 primitives support]、并发库 concurrency libraries 、通用注解 common annotations、字符串处理 string processing、I/O 等等提供实用程序方法.

Cache是Google的Guava当中的缓存部分功能.

其具备了很多优秀的特点:

  • 数据写入缓存时是原子操作.
  • 缓存的数据达到最大规模时,会使用“最近最少使用(LRU)”算法来清除缓存数据.
  • 每一条数据还可以基于时间回收,未使用时间超过一定时间后,数据会被回收.
  • 缓存被清除时,可发送通知告知
  • 提供访问统计功能

2 官方文档

官方范例:

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumSize(1000)
       .expireAfterWrite(10, TimeUnit.MINUTES)
       .removalListener(MY_LISTENER)
       .build(
           new CacheLoader<Key, Graph>() {
    
    
             @Override
             public Graph load(Key key) throws AnyException {
    
    
               return createExpensiveGraph(key);
             }
           });

Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。

使用范围:

  • 消耗内存提升访问速度
  • 访问的数据会被多次使用
  • 缓存的数据量不大,因为是基于内存保存数据,且仅仅支持单体应用.

1 加载

CacheLoader

LoadingCache是附带CacheLoader构建而成的缓存实现。创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法.

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumSize(1000)
       .build(
           new CacheLoader<Key, Graph>() {
    
    
             public Graph load(Key key) throws AnyException {
    
    
               return createExpensiveGraph(key);
             }
           });

...
try {
    
    
  return graphs.get(key);
} catch (ExecutionException e) {
    
    
  throw new OtherException(e.getCause());
}

从LoadingCache查询的正规方式是使用get(K)方法。这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值

Callable

所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable)方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式"如果有缓存则返回;否则运算、缓存、然后返回.

Cache<Key, Value> 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<Value>() {
    
    
    @Override
    public Value call() throws AnyException {
    
    
      return doThingsTheHardWay(key);
    }
  });
} catch (ExecutionException e) {
    
    
  throw new OtherException(e.getCause());
}

显式插入

使用cache.put(key, value)方法可以直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值.

2 缓存回收

Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收.

1 基于容量的回收(size-based eviction)

如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long),缓存将尝试回收最近没有使用或总体上很少使用的缓存项。

在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时.

缓存的权重:

不同的缓存项有不同的“权重”(weights),如果你的缓存值,占据完全不同的内存空间,你可以使用``CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)`指定最大总重.

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumWeight(100000)
       .weigher(new Weigher<Key, Graph>() {
    
    
          public int weigh(Key k, Graph g) {
    
    
            return g.vertices().size();
          }
        })
       .build(
           new CacheLoader<Key, Graph>() {
    
    
             public Graph load(Key key) {
    
     // no checked exception
               return createExpensiveGraph(key);
             }
           });

2 定时回收(Timed Eviction)

提供了两种定时回收策略:

  • expireAfterAccess(long, TimeUnit) 缓存项在给定时间内没有被读/写访问,则回收.
  • expireAfterWrite(long, TimeUnit) 缓存项在给定时间内没有被写访问(创建或覆盖),则回收.

3 基于引用的回收(Reference-based Eviction)

通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:

  • CacheBuilder.weakKeys() 使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收.(因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键)
  • CacheBuilder.weakValues() 使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收.(因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键)
  • CacheBuilder.softValues() 使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收.(使用软引用值的缓存同样用==而不是equals比较值)

4 显式回收

可以手动显式清楚缓存项,即通过代码完成清除功能.

清除方式:

  • 单个清除 Cache.invalidate(key)
  • 批量清除 Cache.invalidateAll(keys)
  • 清除所有缓存 Cache.invalidateAll()

5 监听器

通过CacheBuilder.removalListener(RemovalListener),可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知RemovalNotification,其中包含移除原因RemovalCause、键和值.

CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
    
    
  public DatabaseConnection load(Key key) throws Exception {
    
    
    return openConnection(key);
  }
};
RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
    
    
  public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
    
    
    DatabaseConnection conn = removal.getValue();
    conn.close(); // tear down properly
  }
};

return CacheBuilder.newBuilder()
  .expireAfterWrite(2, TimeUnit.MINUTES)
  .removalListener(removalListener)
  .build(loader);

默认情况下,监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操作.

6 什么时候清理?

使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话.

如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了.

如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()ScheduledExecutorService可以帮助你很好地实现这样的定时调度

7 刷新

刷新和回收不太一样。正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成.

// Some keys don't need refreshing, and we want refreshes to be done asynchronously.
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumSize(1000)
       .refreshAfterWrite(1, TimeUnit.MINUTES)
       .build(
           new CacheLoader<Key, Graph>() {
    
    
             public Graph load(Key key) {
    
     // no checked exception
               return getGraphFromDatabase(key);
             }

             public ListenableFuture<Graph> reload(final Key key, Graph prevGraph) {
    
    
               if (neverNeedsRefresh(key)) {
    
    
                 return Futures.immediateFuture(prevGraph);
               } else {
    
    
                 // asynchronous!
                 ListenableFutureTask<Graph> task = ListenableFutureTask.create(new Callable<Graph>() {
    
    
                   public Graph call() {
    
    
                     return getGraphFromDatabase(key);
                   }
                 });
                 executor.execute(task);
                 return task;
               }
             }
           });

CacheBuilder.refreshAfterWrite(long, TimeUnit)可以为缓存增加自动定时刷新功能。和expireAfterWrite相反,refreshAfterWrite通过定时刷新可以让缓存项保持可用.

8 其他特性

1 统计

CacheBuilder.recordStats()用来开启Guava Cache的统计功能.

Cache.stats()方法会返回CacheStats对象以提供如下统计信息:

  • hitRate() 缓存命中率
  • averageLoadPenalty() 加载新值的平均时间,单位为纳秒
  • evictionCount() 缓存项被回收的总数,不包括显式清除

2 asMap视图

asMap视图提供了缓存的ConcurrentMap形式.

  • cache.asMap()包含当前所有加载到缓存的项.
  • asMap().get(key)实质上等同于cache.getIfPresent(key),而且不会引起缓存项的加载.
  • 所有读写操作都会重置相关缓存项的访问时间,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合视图上的操作.

3 中断

缓存加载方法(如Cache.get)不会抛出InterruptedException.

Cache.get请求到未缓存的值时会遇到两种情况:当前线程加载值;或等待另一个正在加载值的线程。这两种情况下的中断是不一样的。等待另一个正在加载值的线程属于较简单的情况:使用可中断的等待就实现了中断支持;但当前线程加载值的情况就比较复杂了:因为加载值的CacheLoader是由用户提供的,如果它是可中断的,那我们也可以实现支持中断,否则我们也无能为力.

建议AsyncLoadingCache,这个实现会返回一个有正确中断行为的Future对象.

3 使用方法

使用是只需要导入maven依赖即可

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>

范例1:

使用Guava构建本地缓存,在拦截器中,保存接口的调用信息,做接口一定时间内重复调用的校验.

@Component
@Slf4j
public class MyInterceptor implements HandlerInterceptor {
    
    

    // 缓存方案
    // 方案1 使用redis保存该ip在一定时间内调用接口的次数 (适用集群)
//    @Autowired
//    private RedisTemplate redisTemplate;

    public static final int IntervalTime = 8;

    // 方案2 谷歌的guava , 使用本地缓存,设置有效期8秒 (适用单体)
    private final Cache<String, Integer> cache = CacheBuilder.newBuilder()
            .expireAfterAccess(IntervalTime, TimeUnit.SECONDS).build();


    /**
     * 请求处理前调用
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        log.info("========================进入拦截器1=================================");

        HandlerMethod handlerMethod = (HandlerMethod) handler;

        // 查看该方法上是否有需要拦截的注释
        MyAnnotation myAnnotation = handlerMethod.getMethodAnnotation(MyAnnotation.class);
        // 注解存在
        if (myAnnotation != null) {
    
    
            int maxCount = myAnnotation.maxCount();
            int seconds = myAnnotation.seconds();

            // 1 获取ip  ip可能代理
            // 代理服务器在请求转发时添加上去的
            String ip = request.getHeader("x-forwarded-for");
            log.info("x-forwarded-for = {} ", ip);

            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    
    
                ip = request.getHeader("Proxy-Client-IP");
                log.info("Proxy-Client-IP = {} ", ip);
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    
    
                ip = request.getHeader("WL-Proxy-Client-IP");
                log.info("WL-Proxy-Client-IP = {} ", ip);
            }
            // remote_addr http协议传输的时候自动添加,不受请求头header的控制
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    
    
                ip = request.getRemoteAddr();
                log.info("remote_addr = {} ", ip);
            }
            // 缓存方案1 redis
//            if (redisCache(response, maxCount, seconds, ip)) {return false;}

            // 缓存方案2 Google的guava
            if (googleCache(response, maxCount, seconds, ip)) {
    
    return false;}
        }

        // 不拦截,进入下一个拦截器
        return true;
    }

    /**
     * 使用Google的guava
     * @param response
     * @param maxCount
     * @param seconds
     * @param ip
     * @return
     * @throws IOException
     */
    private boolean googleCache(HttpServletResponse response, int maxCount, int seconds, String ip) throws IOException, ExecutionException {
    
    
        // 2 从缓存中获取该ip的访问次数
        Integer count = cache.getIfPresent(ip);

        // 3 判断是否满足注解设置要求
        if (count == null) {
    
    
            cache.put(ip,1);
        } else if (count < maxCount) {
    
    
            // 3.2 在有效期内,访问次数满足要求
            cache.put(ip,++count);
        } else {
    
    
            // 3.3 超过最大访问次数,拒绝该请求
            log.info("访问次数超过要求,请稍后访问系统 = {} ", ip);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            ResultResponse resultResponse = new ResultResponse();
            resultResponse.setResult("操作太快,请稍后访问系统");
            Object obj = JSONObject.toJSON(resultResponse);
            response.getWriter().write(JSONObject.toJSONString(obj));
            return true;
        }
        return false;
    }


    /**
     *  使用redis作为缓存
     * @param response
     * @param maxCount
     * @param seconds
     * @param ip
     * @return
     * @throws IOException
     */
/*    private boolean redisCache(HttpServletResponse response, int maxCount, int seconds, String ip) throws IOException {
        // 2 从redis中获取该ip的访问次数
        Integer count = (Integer) redisTemplate.opsForValue().get(ip);

        // 3 判断是否满足注解设置要求
        if (count == null) {
            // 3.1 第一次访问,设置次数,有效期
            redisTemplate.opsForValue().set(ip, 1);
            redisTemplate.expire(ip, seconds, TimeUnit.SECONDS);
        } else if (count < maxCount) {
            // 3.2 在有效期内,访问次数满足要求
            redisTemplate.opsForValue().set(ip, ++count);
        } else {
            // 3.3 超过最大访问次数,拒绝该请求

            log.info("访问次数超过要求,请稍后访问系统 = {} ", ip);

            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");

            ResultResponse resultResponse = new ResultResponse();
            resultResponse.setResult("操作太快,请稍后访问系统");
            Object obj = JSONObject.toJSON(resultResponse);
            response.getWriter().write(JSONObject.toJSONString(obj));
            return true;
        }
        return false;
    }*/
}

范例2

设置一个有效期5秒的缓存cache,当访问不存在的key,则查询数据库,缓存数据.当key存在,直接返回.在有效期过后,会自动删除key.

public class GoogleGuavaCacheTest {
    
    

    private final LoadingCache<String, String> cache;

    public GoogleGuavaCacheTest() {
    
    
        /**
         *  设置5秒自动过期
         */
        cache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {
    
    
            public String load(String id) throws Exception {
    
    
                System.out.println("method inovke");
                //这里执行查询数据库,等其他复杂的逻辑
                return "User:" + id;
            }
        });
    }

    public String getAndyName(String id) throws Exception {
    
    
        return cache.get(id);
    }
}

// 测试
class GuavaCacheTest {
    
    
    public static void main(String[] args) throws Exception {
    
    
        GoogleGuavaCacheTest us = new GoogleGuavaCacheTest();
        for (int i = 0; i < 20; i++) {
    
    
            System.out.println(us.getAndyName("6666"));
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

Guess you like

Origin blog.csdn.net/ABestRookie/article/details/119901114