[缓存] 缓存技术初探
1 背景
使用场景:计算或检索一个值的代价很高,并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存。
高并发下,为提高 频繁 查询 大量 可能常用的 数据库数据的 查询效率。
大部分情况下,单机用Google Guava(Cache/LoadCache) / ehcache,分布式用redis和memcache,各有各的好处,现在企业都是应用很多种中间件供后端程序员选择。
2 缓存技术
什么是缓存?
1 - Cache是高速缓冲存储器 一种特殊的存储器子系统,其中复制了频繁使用的数据以利于快速访问
2 - 凡是位于速度相差较大的两种硬件/软件之间的,用于协调两者数据传输速度差异的结构,均可称之为 Cache
3 - 缓存技术设计思想: 典型的空间换时间
2-1 分类
-
操作系统磁盘缓存(加速/减少磁盘机械操作) / 数据库缓存(加速/减少访问文件系统I/O) / 【应用程序缓存】(加快/减少对数据库的查询) / Web服务器缓存(加速/减少应用服务器请求) / 浏览器缓存(加速/减少对网站的访问)
-
分布式缓存 / 本地缓存
-
介质: 基于内存缓存 / 基于磁盘缓存 / 基于中间件[数据库]缓存(Redis/Memcache/...,本质:内存+磁盘) / 基于JVM缓存(本质:基于内存)
2-2 缓存开源组件
OSCache / Java Caching System(JCS) / / JCache / ShiftOne / SwarmCache / TreeCache / JBossCache / WhirlyCache
EHCache
Google Guava(核心类: Cache/LoadingCache;内存/JVM/本地缓存; Spring5之后,官方放弃Guava改用Caffeine)
Caffeine
2-3 缓存的指标
- 命中率
- 最大容量
- 清空策略(过期策略)
先进先出算法(FIFO)
first in first out ,最先进入缓存得数据在缓存空间不够情况下(超出最大元素限制时)会被首先清理出去
最不经常使用算法(LFU)
Less Frequently Used ,一直以来最少被使用的元素会被被清理掉。这就要求缓存的元素有一个hit 属性,在缓存空间不够得情况下,hit 值最小的将会被清出缓存
最近最少使用算法(LRU)
Least Recently Used ,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存
最近最常使用算法(MRU)
这个缓存算法最先移除最近最常使用的条目。一个MRU算法擅长处理一个条目越久,越容易被访问的情况。
自适应缓存替换算法(ARC)
在IBM Almaden研究中心开发,这个缓存算法同时跟踪记录LFU和LRU,以及驱逐缓存条目,来获得可用缓存的最佳使用。
2-4 基于JVM缓存的实现方案
- 方案1: HashMap / CocurrentHashMap
- 方案2: 开源组件(Google Guava: Cache / LoadingCache)
Cache/LoadingCache 均继承自 CocurrentHashMap
2-5 缓存产生的问题
- Q1: 缓存数据与源数据一致性问题(数据同步)
解决方法
1) write back(写回策略): 更新数据源数据时,只更新缓存的数据。当缓存需要被替换(挤出)时,才将缓存中更新的值写回磁盘。
在写回策略中,为了减少写操作,缓存数据单元通常还设有1个脏位(dirty bit),用于标识该块在被载入后,是否发生过更新。
若1个缓存数据单元在被置换回内存之前,从未被写入过,则:可以免去回写操作;
写回的优点是:节省了大量的写操作
2) write through(写通策略): 更新数据源数据时,同时更新缓存的数据。
- Q2: 缓存数据存放时间问题
- Q3: 缓存的多线程并发控制问题
3 基于Google Guava开源组件的JVM缓存实现
需求背景: 一项目中多个接口、频繁地批量查询数据库一类数据————
发布的数据服务信息
,又要求3s内立即做出响应。 (存在高并发问题)
关于 Google Guava 开源缓存组件: google guava中有cache包,此包提供内存缓存功能。内存缓存需要考虑很多问题,包括并发问题,缓存失效机制,内存不够用时缓存释放,缓存的命中率,缓存的移除等等。 当然这些东西guava都考虑到了。
guava中使用缓存需要先声明一个CacheBuilder对象,并设置缓存的相关参数,然后调用其build方法获得一个Cache接口的实例。请看下面的代码和注释,注意在注释中指定了Cache的各个参数。
3-0 Maven依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
3-1 IDataServiceInfoCacheService
package xxx.service;
import com.google.common.cache.LoadingCache;
import com.yyy.DataServiceInfo;
import java.util.Map;
/**
* @date: 2020/11/12 16:58:05
* @description: 缓存数据服务信息
*/
public interface IDataServiceInfoCacheService {
/**
* 从缓存中 获取 数据服务信息
* 若缓存中不存在该信息,将自动从数据库中加载,再返回
* @param serviceId
* @return
* @throws Exception
*/
public DataServiceInfo get(String serviceId) throws Exception;
//public void put(String serviceId, DataServiceInfo dataServiceInfo);
//public void putAll(Map<? extends String, ? extends DataServiceInfo> dataServiceInfoMap);
public long size();
public void remove(String serviceId);
public void removeAll(Iterable<Long> serviceIds);
public void removeAll();
}
3-2 DataServiceCacheServiceImpl
package xxx.service.impl;
import com.xxx..Dept;
import com.xxx.ServerSystem;
import com.xxx.BusinessException;
import com.google.common.cache.*;
import com.xxx.BmsCacheService;
import com.xxx.LoggerUtil;
import com.xxx.Tools;
import com.xxx.DataServiceInfo;
import com.xxx.DataServiceInfoCacheService;
import com.xxx.ServiceInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @date: 2020/11/12 17:28:03
* @description: ...
*/
@Service
public class DataServiceCacheServiceImpl implements IDataServiceInfoCacheService {
private static LoadingCache<String, DataServiceInfo> DATA_SERVICE_INFO_CACHE;
@Autowired
private ServiceInfoMapper serviceInfoMapper;
@Autowired
private BmsCacheService bmsCacheService;
@PostConstruct // 解决 【静态变量】初始化时调用【实例方法】问题
public void init() {
DATA_SERVICE_INFO_CACHE = CacheBuilder
.newBuilder() ////CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
.concurrencyLevel(8) // //8个segment,分段锁8;设置并发级别为8,并发级别是指可以同时写缓存的线程数
.expireAfterWrite(120, TimeUnit.SECONDS)// 设置写缓存后120秒钟过期 (缓存在写缓存后的指定时间内没有被新的值覆盖时,将失效) 【expire是指定时间过后,expire是remove该key,下次访问是发起同步请求以返回获取到新值】
//.expireAfterAccess(120, TimeUnit.SECONDS)// 设置读缓存后120秒钟过期 (缓存在读缓存后的指定时间内没有被读写时,将失效)
.refreshAfterWrite(120, TimeUnit.SECONDS) // 设置写缓存后120秒钟刷新 【refresh是指定时间后,不会remove该key,下次访问会触发刷新,新值没有回来时返回旧值】
//.refreshAfterAccess(120, TimeUnit.SECONDS) // 设置读缓存后120秒钟刷新
//使用弱引用存储键。当没有(强或软)引用到该键时,相应的缓存项将可以被垃圾回收。由于垃圾回收是依赖==进行判断,因此这样会导致整个缓存也会使用==来比较键的相等性,而不是使用equals()
.weakKeys()
//使用弱引用存储缓存值。当没有(强或软)引用到该缓存项时,将可以被垃圾回收。由于垃圾回收是依赖==进行判断,因此这样会导致整个缓存也会使用==来比较缓存值的相等性,而不是使用equals()
.weakValues()
.initialCapacity(1000)// 设置缓存容器的初始容量为1000
.maximumSize(10000)// 设置缓存最大容量为10000,超过10000之后就会按照LRU最近虽少使用算法来移除缓存项
.recordStats()// 设置要统计缓存的命中率
.removalListener(getRemovalListener())// 设置缓存的移除通知(移除时的触发操作)
.build(getCacheLoader());// build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
}
@Transactional(readOnly = true)
protected Map<String, Object> getSourceSystemInfo(String serviceId) throws Exception {
Map<String, Object> data = new HashMap<String, Object>();
Map<String, Object> provides = serviceInfoMapper.getServiceProvideSystems(serviceId);
if (provides != null) {
if (provides.get("provideSystemId") != null) {
String provideSystemIds = provides.get("provideSystemId").toString();
String[] provideIds = provideSystemIds.split(",");
StringBuilder systemNames = new StringBuilder();
for (String id : provideIds) {
ServerSystem sys = bmsCacheService.getSysById(id);
if (sys != null) {
systemNames.append(sys.getSystemName()).append(",");
}
}
data.put("provideSystemIds", provideSystemIds);
if (provideIds.length > 0) {
data.put("provideSystemNames", systemNames.deleteCharAt(systemNames.length() - 1));
}
}
if (provides.get("deptId") != null) {
String deptIds = provides.get("deptId").toString();
String[] provideIds = deptIds.split(",");
StringBuilder departNames = new StringBuilder();
for (String id : provideIds) {
Dept dept = bmsCacheService.getDeptById(id);
if (dept != null) {
departNames.append(dept.getDeptName()).append(",");
}
}
data.put("provideDepartIds", deptIds);
if (provideIds.length > 0) {
data.put("provideDepartNames", departNames.deleteCharAt(departNames.length() - 1));
}
}
}
return data;
}
@Transactional(readOnly = true)
protected DataServiceInfo loadDataServiceInfo(String serviceId) throws Exception {
DataServiceInfo dataServiceInfo = new DataServiceInfo();
Map<String, Object> sourceSystemInfo = null;
sourceSystemInfo = this.getSourceSystemInfo(serviceId);
Map<String, String> serviceAndCatalogInfoMap = null;
serviceAndCatalogInfoMap = serviceInfoMapper.getServiceInfoAndCatalogInfoById(serviceId);
if (Tools.isNull(sourceSystemInfo) && Tools.isNull(serviceAndCatalogInfoMap)) {//通过 serviceId,均未查找到 数据服务信息
String errorMsg = "根据所提供的数据服务编号,未能查找到数据服务信息!";
LoggerUtil.error(LoggerUtil.DATASERVICE_MNG_CORE_LOGGER_INSTANCE, String.format(errorMsg + " [serviceId: %s]", serviceId));
throw new BusinessException(errorMsg);
}
dataServiceInfo.setServiceId(serviceId);
if (Tools.isNotNull(sourceSystemInfo)) {
dataServiceInfo.setProvideDepartIds(Tools.isNotNull(sourceSystemInfo.get("provideDepartIds")) ? sourceSystemInfo.get("provideDepartIds").toString() : "");
dataServiceInfo.setProvideDepartNames(Tools.isNotNull(sourceSystemInfo.get("provideDepartNames")) ? sourceSystemInfo.get("provideDepartNames").toString() : "");
dataServiceInfo.setProvideSystemIds(Tools.isNotNull(sourceSystemInfo.get("provideSystemIds")) ? sourceSystemInfo.get("provideSystemIds").toString() : "");
dataServiceInfo.setProvideSystemNames(Tools.isNotNull(sourceSystemInfo.get("provideSystemNames")) ? sourceSystemInfo.get("provideSystemNames").toString() : "");
}
if (Tools.isNotNull(serviceAndCatalogInfoMap)) {
dataServiceInfo.setCatalogId(Tools.isNotNull(serviceAndCatalogInfoMap.get("catalogId")) ? serviceAndCatalogInfoMap.get("catalogId").toString() : "");
dataServiceInfo.setCatalogName(Tools.isNotNull(serviceAndCatalogInfoMap.get("catalogName")) ? serviceAndCatalogInfoMap.get("catalogName").toString() : "");
dataServiceInfo.setServiceName(Tools.isNotNull(serviceAndCatalogInfoMap.get("serviceName")) ? serviceAndCatalogInfoMap.get("serviceName").toString() : "");
dataServiceInfo.setTableUnicode(Tools.isNotNull(serviceAndCatalogInfoMap.get("tableUnicode")) ? serviceAndCatalogInfoMap.get("tableUnicode").toString() : "");
}
return dataServiceInfo;
}
private RemovalListener<Object, Object> getRemovalListener() {
return new RemovalListener<Object, Object>() {
public void onRemoval(RemovalNotification<Object, Object> removalNotification) {
String removeLog = removalNotification.getKey() + " was removed, cause is " + removalNotification.getCause();
LoggerUtil.info(LoggerUtil.DATASERVICE_MNG_CORE_LOGGER_INSTANCE, removeLog);
}
};
}
private CacheLoader getCacheLoader() {
return new CacheLoader<String, DataServiceInfo>() {
@Override
public DataServiceInfo load(String serviceId) throws Exception {// 处理缓存键不存在缓存值时的重新获取最新缓存值的处理逻辑
LoggerUtil.info(LoggerUtil.DATASERVICE_MNG_CORE_LOGGER_INSTANCE, "[dataServiceInfoCache] loading dataService is: " + serviceId);
return loadDataServiceInfo(serviceId);
}
};
}
@Override
public DataServiceInfo get(String serviceId) throws Exception {
DataServiceInfo dataServiceInfo = null;
dataServiceInfo = DATA_SERVICE_INFO_CACHE.get(serviceId);
if (Tools.isNull(dataServiceInfo)) {
dataServiceInfo = loadDataServiceInfo(serviceId);
if (Tools.isNotNull(dataServiceInfo)) {
DATA_SERVICE_INFO_CACHE.put(serviceId, dataServiceInfo);
}
return dataServiceInfo;
}
return dataServiceInfo;
}
/**
* @Override public void put(String serviceId, DataServiceInfo dataServiceInfo) {
* DATA_SERVICE_INFO_CACHE.put(serviceId, dataServiceInfo);
* }
* @Override public void putAll(Map<? extends String, ? extends DataServiceInfo> dataServiceInfoMap) {
* DATA_SERVICE_INFO_CACHE.putAll(dataServiceInfoMap);
* }
*/
@Override
public long size() {
return DATA_SERVICE_INFO_CACHE.size();
}
@Override
public void remove(String serviceId) {
DATA_SERVICE_INFO_CACHE.invalidate(serviceId);
}
@Override
public void removeAll(Iterable<Long> serviceIds) {
DATA_SERVICE_INFO_CACHE.invalidateAll(serviceIds);
}
@Override
public void removeAll() {
DATA_SERVICE_INFO_CACHE.invalidateAll();
}
}
3-3 补充
Guava的其它API
另外Guava还提供了下面一些方法,来方便各种需要:
/**
* 该接口的实现被认为是线程安全的,即可在多线程中调用
* 通过被定义单例使用
*/
public interface Cache<K, V> {
/**
* 通过key获取缓存中的value,若不存在直接返回null
*/
V getIfPresent(Object key);
/**
* 一次获得多个键的缓存值
*/
ImmutableMap<K, V> getAllPresent(Iterable<?> var1);
/**
* 获得缓存数据的ConcurrentMap<K, V>快照
*/
ConcurrentMap<K, V> asMap()
/**
* 通过key获取缓存中的value,若不存在就通过valueLoader来加载该value
* 整个过程为 "if cached, return; otherwise create, cache and return"
* 注意valueLoader要么返回非null值,要么抛出异常,绝对不能返回null
*/
V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;
/**
* 添加缓存,若key存在,就覆盖旧值
*/
void put(K key, V value);
/**
* 刷新缓存,即重新取缓存数据,更新缓存
*/
void refresh(K key)
/**
* 从缓存中移除缓存项;删除该key关联的缓存
*/
void invalidate(Object key);
/**
* 从缓存中移除缓存项;删除所有缓存
*/
void invalidateAll();
/**
* 清理缓存 。执行一些维护操作,包括清理缓存
*/
void cleanUp();
}
性能测试
- 配置信息
jdk: 1.8
本机电脑测试:
database: mysql 5.7 / 物理表 32条数据
Guava-Config:
int concurrencyLevel = 8;
long expireAfterWriteTime = 300;
long refreshAfterWriteTime = 600;
int initialCapacity = 100;
int maximumSize = 100;
- 查询性能测试
0.125000s --> 0.015000s (0.125/0.015≈8.3)
【首次查询(无缓存)】
Johnny@LAPTOP-RFPOFJM7 MINGW64 /xx/share-portal (dev)
$ curl -so /tmp/sdc-tmp-data/tmpfile.json -w ' namelookup: %{time_namelookup}
> connect: %{time_connect}
> appconnect: %{time_appconnect}
> pretransfer: %{time_pretransfer}
> redirect: %{time_redirect}
> starttransfer: %{time_starttransfer}
> -------
> total: %{time_total}
> ' http://localhost:18181/backend/access-log/v1/accessStatisticOverview
namelookup: 0.016000
connect: 0.016000
appconnect: 0.000000
pretransfer: 0.016000
redirect: 0.000000
starttransfer: 0.125000
-------
total: 0.125000
【第二/三/四/五/六/...次查询(有缓存)】
Johnny@LAPTOP-RFPOFJM7 MINGW64 /xx/share-portal (dev)
$ curl -so /tmp/sdc-tmp-data/tmpfile.json -w ' namelookup: %{time_namelookup}
> connect: %{time_connect}
> appconnect: %{time_appconnect}
> pretransfer: %{time_pretransfer}
> redirect: %{time_redirect}
> starttransfer: %{time_starttransfer}
> -------
> total: %{time_total}
> ' http://localhost:18181/backend/access-log/v1/accessStatisticOverview
namelookup: 0.015000
connect: 0.015000
appconnect: 0.000000
pretransfer: 0.015000
redirect: 0.000000
starttransfer: 0.015000
-------
total: 0.015000 (第二/三/四/五/六/...次查询均为此结果)
X 参考与推荐文献
- 你应该知道的缓存进化史 - OSChina
- 如何优雅的设计和使用缓存? - OSChina
- 深入解密比Guava Cache更优秀的缓存-Caffeine - chinacion
- java缓存技术总结 - 腾讯云
- JAVA几种缓存技术 - 博客园
- java中的缓存技术该如何实现 - iteye.com
- Java-缓存技术 - 博客园
- Java中的缓存技术 - 网易云课堂
- 介绍两种Google guava缓存使用方式 - CSDN
- [Google Guava] 3-缓存 - ifeve.com
- SpringBoot学习(十一):缓存Guava的使用 - CSDN(推荐)
- Google Guava Cache详解及使用场景 - imooc
- Ehcache与Guava Cache的介绍 - CSDN
- memcached
- ehcache
- 同事乱用缓存,CTO发飙了... - Wexin/数据库开发 【推荐】
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!