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){ ... }
@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