Java基于LoadingCache实现本地缓存的示例代码

目录
  • 一、 添加 maven 依赖
  • 二、CacheBuilder 方法说明
  • 三、创建 CacheLoader
  • 四、工具类
  • 五、guava Cache数据移除

一、 添加 maven 依赖

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

二、CacheBuilder 方法说明

1️⃣LoadingCache build(CacheLoader loader)

2️⃣CacheBuilder.maximumSize(long size)配置缓存数量上限,快达到上限或达到上限,处理了时间最长没被访问过的对象或者根据配置的被释放的对象

3️⃣expireAfterAccess(long, TimeUnit)缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样

4️⃣expireAfterWrite(long, TimeUnit)缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。

5️⃣refreshAfterWrite(long duration, TimeUnit unit)定时刷新,可以为缓存增加自动定时刷新功能。和expireAfterWrite相反,refreshAfterWrite通过定时刷新可以让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新,即只有刷新间隔时间到了再去get(key)才会重新去执行Loading,否则就算刷新间隔时间到了也不会执行loading操作。因此,如果在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。还有一点比较重要的是refreshAfterWrite和expireAfterWrite两个方法设置以后,重新get会引起loading操作都是同步串行的。这其实可能会有一个隐患,当某一个时间点刚好有大量检索过来而且都有刷新或者回收的话,是会产生大量的请求同步调用loading方法,这些请求占用线程资源的时间明显变长。如正常请求也就20ms,当刷新以后加上同步请求loading这个功能接口可能响应时间远远大于20ms。为了预防这种井喷现象,可以不设refreshAfterWrite方法,改用LoadingCache.refresh(K)因为它是异步执行的,不会影响正在读的请求,同时使用ScheduledExecutorService可以很好地实现这样的定时调度,配上cache.asMap().keySet()返回当前所有已加载键,这样所有的key定时刷新就有了。如果访问量没有这么大则直接用CacheBuilder.refreshAfterWrite(long, TimeUnit)也可以。

三、创建 CacheLoader

LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
        //缓存池大小,在缓存项接近该大小时, Guava开始回收旧的缓存项
        .maximumSize(10000)
        //设置时间对象没有被读/写访问则对象从内存中删除(在另外的线程里面不定期维护)
        .expireAfterAccess(10, TimeUnit.MINUTES)
        //移除监听器,缓存项被移除时会触发
        .removalListener(new RemovalListener <Long, String>() {
          @Override
          public void onRemoval(RemovalNotification<Long, String> rn) {
            //执行逻辑操作
          }
        })
        .recordStats()//开启Guava Cache的统计功能
        .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) {
                       //从 SQL或者NoSql 获取对象
                    }
                });//CacheLoader类 实现自动加载

四、工具类

import com.google.common.cache.*;
import org.slf4j.Logger;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

public class CacheManager {

  private static Logger log = Log.get();
  /** 缓存项最大数量 */
  private static final long GUAVA_CACHE_SIZE = 100000;
  /** 缓存时间:天 */
  private static final long GUAVA_CACHE_DAY = 10;
  /** 缓存操作对象 */
  private static LoadingCache<Long, String> GLOBAL_CACHE = null;

  static {
    try {
      GLOBAL_CACHE = loadCache(new CacheLoader <Long, String>() {
        @Override
        public String load(Long key) throws Exception {
          // 处理缓存键不存在缓存值时的处理逻辑
          return "";
        }
      });
    } catch (Exception e) {
      log.error("初始化Guava Cache出错", e);
    }
  }

  /**
   * 全局缓存设置
   * 缓存项最大数量:100000
   * 缓存有效时间(天):10
   * @param cacheLoader
   * @return
   * @throws Exception
   */
  private static LoadingCache<Long, String> loadCache(CacheLoader<Long, String> cacheLoader) 
throws Exception {
    LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
        //缓存池大小,在缓存项接近该大小时, Guava开始回收旧的缓存项
        .maximumSize(GUAVA_CACHE_SIZE)
        //设置时间对象没有被读/写访问则对象从内存中删除(在另外的线程里面不定期维护)
        .expireAfterAccess(GUAVA_CACHE_DAY, TimeUnit.DAYS)
        // 设置缓存在写入之后 设定时间 后失效
        .expireAfterWrite(GUAVA_CACHE_DAY, TimeUnit.DAYS)
        //移除监听器,缓存项被移除时会触发
        .removalListener(new RemovalListener <Long, String>() {
          @Override
          public void onRemoval(RemovalNotification<Long, String> rn) {
            //逻辑操作
          }
        })
        //开启Guava Cache的统计功能
        .recordStats()
        .build(cacheLoader);
    return cache;
  }

  /**
   * 设置缓存值
   * 注: 若已有该key值,则会先移除(会触发removalListener移除监听器),再添加
   *
   * @param key
   * @param value
   */
  public static void put(Long key, String value) {
    try {
      GLOBAL_CACHE.put(key, value);
    } catch (Exception e) {
      log.error("设置缓存值出错", e);
    }
  }

  /**
   * 批量设置缓存值
   *
   * @param map
   */
  public static void putAll(Map<? extends Long, ? extends String> map) {
    try {
      GLOBAL_CACHE.putAll(map);
    } catch (Exception e) {
      log.error("批量设置缓存值出错", e);
    }
  }

  /**
   * 获取缓存值
   * 注:如果键不存在值,将调用CacheLoader的load方法加载新值到该键中
   *
   * @param key
   * @return
   */
  public static String get(Long key) {
    String token = "";
    try {
      token = GLOBAL_CACHE.get(key);
    } catch (Exception e) {
      log.error("获取缓存值出错", e);
    }
    return token;
  }

 /**
   * 移除缓存
   * @param key
   */
  public static void remove(Long key) {
    try {
      GLOBAL_CACHE.invalidate(key);
    } catch (Exception e) {
      log.error("移除缓存出错", e);
    }
  }

  /**
   * 批量移除缓存
   * @param keys
   */
  public static void removeAll(Iterable<Long> keys) {
    try {
      GLOBAL_CACHE.invalidateAll(keys);
    } catch (Exception e) {
      log.error("批量移除缓存出错", e);
    }
  }

  /**
   * 清空所有缓存
   */
  public static void removeAll() {
    try {
      GLOBAL_CACHE.invalidateAll();
    } catch (Exception e) {
      log.error("清空所有缓存出错", e);
    }
  }

  /**
   * 获取缓存项数量
   * @return
   */
  public static long size() {
    long size = 0;
    try {
      size = GLOBAL_CACHE.size();
    } catch (Exception e) {
      log.error("获取缓存项数量出错", e);
    }
    return size;
  }
}

五、guava Cache数据移除

1️⃣移除机制

guava做cache的时候,数据的移除分为被动移除和主动移除两种。

【被动移除分为三种】
1)基于大小的移除:
按照缓存的大小来移除,如果即将到达指定的大小,那就会把不常用的键值对从cache中移除。定义的方式一般为 CacheBuilder.maximumSize(long),还有一种可以算权重的方法,个人认为实际使用中不太用到。就这个常用有一下注意点:

a. 这个size指的是cache中的条目数,不是内存大小或是其他;
b. 并不是完全到了指定的size系统才开始移除不常用的数据的,而是接近这个size的时候系统就会开始做移除的动作;
c. 如果一个键值对已经从缓存中被移除了,再次请求访问的时候,如果cachebuild是使用cacheloader方式的,那依然还是会从cacheloader中再取一次值,如果这样还没有,就会抛出异常。

2)基于时间的移除:
expireAfterAccess(long, TimeUnit) 根据某个键值对最后一次访问之后多少时间后移除;
expireAfterWrite(long, TimeUnit) 根据某个键值对被创建或值被替换后多少时间移除
3)基于引用的移除:主要是基于Java的垃圾回收机制,根据键或者值的引用关系决定移除

【主动移除分为三种】
1)单独移除:Cache.invalidate(key)
2)批量移除:Cache.invalidateAll(keys)
3)移除所有:Cache.invalidateAll()

如果需要在移除数据的时候有所动作还可以定义Removal Listener,但是有点需要注意的是默认Removal Listener中的行为是和移除动作同步执行的,如果需要改成异步形式,可以考虑使用RemovalListeners.asynchronous(RemovalListener, Executor)

2️⃣遇到的问题

1)在put操作之前,如果已经有该键值,会先触发removalListener移除监听器,再添加
2)配置了expireAfterAccess和expireAfterWrite,但在指定时间后没有被移除。

解决方案:CacheBuilder在文档上有说明:

If expireAfterWrite or expireAfterAccess is requested entries may be evicted on each cache modification, on occasional cache accesses, or on calls to Cache.cleanUp(). Expired entries may be counted in Cache.size(), but will never be visible to read or write operations.

翻译过来大概的意思是:CacheBuilder构建的缓存不会在特定时间自动执行清理和回收工作,也不会在某个缓存项过期后马上清理,它不会启动一个线程来进行缓存维护,因为:

a)线程相对较重
b)某些环境限制线程的创建。它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做

当然,也可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。

到此这篇关于Java基于LoadingCache实现本地缓存的示例代码的文章就介绍到这了,更多相关Java LoadingCache本地缓存内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java本地缓存工具之LoadingCache的使用详解

    目录 前言 环境依赖 代码 演示一下 总结 前言 在工作总常常需要用到缓存,而redis往往是首选,但是短期的数据缓存一般我们还是会用到本地缓存.本文提供一个我在工作中用到的缓存工具,该工具代码为了演示做了一些调整.如果拿去使用的话,可以考虑做成注入Bean对象,看具体需求了. 环境依赖 先添加maven依赖 <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</arti

  • 如何基于LoadingCache实现Java本地缓存

    这篇文章主要介绍了如何基于LoadingCache实现Java本地缓存,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 前言 Guava是Google开源出来的一套工具库.其中提供的cache模块非常方便,是一种与ConcurrentMap相似的缓存Map. 官方地址:https://github.com/google/guava/wiki/CachesExplained 开始构建 一. 添加依赖 <dependency> <groupI

  • Java基于LoadingCache实现本地缓存的示例代码

    目录 一. 添加 maven 依赖 二.CacheBuilder 方法说明 三.创建 CacheLoader 四.工具类 五.guava Cache数据移除 一. 添加 maven 依赖 <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.1-jre</version> </depend

  • Java基于JNDI 实现读写分离的示例代码

    目录 一.JNDI数据源配置 二.JNDI数据源使用 三.web.xml配置 四.spring-servlet.xml配置 五.spring-db.xml配置 六.log4j.properties配置 七.相关路由数据源切换逻辑代码 八.搭建过程中遇到的问题和解决方案 一.JNDI数据源配置 在Tomcat的conf目录下,context.xml在其中标签中添加如下JNDI配置: <Resource name="dataSourceMaster" factory="or

  • Java基于IDEA实现http编程的示例代码

    http开发前言之为什么要有应用层 我们已经学过TCP/IP , 已经知道目前数据能从客户端进程经过路径选择跨网络传送到服务器端进程 [ IP+Port ],可是,仅仅把数据从A点传送到B点就完了吗?这就好比,在淘宝上买了一部手机,卖家[ 客户端 ]把手机通过顺丰[ 传送+路径选择 ] 送到买家 [ 服务器 ] 手里就完了吗?当然不是,买家还要使用这款产品,还要在使用之后,给卖家打分评论.所以,我们把数据从A端传送到B端, TCP/IP 解决的是顺丰的功能,而两端还要对数据进行加工处理或者使用,

  • java基于mongodb实现分布式锁的示例代码

    目录 原理 实现 使用 原理 通过线程安全findAndModify 实现锁 实现 定义锁存储对象: /** * mongodb 分布式锁 */ @Data @NoArgsConstructor @AllArgsConstructor @Document(collection = "distributed-lock-doc") public class LockDocument { @Id private String id; private long expireAt; privat

  • java基于jedisLock—redis分布式锁实现示例代码

    分布式锁是啥? 单机锁的概念:我们正常跑的单机项目(也就是在tomcat下跑一个项目不配置集群)想要在高并发的时候加锁很容易就可以搞定,java提供了很多的机制例如:synchronized.volatile.ReentrantLock等锁的机制. 为啥需要分布式锁:当我们的项目比较庞大的时候,单机版的项目已经不能满足吞吐量的需求了,需要对项目做负载均衡,有可能还需要对项目进行解耦拆分成不同的服务,那么肯定是做成分布式的项目,分布式的项目因为是不同的程序控制,所以使用java提供的锁并不能完全保

  • java基于spring boot本地上传图片示例解析

    前几天项目中刚好需要上传图片的需求,当时想的是用七牛云,因为我用七牛云也用了好几次,就是把图片上传到七牛云空间里面,数据库里面保存的是这张上传图片的url地址 那么页面访问也就很方便,考虑到项目部署的环境我就用了本地上传,不牵涉数据库的操作.我就花了半个小时写了个本地上传图片的小demo.非常的简单. 下面是需要的依赖 pom.xml文件: <?xml version="1.0" encoding="UTF-8"?> <project xmlns=

  • Java 基于AQS实现自定义同步器的示例

    一.AQS-条件变量的支持 在如下代码中,当另外一个线程调用条件变量的signal方法的时候(必须先调用锁的lock方法获取锁),在内部会把条件队列里面队头的一个线程节点从条件队列里面移除并且放入AQS的阻塞队列里面,然后激活这个线程. public final void signal() {  if(!isHeldExclusively()) {   throw IllegalMonitorException();  }  Node first = firstWaiter;  if(first

  • 基于Java的MathML转图片的方法(示例代码)

    Maven依赖: <dependency> <groupId>de.rototor.jeuclid</groupId> <artifactId>jeuclid-core</artifactId> <version>3.1.14</version> </dependency> 示例: @Test public void testMathMlToImg() throws IOException { //MathML

  • 基于Java实现QQ登录注册功能的示例代码

    目录 前言 实现代码 登录页面 注册页面 效果展示 前言 本文主要应用的技术有:GUI.JDBC.多线程 实现的功能具体如下: 1.登录功能 2.注册功能 3.是否隐藏密码的选择以及实现功能 4.选择性别功能 5.密码与确认密码功能 6.登录页面实时展示当前的时间 7.当登录时用户名与密码在数据库中没有相匹配的数据,则会跳转到注册页面上去. 8.同样,注册完毕后,数据会运用JDBC将数据写入数据库中,然后跳转回登录页面. 实现代码 登录页面 import javax.swing.*; impor

  • java使用软引用实现缓存机制示例

    目录 正文 软引用和强引用 项目 使用idea创建一个maven项目 首先对Good实体类进行编写. 然后我们在goodbase里面编写代码,模拟一个数据库 然后书写goodscache缓存类 goodsservice模拟数据库增删改查 最后我们书写test文件 运行结果 正文 “读多写少”是大部分项目的一个特点.例如“购物”,总是看的人多(读).买的人少(写).因此,如果能减少“读”请求的次数,就能减少服务端的压力.最直接的减少“读”请求次数的方法就是使用缓存. 软引用和强引用 对于同一个读请

随机推荐