1 背景

使用场景:计算或检索一个值的代价很高,并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存。

高并发下,为提高 频繁 查询 大量 可能常用的 数据库数据的 查询效率。

大部分情况下,单机用Google Guava(Cache/LoadCache) / ehcache,分布式用redismemcache,各有各的好处,现在企业都是应用很多种中间件供后端程序员选择。

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 参考与推荐文献

[Java EE]缓存技术初探的更多相关文章

  1. Java EE开发技术课程

    新的学期开始了,j2e已经上了两节课,接下来就是对该课程的一些作业以及相关的认识: 一.课程目标: Java EE是java的企业级应用,所以在我看来在学习这门课程之前肯定要对java有一个具体的认识 ...

  2. 《Java EE 开发技术与案例教程》 这是一本好书啊:简洁精辟(相见恨晚)

    第一章:Java EE 概述 1.get:JPA:Java Persistence API, 数据持久化API: JPA是一种ORM规范,它的实现实例:Hibernate.mybatis 2.Web ...

  3. java动态缓存技术:WEB缓存应用(转)

    可以实现不等待,线程自动更新缓存 Java动态缓存jar包请下载. 源代码: CacheData.java 存放缓存数据的Bean /** *  */package com.cari.web.cach ...

  4. Java EE开发技术课程第六周(jsf、facelets)

    1.jsf(java sever faces) 1.1 jsf的定义: jsf是一种用于构建java web应用程序的框架.它提供了一种以组件为中心的用户界面(UI)构建方法,从而简化了Java服务器 ...

  5. Java EE开发技术课程第五周(Applet程序组件与AJAX技术)

    1.Applet程序组件 1.1.定义: Applet是采用Java编程语言编写的小应用程序,该程序可以包含在HTML(标准通用标记语言的一个应用)页中,与在页中包含图像的方式大致相同.含有Apple ...

  6. Java EE开发技术课程第三周

    一.分析Filter例子: @WebFilter(filterName="log",urlPatterns={"/*"})//创建一个LOgFilter类pub ...

  7. Java EE会话技术Cookie和Session

    会话技术 一.定义 会话技术是帮助服务器记住客户端状态的(区分客户端的).将客户访问的信息存在本地的叫Cookie技术,存在服务器上的叫Session技术. 注意: 一次会话何时开始?从打开一个浏览器 ...

  8. Java EE开发技术课程第七周(json)

    JSON: https://baike.baidu.com/item/JSON/2462549?fr=aladdin JSON指JavaScript对象表示法(JavaScript Object No ...

  9. 最重要的 Java EE 最佳实践

    參考:IBM WebSphere 开发人员技术期刊: 最重要的 Java EE 最佳实践 IBM WebSphere 开发人员技术期刊: 最重要的 Java EE 最佳实践 2004 年 IBM® W ...

  10. 各种容器与服务器的区别与联系:Servlet容器、WEB容器、Java EE容器、应用服务器、WEB服务器、Java EE服务器

    1.容器与服务器的联系 如上图,我们先来看下容器与服务器的联系:容器是位于应用程序/组件和服务器平台之间的接口集合,使得应用程序/组件可以方便部署到服务器上运行. 2.各种容器的区别/联系 2-1.容 ...

随机推荐

  1. selenium定位元素位置-xpath

    xpath定位元素位置: 1.定位父兄节点: <div> <span>我是span1标签</span> <a id="1" href=&q ...

  2. PHP实现斐波那契数列(递归 + 非递归)实现

    非递归写法:function fbnq($n){ //传入数列中数字的个数    if($n <= 0){        return 0;    }    $array[1] = $array ...

  3. STL练习-ACboy needs your help again!

      ACboy was kidnapped!! he miss his mother very much and is very scare now.You can't image how dark ...

  4. org.nutz.http.Http忽略https SSL证书验证

    访问的是一个https get请求,报错需要SSL证书验证,以下方法直接跳过 boolean check = Http.disableJvmHttpsCheck(); // 忽略https的证书检查

  5. ZFBJ - 1 - 42 - NO.2

  6. linux下python程序部署

    1.安装python 2.安装virtualenv https://blog.csdn.net/mulangren1988/article/details/59106682 3.指定virtualen ...

  7. Windows下Zookeeper安装使用

    Windows下Zookeeper安装使用 ZooKeeper是一种分布式协调服务,用于管理大型主机. 在分布式环境中协调和管理服务是一个复杂的过程. ZooKeeper通过其简单的架构和API解决了 ...

  8. AWS、谷歌云、Azure:云计算安全功能比较

    每个云平台提供给客户用以保护其云资产安全工具和安全功能都不一样. 公有云安全建立在共担责任概念基础之上:大型云服务提供商交付安全超大规模环境,但保护推上云端一切是客户自己责任.对企业而言,这种安全责任 ...

  9. svn备份迁移

    参考链接: SVN版本库的迁移 dump的详细使用 https://www.iteye.com/blog/tdcq-1684344 svn备份与还原_脚本_(dump命令)

  10. HTTPS、HTTP/2前端入门篇

    随着网络安全重要性日益凸显,越来越多的站点已经全站切换到HTTPS,其中很多HTTPS站点同时将HTTP协议升级到了HTTP/2.作为一只前端,最近一直在学习和应用相关知识点,便总结梳理如下. 一.何 ...