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) / EHCache / JCache / ShiftOne / SwarmCache / TreeCache / JBossCache / WhirlyCache
Google Guava(Cache/LoadingCache)
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
- 方案2: CocurrentHashMap
- 方案3: 开源组件(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内立即做出响应。 (存在高并发问题)
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() //只能通过其静态方法newBuilder()来获得CacheBuilder的实例
.concurrencyLevel(8) // 设置并发级别为8,并发级别是指可以同时写缓存的线程数
.expireAfterWrite(120, TimeUnit.SECONDS)// 设置写缓存后120秒钟过期
.initialCapacity(1000)// 设置缓存容器的初始容量为1000
.maximumSize(10000)// 设置缓存最大容量为10000,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
.recordStats()// 设置要统计缓存的命中率
.removalListener(getRemovalListener())
.build(getCacheLoader());
}
@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();
}
}