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. Unity 2D 记录

    Unity 2D 记录 1. 环境配置 1.1 下载安装unity hub和vs code 搜索unity hub 进行下载 https://unity.com/download 安装vs code ...

  2. centos7中通过源码安装postgresql13.6

    下载地址:https://www.postgresql.org/ftp/source/ 0.安装相关依赖库 centos依赖包下载地址:https://developer.aliyun.com/pac ...

  3. 时间格式转换成指定格式(Vue)

    1 /** 2 * Parse the time to string 3 * @param {(Object|string|number)} time 4 * @param {string} cFor ...

  4. 3月1日Android开学学习

    Android开发的简单控件 1.文本显示 2.视图基础 3.常用布局 4.按钮触控 5.图像显示 文本显示 (1)设置文本内容 1.在XML文件中通过属性Android:text设置文本 Andro ...

  5. Verilog标识符与关键字

    Verilog标识符与关键字 1.标识符: Verilog HDL中的标识符是指用来声明数据,变量,端口,例化名等除关键字外的所有名称的组合.如:input a, 这里a就是一个标识符,用来代表一个输 ...

  6. 获取n位数m进制的随机数 js

    js 获取n位数m进制的随机数 n 的取值范围为 0 < n > 1.7976931348623157e+308 (Number.MAX_VALUE) m的取值范围为 2 <= m ...

  7. nodejs批量重命名

    const fs = require("fs"); // directory path let config = {   affix: null,   src: null, }; ...

  8. Crypto入门 (九) easy_RSA

    前言: 建议看这篇随笔之前先看入门(8)数论基础,简单学习下,有利于你看懂后面得算法原理,链接给出:https://www.cnblogs.com/yuanchu/p/13494104.html ea ...

  9. K8s+Docker 学习笔记系列

    学前知识 你需要掌握 Linux 的常用命令.你可以通过Linux 教程 来学习相关命令. Docker应用场景 Web 应用的自动化打包和发布. 自动化测试和持续集成.发布. 在服务型环境中部署和调 ...

  10. 【原创】windows环境下Java串口编程

    由于工作中遇到需要读取SBG Ellipse N系列的惯导模块数据,为了方便操作,我选择在Windows下进行串口开发.串口使用RS232. Ellipse-N RS232的引脚定义 开始我尝试使用的 ...