1. Guava Cache

Guava Cache是一個全内存的本地缓存实现,提供了线程安全实现机制

1.1 GuavaCache数据结构

底层类似ConcurrentlHashMap,所以是线程安全的(分段锁)

 

 1.2 Guava Cache优势

设置过期时间,并提供数据过多时淘汰机制

线程安全,支持数据并发读写

缓存中没有数据(超过过期时间等原因)会从数据源中获取并加入到缓存。Guava Cache可以使用CacheLoader 的load方法控制,对同一个key只允许一个请求去读源并回填缓存,其他请求阻塞等待

 

1.3 Guava Cache参数

initialCapacity缓存初始数据容量大小

maximumSize:缓存包含最大entry数量,超过数量后淘汰entry,接近最大值时淘汰不常用数据

过期时间:

  expireAfterAccess:超过指定时间没有读/写,缓存就会被回收;

  expireAfterWrite:超过指定时间没有写,缓存就被回收;

  refreshAfterWrite:超过指定时间,缓存会调用reload方法(如果没有reload则调用load方法)刷新;在refresh过程中,如果这是有查询过来,查询会返回旧值;而expireAfterAccess和expireAfterWriter超过超时时间后查询会去数据源查询数据,对比发现refreshAfterWriter比前两个性能好一些,因为不需要去数据源load数据,但不能严格保证查询数据都是新值

删除监听器:

  removalListener:用于监控缓存中数据,当数据被删除时触发

  

 1.4 Guava Cache vs ConcurrentHashMap vs Redis

1.4.1 Guava Cache vs ConcurrentHashMap

concurrentHashmap数据不会失效,直到显式移除; Guava Cache设置过期时间,超过时间再获取数据时会调用load()去数据源拿数据

1.4.2 Guava Cache vs Redis

Guava Cache是单个应用运行时的本地缓存,单机版的缓存,数据没有持久化到服务器,应用重启数据会丢失。另外分布式系统无法保证缓存数据一致性问题

Redis在分布式环境下保证数据一致性方式:

Redis作为第三方缓存,有自己的server,而localcache是直接使用计算机/服务器内存 ,但Redis也是集群模式,通过以下方式实现一致性:

a)集群以及主从复制,Redis支持主从复制,当主节点有数据写入时,会自动同步到从节点,从节点与主节点数据保持一致

b)Sentinel哨兵,哨兵监控主节点状态,如果主节点发生故障,哨兵自动将从节点提升为主节点

c)Redis事务:Redis 支持事务操作,将一组操作当做一个原子操作执行,要么全部执行成功,要么全部执行失败,可以通过事务来保证数据的一致性。

 

1.5 Guava Cache创建方式

Guava Cache可以通过builder方式生成两种缓存对象:LoadingCache(同步填充)和Cache(手动填充);LoadingCache继承cache,相比于Cache,LoadingCache提供了get获取值时如果之不存在自动通过CacheLoad的load方法去数据源加载数据

1.5.1 Cache

//构建缓存构建器 
Cache cache = CacheBuilder.newBuilder()
        .maximumSize(maximumSize).
                expireAfterWrite(expireAfterWriteDuration, timeUnit)
        .recordStats().build();
/缓存数据 
cache.put("cache","cache-value");
 //获取缓存数据 
 String value=cache.getIfPresent("cache"); 
 //删除缓存 
 cache.invalidate("cache5");

 

1.5.2 LoadingCache

public abstract class CacheService<T> {
  final GpuDbService gpuDbService;
  final ObjectMapper objectMapper;
  public final LoadingCache<String, T> cacheLoad =
      CacheBuilder.newBuilder()
          .maximumSize(1000)
          .expireAfterAccess(1, HOURS)
      .refreshAfterWrite(30, MINUTES)  .removalListener( n
-> { log.debug( "Removed [key {}: value {}] from cache. (wasEvicted={}, cause={})", n.getKey(), n.getValue(), n.wasEvicted(), n.getCause()); }) .build(createCache()); public CacheLoader<String, T> createCache() { return new CacheLoader<>() { @Override public T load(String key) { return loadData(key); } @Override public Map<String, T> loadAll(Iterable<? extends String> keys) { return loadAllData(); } }; } public abstract T loadData(String key); //具体实现类加载不同数据 public abstract Map<String, T> loadAllData(); //具体实现类加载不同数据 }

 

1.5.3 GuavaCache Manager

CacheManager可以管理一堆Cache;CacheManager约定下边所有Cache过期时间、刷新时间、初始化大小等

使用Cache Manager其实是结合SpringCache来实现的。Spring Cache是一个框架,基于注解方式,业务代码不用关心底层使用的那种缓存(guava、caffeine等),只需要加一个注解就能实现缓存功能

Guava Cache结合SpringCache注解:

@EnableCaching:开启缓存

@Caching让我们在一个方法或者类上同时指定多个spring cache相关注解,其拥有三个属性:cacheable put evict分别指定@Cacheable @CachePut @CacheEvict

 @Caching(
            cacheable = @Cacheable(value="report",key="#reportExecutionId",cacheManager = "reportManager"),
            evict = {@CacheEvict(value=CacheName,cacheManager = CacheManagerName),@CacheEvict(value = CacheName)},
            put = {@CachePut(value=CacheName,key = "#reportExecutionId",cacheManager = CacheManagerName)}
    )
    public ReportExecution getReportExecution(String reportExecutionId){
...
}
View Code

 

@Cacheable:在要缓存的方法上添加@Cacheable注解表示这个方法有缓存功能,方法返回值会被缓存下来,下次调用该方法前会检查缓存中是否有值,有过有直接返回,如果没有就调用方法,然后把结果缓存起来

  @Cacheable参数:key、value、condition、keyGenerator、cacheManger

  value: cacheNames

  cacheManager:指定用哪个CacheManger(多个cacheManger情况下)

  condition:制定法发生条件,为空表示缓存所有调用情形,其值通过SpringEL表达式来指定,如下示例,只有当role不为空是才会进行缓存

@Cacheable(value = "role",key = "#role.id",condition = "#role.id %2==0")
 public Role selectRole(Integer roleId){
       Role role = new Role();
       role.setId(roleId);
       return roleMapper.selectOne(role);

}

 

  key:用于指定Spring缓存方法结果对应key,该属性支持EL表达式。如果有方法没有参数则使用0作为key;如果有一个参数则使用该参数作为key,如果参数多于一个(比如是entity)则用所有参数hashcode作为key.如果没有输入key则会用keygenerator生成 key

@CacheEvict:使用CacheEvict会清空指定缓存,一般用在更新或者删除方法上

@CacheEvict(value = "role",key = "#roleId",allEntries = true,beforeInvocation = true)
public Integer deleteRole(Integer roleId){
    return roleMapper.deleteByPrimaryKey(roleId);

 

  allEntries :表示是否清除缓存中所有数据

  beforeInvocation:清楚操作是否在方法成功执行之后触发(默认false:方法执行之后触发缓存数据清除)

@EnableCache vs @CachePut:

@EnableCache执行前会判断缓存中是否有之前执行结果,如果有则直接返回;而@CachePut执行前不会检查缓存中是否有之前执行结果,每次都会执行其注解的方法,并将结果以键值对形式存到缓存

相关代码

依赖:
    implementation 'org.springframework.boot:spring-boot-starter-cache'    
    implementation group: 'com.google.guava', name: 'guava', version: '24.0-jre'
    implementation group: 'org.springframework', name: 'spring-context-support', version: '4.3.18.RELEASE'

CacheConfig:
@Configuration
@EnableCaching
public class CacheManager extends CachingConfigurerSupport {
    public static final String CacheManagerName = "reportManager";
    public static final String CacheName = "report";
    @Bean(name=CacheManagerName)
//    @Primary  //如果设置多个CacheManager就需要在众多实现类中加上@Primary,不然spring会报错能选择的bean太多而不知道用哪一个
    @Override
    public GuavaCacheManager cacheManager() {  //

        GuavaCacheManager cacheManager = new GuavaCacheManager(CacheName); //输入多个CacheName,可定义多个Cache. public GuavaCacheManager(String... cacheNames)
        cacheManager
                .setCacheBuilder(CacheBuilder.newBuilder()
                        // 存活时间(30秒内没有被访问则被移除)
                        .expireAfterAccess(30, TimeUnit.SECONDS)
                        // 存活时间(写入10分钟后会自动移除)
                        .expireAfterWrite(10, TimeUnit.MINUTES)
                        // 最大size
                        .maximumSize(1000)
                        // 最大并发量同时修改
                        .concurrencyLevel(6)
                        // 初始化大小为100个键值对
                        .initialCapacity(100)
                        // 变成软引用模式(在jvm内存不足时会被回收)
                        .softValues());

        return cacheManager;
    }

}

具有缓存功能接口
    @Cacheable(value=CacheName,key="#reportName",cacheManager = CacheManagerName) //value表示下边方法结果缓存到哪个缓存中,可以是一个也可以是多个,多个情形:@Cacheable(value={CacheName1,CacheName2},key="xxx",cacheManager="xxx")
    public Report getReport(String reportName){
        final String expression = and(eq(REPORT_NAME_COLUMN_NAME, reportName));
        Report report= gpuDbService
                .findChosenColumnsByExpression(REPROT_TABLE_NAME, REPORT_COLUMNS, expression, null, 1)
                .stream()
                .findFirst()
                .map(
                        r -> {
                            try {
                                return objectMapper.readValue(r.toString(), Report.class);
                            } catch (JsonProcessingException e) {
                                throw new RuntimeException("Report parse failed for json: " + r, e);
                            }
                        })
                .orElse(null);
        return report;
    }

    @Cacheable(value="report",key="#reportExecutionId",cacheManager = "reportManager")
    public ReportExecution getReportExecution(String reportExecutionId){
        final String expression = and(eq(REPORT_EXECUTION_ID_COLUMN_NAME, reportExecutionId));
        ReportExecution reportExecution= gpuDbService
                .findChosenColumnsByExpression(REPORT_EXECUTION_TABLE_NAME, REPORT_EXECUTION_COLUMNS, expression, null, 1)
                .stream()
                .findFirst()
                .map(
                        r -> {
                            try {
                                return objectMapper.readValue(r.toString(), ReportExecution.class);
                            } catch (JsonProcessingException e) {
                                throw new RuntimeException("Report parse failed for json: " + r, e);
                            }
                        })
                .orElse(null);
        return reportExecution;
    }
}

 

 2 Caffeine

 对于本地缓存,Caffeine较Guava适用性更广

2.1 Caffeine缓存创建方式

手动、同步加载、异步加载

import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

public class CaffenieCacheService {

    /**
     * 1. 手动加载,如果key不存在则调用加载函数生成value
     */
    public Object manulOperator(String key) {
        Cache<String, Object> cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.SECONDS)
                .expireAfterAccess(1, TimeUnit.SECONDS)
                .maximumSize(10)
                .build();
        //如果一个key不存在,那么会进入指定的函数生成value
        Object value = cache.get(key, t -> setValue(key).apply(key));
        cache.put("hello", value);

        //判断是否存在如果不存返回null
        Object ifPresent = cache.getIfPresent(key);
        //移除一个key
        cache.invalidate(key);
        return value;
    }

    public Function<String, Object> setValue(String key) {
        return t -> key + "value";

    }


    /**
     * 2. 同步加载:构造cache时候,build传入一个CacheLoader实现类。实现load方法
     */
    public Object syncOperator(String key) {
        LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .build(k -> setValueBySync(key).apply(key));
        return cache.get(key);
    }

    public Function<String, Object> setValueBySync(String key) {
        return t -> key + "value";

    }


    /**
     * 3. 异步加载:AsyncLoadingCache继承LoadingCache,异步加载使用响应式编程CompletableFuture
     * 如果以如2中同步加载需要提供ClassLoader;如果以3中异步加载需要提供一个AsyncClassLoader并返回一个CompletableFuture
     */
    public Object asyncOperator(String key) {
        AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .buildAsync(k -> setAsyncValue(key).get());

        return cache.get(key);
    }

    public CompletableFuture<Object> setAsyncValue(String key) {
        return CompletableFuture.supplyAsync(() -> {
            return key + "value";
        });

    }
}

 

 

 2.2 CaffeineManager方式(和Guava Manager很类似,区别是是CacheManager创建不同)

 

依赖
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation 'com.github.ben-manes.caffeine:caffeine'

CachingCofnig:
@Configuration
@EnableCaching
public class CachingConfig extends CachingConfigurerSupport {

  public static final String REPORTS_CACHE_NAME = "reports";
  public static final String REPORT_EXECUTIONS_CACHE_NAME = "reportExecutions";
  public static final String REPORTS_CACHE_MANAGER_NAME = "reportsCacheManager";
  public static final String REPORT_EXECUTIONS_CACHE_MANAGER_NAME = "reportExecutionsCacheManager";
  public static final int REPORTS_CACHE_MANAGER_SIZE = 200;
  public static final int REPORT_EXECUTIONS_CACHE_MANAGER_SIZE = 200;

  @Bean(name = REPORTS_CACHE_MANAGER_NAME)
  @Primary
  public CacheManager reportsCacheManager() {
    CaffeineCacheManager cacheManager = new CaffeineCacheManager(REPORTS_CACHE_NAME);
    cacheManager.setCaffeine(
        Caffeine.newBuilder()
            .initialCapacity(REPORTS_CACHE_MANAGER_SIZE)
            .maximumSize(REPORTS_CACHE_MANAGER_SIZE)
            .expireAfterWrite(12, TimeUnit.HOURS)
            .recordStats());
    return cacheManager;
  }

  @Bean(name = REPORT_EXECUTIONS_CACHE_MANAGER_NAME)
  public CacheManager reportExecutionsCacheManager() {
    CaffeineCacheManager cacheManager = new CaffeineCacheManager(REPORT_EXECUTIONS_CACHE_NAME);
    cacheManager.setCaffeine(
        Caffeine.newBuilder()
            .initialCapacity(REPORT_EXECUTIONS_CACHE_MANAGER_SIZE)
            .maximumSize(REPORT_EXECUTIONS_CACHE_MANAGER_SIZE)
            .expireAfterAccess(5, TimeUnit.MINUTES)
            .recordStats());
    return cacheManager;
  }
}

实现缓存的方法:
@Slf4j
@Service
@RequiredArgsConstructor
public class DBCacheService {

  private final GpuDbService gpuDbService;
  private final ObjectMapper objectMapper;
  public static final String REPORT_EXECUTION_TABLE_NAME = "ReportExecution";
  private static final String REPORT_EXECUTION_ID_COLUMN_NAME = "reportExecutionId";
  public static final String COB_CALENDAR_KEY_COLUMN_NAME = "cobCalendarKey";
  private static final List<String> REPORT_EXECUTION_COLUMNS =
      singletonList(REPORT_EXECUTION_ID_COLUMN_NAME);
  public static final String REPROT_TABLE_NAME = "Report";
  private static final String REPORT_NAME_COLUMN_NAME = "reportName";
  private static final List<String> REPORT_COLUMNS =
      Arrays.asList(REPORT_NAME_COLUMN_NAME, "reportId", "reportCriteria");

  @Cacheable(
      value = REPORT_EXECUTIONS_CACHE_NAME,
      key = "#cobCalendarKey",
      cacheManager = REPORT_EXECUTIONS_CACHE_MANAGER_NAME)
  public @Nullable String getReportExecution(String cobCalendarKey) {
    final String expression = and(eq(COB_CALENDAR_KEY_COLUMN_NAME, cobCalendarKey));
    return gpuDbService
        .findChosenColumnsByExpression(
            REPORT_EXECUTION_TABLE_NAME, REPORT_EXECUTION_COLUMNS, expression, null, 1)
        .stream()
        .findFirst()
        .map(r -> r.getString(REPORT_EXECUTION_ID_COLUMN_NAME))
        .orElse(null);
  }

  @Cacheable(value = REPORTS_CACHE_NAME, key = "#reportName")
  public Report getReport(String reportName) {
    final String expression = and(eq(REPORT_NAME_COLUMN_NAME, reportName));
    return gpuDbService
        .findChosenColumnsByExpression(REPROT_TABLE_NAME, REPORT_COLUMNS, expression, null, 1)
        .stream()
        .findFirst()
        .map(
            r -> {
              try {
                return objectMapper.readValue(r.toString(), Report.class);
              } catch (JsonProcessingException e) {
                throw new RuntimeException("Report parse failed for json: " + r, e);
              }
            })
        .orElse(null);
  }
}

调用方式:
@Slf4j
@Service
@RequiredArgsConstructor
public class PVDepsCalcRequestsService {

   private final DBCacheService dbCacheService;

  public void test(){
     final String baseStepReportExecutionId = dbCacheService.getReportExecution(cobCalendarKey);
}

}

 

 

 2.3 GuavaCache vs Caffeine

  GuavaCache Caffeine
剔除算法 LRU(最近最少使用算法) Window FinyLFU
异步优化   更好
     
     

 

 

参考文献

https://blog.csdn.net/weixin_40251199/article/details/88028009

posted on 2023-03-19 12:02  colorfulworld  阅读(2120)  评论(0编辑  收藏  举报