聊一聊JAVA中的缓存规范 —— 虽迟但到的JCache API与天生不俗的Spring Cache

news2024/11/16 13:42:18

为何需要规范

上一章中构建的最简化版本的缓存框架,虽然可以使用,但是也存在一个问题,就是它对外提供的实现接口都是框架根据自己的需要而自定义的。这样一来,项目集成了此缓存框架,后续如果想要更换缓存框架的时候,业务层面的改动会比较大。 —— 因为是自定义的框架接口,无法基于里氏替换原则来进行灵活的更换。

在业界各大厂商或者开源团队都会构建并提供一些自己实现的缓存框架或者组件,提供给开发者按需选择使用。如果大家都是各自闭门造车,势必导致业务中集成并使用某一缓存实现之后,想要更换缓存实现组件会难于登天。

千古一帝秦始皇统一天下后,颁布了书同文、车同轨等一系列法规制度,使得所有的车辆都遵循统一的轴距,然后都可以在官道上正常的通行,大大提升了流通性。而正所谓“国有国法、行有行规”,为了保证缓存框架的通用性、提升项目的可移植性,JAVA行业也迫切需要这么一个缓存规范,来约束各个缓存提供商给出的缓存框架都遵循相同的规范接口,业务中按照标准接口进行调用,无需与缓存框架进行深度耦合,使得缓存组件的更换成为一件简单点的事情。

在JAVA的缓存领域,流传比较广泛的主要是JCache APISpring Cache两套规范,下面就一起来看下。

虽迟但到的JSR107 —— JCache API

提到JAVA中的“行业规矩”,JSR是一个绕不开的话题。它的全称为Java Specification Requests,意思是JAVA规范提案。在该规范标准中,有公布过一个关于JAVA缓存体系的规范定义,也即JSR 107规范(JCache API),主要明确了JAVA中基于内存进行对象缓存构建的一些要求,涵盖内存对象的创建查询更新删除一致性保证等方面内容。

JSR107规范早在2012年时草案就被提出,但却直到2014年才正式披露首个规范版本,也即JCache API 1.0.0版本,至此JAVA领域总算是有个正式的关于缓存的官方规范要求。

揭秘JSR107 —— JCache API内容探究

JSR107规范具体的要求形式,都以接口的形式封装在javax.cache包中进行提供。我们要实现的缓存框架需要遵循该规范,也就是需要引入javax.cache依赖包,并实现其中提供的相关接口即可。对于使用maven构建的项目中,可以在pom.xml中引入javax.cache依赖:

<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.1.1</version>
</dependency>
复制代码

JCache API规范中,定义的缓存框架相关接口类之间的关系逻辑梳理如下:

我们要实现自己的本地缓存框架,也即需要实现上述各个接口。对上述各接口类的含义介绍说明如下:

接口类功能定位描述
CachingProviderSPI接口,缓存框架的加载入口。每个Provider中可以持有1个或者多个CacheManager对象,用来提供不同的缓存能力
CacheManager缓存管理器接口,每个缓存管理器负责对具体的缓存容器的创建与管理,可以管理1个或者多个不同的Cache对象
CacheCache缓存容器接口,负责存储具体的缓存数据,可以提供不同的容器能力
EntryCache容器中存储的key-value键值对记录

作为通用规范,这里将CachingProvider定义为了一个SPI接口Service Provider Interface,服务提供接口),主要是借助JDK自带的服务提供发现能力,来实现按需加载各自实现的功能逻辑,有点IOC的意味。这样设计有一定的好处:

  • 对于框架

需要遵循规范,提供上述接口的实现类。然后可以实现热插拔,与业务解耦。

  • 对于业务

先指定需要使用的SPI的具体实现类,然后业务逻辑中便无需感知缓存具体的实现,直接基于JCache API通用接口进行使用即可。后续如果需要更换缓存实现框架,只需要切换下使用的SPI的具体实现类即可。

根据上述介绍,一个基于JCache API实现的缓存框架在实际项目中使用时的对象层级关系可能会是下面这种场景(假设使用LRU策略存储部门信息、使用普通策略存储用户信息):

那么如何去理解JCache API中几个接口类的关系呢?

几个简单的说明:

  1. CachingProvider并无太多实际逻辑层面的功能,只是用来基于SPI机制,方便项目中集成插拔使用。内部持有CacheManager对象,实际的缓存管理能力,由CacheManager负责提供。

  2. CacheManager负责具体的缓存管理相关能力实现,实例由CachingProvider提供并持有,CachingProvider可以持有一个或者多个不同的CacheManager对象。这些CacheManager对象可以是相同类型,也可以是不同类型,比如我们可以实现2种缓存框架,一种是基于内存的缓存,一种是基于磁盘的缓存,则可以分别提供两种不同的CacheManager,供业务按需调用。

  3. Cache是CacheManager负责创建并管理的具体的缓存容器,也可以有一个或者多个,如业务中会涉及到为用户列表和部门列表分别创建独立的Cache存储。此外,Cache容器也可以根据需要提供不同的Cache容器类型,以满足不同场景对于缓存容器的不同诉求,如我们可以实现一个类似HashMap的普通键值对Cache容器,也可以提供一个基于LRU淘汰策略的Cache容器。

至此呢,我们厘清了JCache API规范的大致内容。

插叙 —— SPI何许人也

按照JSR107规范试编写缓存具体能力时,我们需要实现一个SPI接口的实现类,然后由JDK提供的加载能力将我们扩展的缓存服务加载到JVM中供使用。

提到API我们都耳熟能详,也就是我们常规而言的接口。但说起SPI也许很多小伙伴就有点陌生了。其实SPI也并非是什么新鲜玩意,它是JDK内置的一种服务的提供发现加载机制。按照JAVA的面向对象编码的思想,为了降低代码的耦合度、提升代码的灵活性,往往需要利用好抽象这一特性,比如一般会比较推荐基于接口进行编码、而尽量避免强依赖某个具体的功能实现类 —— 这样才能让构建出的系统具有更好的扩展性,更符合面向对象设计原则中的里式替换原则。SPI便是为了支持这一诉求而提供的能力,它允许将接口具体的实现类交由业务或者三方进行独立构建,然后加载到JVM中以供业务进行使用。

为了这一点,我们需要在resource/META-INF/services目录下新建一个文件,文件名即为SPI接口名称javax.cache.spi.CachingProvider,然后在文件内容中,写入我们要注入进入的我们自己的Provider实现类:

这样,我们就完成了将我们自己的MyCachingProvider功能注入到系统中。在业务使用时,可以通过Caching.getCachingProvider()获取到注入的自定义Provider

public static void main(String[] args) {
    CachingProvider provider =  Caching.getCachingProvider();
    System.out.println(provider);
}
复制代码

从输出的结果可以看出,获取到了自定义的Provider对象:

com.veezean.skills.cache.fwk.MyCachingProvider@7adf9f5f
复制代码

获取到Provider之后,便可以进一步的获取到Manager对象,进而业务层面层面可以正常使用。

JCache API规范的实现

JSR作为JAVA领域正统行规,制定的时候往往考虑到各种可能的灵活性与通用性。作为JSR中根正苗红的JCache API规范,也沿袭了这一风格特色,框架接口的定义与实现也非常的丰富,几乎可以扩展自定义任何你需要的处理策略。 —— 但恰是这一点,也让其整个框架的接口定义过于重量级。对于缓存框架实现者而言,遵循JCache API需要实现众多的接口,需要做很多额外的实现处理。

比如,我们实现CacheManager的时候,需要实现如下这么多的接口:

public class MemCacheManager implements CacheManager {
    private CachingProvider cachingProvider;
    private ConcurrentHashMap<String, Cache> caches;
    public MemCacheManager(CachingProvider cachingProvider, ConcurrentHashMap<String, Cache> caches) {
        this.cachingProvider = cachingProvider;
        this.caches = caches;
    }
    @Override
    public CachingProvider getCachingProvider() {
    }
    @Override
    public URI getURI() {
    }
    @Override
    public ClassLoader getClassLoader() {
    }
    @Override
    public Properties getProperties() {
    }
    @Override
    public <K, V, C extends Configuration<K, V>> Cache<K, V> createCache(String s, C c) throws IllegalArgumentException {
    }
    @Override
    public <K, V> Cache<K, V> getCache(String s, Class<K> aClass, Class<V> aClass1) {
    }
    @Override
    public <K, V> Cache<K, V> getCache(String s) {
    }
    @Override
    public Iterable<String> getCacheNames() {
    }
    @Override
    public void destroyCache(String s) {
    }
    @Override
    public void enableManagement(String s, boolean b) {
    }
    @Override
    public void enableStatistics(String s, boolean b) {
    }
    @Override
    public void close() {
    }
    @Override
    public boolean isClosed() {
    }
    @Override
    public <T> T unwrap(Class<T> aClass) {
    }
}
复制代码

长长的一摞接口等着实现,看着都令人上头,作为缓存提供商,便需要按照自己的能力去实现这些接口,以保证相关缓存能力是按照规范对外提供。也正是因为JCache API这种不接地气的表现,让其虽是JAVA 领域的正统规范,却经常被束之高阁,沦落成为了一种名义规范。业界主流的本地缓存框架中,比较出名的当属Ehcache了(当然,Spring4.1中也增加了对JSR规范的支持)。此外,Redis的本地客户端Redisson也有实现全套JCache API规范,用户可以基于Redisson调用JCache API的标准接口来进行缓存数据的操作。

JSR107提供的注解操作方法

前面提到了作为供应商想要实现JSR107规范的时候会比较复杂,需要做很多自己的处理逻辑。但是对于业务使用者而言,JSR107还是比较贴心的。比如JSR107中就将一些常用的API方法封装为注解,利用注解来大大简化编码的复杂度,降低缓存对于业务逻辑的侵入性,使得业务开发人员可以更加专注于业务本身的开发。

JSR107规范中常用的一些缓存操作注解方法梳理如下面的表格:

注解含义说明
@CacheResult将指定的keyvalue映射内容存入到缓存容器中
@CachePut更新指定缓存容器中指定key值缓存记录内容
@CacheRemove移除指定缓存容器中指定key值对应的缓存记录
@CacheRemoveAll字面含义,移除指定缓存容器中的所有缓存记录
@CacheKey作为接口参数前面修饰,用于指定特定的入参作为缓存key值的组成部分
@CacheValue作为接口参数前面的修饰,用于指定特定的入参作为缓存value

上述注解主要是添加在方法上面,用于自动将方法的入参与返回结果之间进行一个映射与自动缓存,对于后续请求如果命中缓存则直接返回缓存结果而无需再次执行方法的具体处理,以此来提升接口的响应速度与承压能力。

比如下面的查询接口上,通过@CacheResult注解可以将查询请求与查询结果缓存起来进行使用:

@CacheResult(cacheName = "books")
public Book findBookByName(@CacheKey String bookName) {
    return bookDao.queryByName(bookName);
}
复制代码

Book信息发生变更的时候,为了保证缓存数据的准确性,需要同步更新缓存内容。可以通过在更新方法上面添加@CachePut接口即可达成目的:

@CachePut(cacheName = "books")
public void updateBookInfo(@CacheKey String bookName, @CacheValue Book book) {
    bookDao.updateBook(bookName, book);
}
复制代码

这里分别适用了@CacheKey@CacheValue指定了需要更新的缓存记录key值,以及需要将其更新为的新的value值。

同样地,借助注解@CacheRemove可以完成对应缓存记录的删除:

@CacheRemove(cacheName = "books")
public void deleteBookInfo(@CacheKey String bookName) {
    bookDao.deleteBookByName(bookName)
}
复制代码

爱屋及乌 —— Spring框架制定的Cache规范

JSR 107(JCache API)规范的诞生可谓是一路坎坷,拖拖拉拉直到2014年才发布了首个1.0.0版本规范。但是在JAVA界风头无两的Spring框架早在2011年就已经在其3.1版本中提供了缓存抽象层的规范定义,并借助Spring的优秀设计与良好生态,迅速得到了各个软件开发团体的青睐,各大缓存厂商也陆续提供了符合Spring Cache规范的自家缓存产品。

Spring Cache并非是一个具体的缓存实现,而是和JSR107类似的一套缓存规范,基于注解并可实现与Spring的各种高级特性无缝集成,受到了广泛的追捧。各大缓存提供商几乎都有基于Spring Cache规范进行实现的缓存组件。比如后面我们会专门介绍的Guava CacheCaffeine Cache以及同样支持JSR107规范的Ehcache等等。

得力于Spring在JAVA领域无可撼动的地位,造就了Spring Cache已成为JAVA缓存领域的“事实标准”,深有“功高盖主”的味道。

Spring Cache使用不同缓存组件

如果要基于Spring Cache规范来进行缓存的操作,首先在项目中需要引入此规范的定义:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
复制代码

这样,在业务代码中,就可以使用Spring Cache规范中定义的一些注解方法。前面有提过,Spring Cache只是一个规范声明,可以理解为一堆接口定义,而并没有提供具体的接口功能实现。具体的功能实现,由业务根据实际选型需要,引入相应缓存组件的jar库文件依赖即可 —— 这一点是Spring框架中极其普遍的一种做法。

假如我们需要使用Guava Cache来作为我们实际缓存能力提供者,则我们只需要引入对应的依赖即可:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1.1-jre</version>
</dependency>
复制代码

这样一来,我们便实现了使用Guava cache作为存储服务提供者、且基于Spring Cache接口规范进行缓存操作。Spring作为JAVA领域的一个相当优秀的框架,得益于其优秀的封装设计思想,使得更换缓存组件也显得非常容易。比如现在想要将上面的Guava cache更换为Caffeine cache作为新的缓存能力提供者,则业务代码中将依赖包改为Caffeine cache并简单的做一些细节配置即可:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.1</version>
</dependency>
复制代码

这样一来,对于业务使用者而言,可以方便的进行缓存具体实现者的替换。而作为缓存能力提供商而言,自己可以轻易的被同类产品替换掉,所以也鞭策自己去提供更好更强大的产品,巩固自己的地位,也由此促进整个生态的良性演进

Spring Cache规范提供的注解

需要注意的是,使用Spring Cache缓存前,需要先手动开启对于缓存能力的支持,可以通过@EnableCaching注解来完成。

除了 @EnableCaching ,在Spring Cache中还定义了一些其它的常用注解方法,梳理归纳如下:

注解含义说明
@EnableCaching开启使用缓存能力
@Cacheable添加相关内容到缓存中
@CachePut更新相关缓存记录
@CacheEvict删除指定的缓存记录,如果需要清空指定容器的全部缓存记录,可以指定allEntities=true来实现

具体的使用上,其实和JSR107规范中提供的注解用法相似。

当然了,JAVA领域缓存事实规范地位虽已奠定,但是Spring Cache依旧是保持着一个兼收并蓄的姿态,并积极的兼容了JCache API相关规范,比如Spring4.1起项目中可以使用JSR107规范提供的相关注解方法来操作。

he

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1525.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

哈希的应用

文章目录前言一、位图1.1位图概念1.2位图的实现1.3 位图的应用二、 布隆过滤器2.1 布隆过滤器提出2.2 布隆过滤器概念2.3 布隆过滤器的插入2.4 布隆过滤器的哈希函数2.5 布隆过滤器的查找2.6 布隆过滤器删除2.7 布隆过滤器的优点2.8 布隆过滤器的缺陷2.9 布隆过滤器的应用场景前…

散射辐射变送器的优势体现在哪些方面?

散射辐射是经过大气分子、水蒸气、灰尘等质点的散射&#xff0c;改变了方向的太阳辐射&#xff0c;也称天空散射辐射。太阳散射辐射强弱程度与太阳辐射的入射角、大气条件&#xff08;云量、水汽、砂粒、烟尘等粒子的多少&#xff09;和地面反射率有关。当天空的浑浊程度加大&a…

链路状态路由协议 OSPF (三)

作者简介&#xff1a;一名在校云计算网络运维学生、每天分享网络运维的学习经验、和学习笔记。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.OSPF领接关系的建立 1.OSPF领接关系的建立概述 &#xff0…

彻底理解Java并发:乐观锁与CAS

本篇内容包括&#xff1a;悲观锁与乐观锁的概述、CAS&#xff08;Compare And Swap&#xff09;比较并交换的介绍、非阻塞算法与ABA问题&#xff0c;以及对 Java 中 CAS 的实现解读&#xff08;AtomicInteger 对 CAS 的实现&#xff0c;Unsafe 类简介&#xff09;。 一、悲观锁…

【树莓派不吃灰】Raspberry Pi上搭建NodeJS运行环境

目录1. 前言2. 安装NodeJS环境2.1 安装npm2.2 安装nodejs2.3 配置NPM国内镜像源3. 总结❤️ 博客主页 单片机菜鸟哥&#xff0c;一个野生非专业硬件IOT爱好者 ❤️❤️ 本篇创建记录 2022-10-28 ❤️❤️ 本篇更新记录 2022-10-28 ❤️&#x1f389; 欢迎关注 &#x1f50e;点赞…

嵌入式C语言编程中经验教训总结(八)变量、指针和指针数组的内存管理

目录嵌入式C语言编程中经验教训总结&#xff08;八&#xff09;变量、指针和指针数组的内存管理变量、指针和指针数组的内存占用指针、指针数组的空间验证指针数组的元素数据访问方法嵌入式C语言编程中经验教训总结&#xff08;八&#xff09;变量、指针和指针数组的内存管理 …

【趣学算法】第一章读书笔记

14天阅读挑战赛 *努力是为了不平庸~ 算法学习有些时候是枯燥的&#xff0c;这一次&#xff0c;让我们先人一步&#xff0c;趣学算法&#xff01; 文章目录1.1打开算法之门1.2 妙不可言——算法复杂性算法的特性好算法的标准时间复杂度和空间复杂度时间复杂度空间复杂度宕机1.4算…

62. 如何通过增强(Enhancement) 的方式给 SAP ABAP 标准程序增添新功能

文章目录 如何找到可以创建增强实现的增强点位置如何创建增强实现如何在 SE80 里找到增强实现本身如何调试 ABAP 增强实现总结ABAP 系统有比较完善的修改控制权限管控,比如笔者试图修改一个 SAP ABAP 系统里标准的函数,就会遇到如下的警告消息,然后修改的尝试会被阻止: You…

Winform和ASP.NET、Web API详解

Winform和ASP.NET、Web API 一、winform基础 1.1 基础学习 1、 winform应用程序是一种智能客户端技术&#xff0c;我们可以使用winform应用程序帮助我们获得信息或者传输信息等。 2、属性 Name:在后台要获得前台的控件对象&#xff0c;需要使用Name属性。 visible:指示一…

认识运营商机房

文章目录走线设备机房走线数据机房走线传输机房列头柜【供电】网络架构ONU设备OLT设备汇聚层交换机BARS设备核心路由器运营商网络架构【必看】铁塔基站核心机房ODF&#xff1a;光纤配线架MME光纤SGWPGWHSS交换机拓扑核心机房拓扑接入层基站&#xff08;BaseStation&#xff09;…

山西大同大学技术会,大同大学的家!

大家好&#xff0c;我是康来个程&#xff0c;山西大同大学技术会的创建者。 低谷时代 近几年校内的竞赛氛围越来越浓厚&#xff0c;随着自身参与并了解的赛事越来越多&#xff0c;随之而来的也是发现了我们学校竞赛方面的问题。疫情原因&#xff0c;我们的比赛取消的取消&…

Gitee在大数据中心的使用

在本地主机或者可以VSCode直接连接可视化的服务器上 1. 首先在gitee新建一个带有develop分支的仓库 2. 在自己的主机&#xff08;e.g., server 1~3&#xff09;上git clone下来&#xff0c;例如 git clone gitgitee.com:PeterBishop0/TransT-based.git 3. 切换成develop分支&…

深度学习入门(十) 模型选择、过拟合和欠拟合

深度学习入门&#xff08;十&#xff09; 模型选择、过拟合和欠拟合前言模型选择例子&#xff1a;预测谁会偿还贷款&#xff1f;训练误差和泛化误差验证数据集和测试数据集K-则交叉验证总结过拟合和欠拟合模型容量模型容量的影响估计模型容量VC维线性分类器的VC维VC维的用处数据…

[云原生之k8s] Kubernetes原理

引言 单机容器编排&#xff1a;docker-compose 容器集群编排&#xff1a;docker swarm、mesosmarathon、kubernetes 应用编排&#xff1a;ansible 一、Kubernetes是什么&#xff1f; Kubernetes的缩写为&#xff1a;K8S&#xff0c;这个缩写是因为k和s之间有八个字符的关系…

线段树模板

好文分享&#xff1a;【数据结构】线段树&#xff08;Segment Tree&#xff09; - 小仙女本仙 - 博客园 线段树和树状数组的基本功能都是在某一满足结合律的操作(比如加法&#xff0c;乘法&#xff0c;最大值&#xff0c;最小值)下&#xff0c;O(logn)的时间复杂度内修改单个元…

Python回归预测建模实战-支持向量机预测房价(附源码和实现效果)

机器学习在预测方面的应用&#xff0c;根据预测值变量的类型可以分为分类问题&#xff08;预测值是离散型&#xff09;和回归问题&#xff08;预测值是连续型&#xff09;&#xff0c;前面我们介绍了机器学习建模处理了分类问题&#xff08;具体见之前的文章&#xff09;&#…

x86 --- 任务隔离特权级保护

程序是记录在载体上的数据和指令。 程序正在执行时的一个副本叫做任务 所有段描述符都放在GDT --> 不做区分。 内核程序&#xff08;任务&#xff09;所占段在GDT中&#xff0c;用户程序&#xff08;任务&#xff09;所占段在LDT中 --> 做区分。 每个任务都有自己独立的…

【无标题】

第1章 概述 本章主要内容&#xff1a; 互联网的概念&#xff08;标准化&#xff09;、组成、发展历程&#xff1b;电路交换的基本概念、分组交换的原理&#xff1b;计算机网络的分类、性能指标及两种体系结构。 重点掌握&#xff1a; 在计算机网络分层模型中&#xff0c;网…

7、GC日志详解

目录如何分析GC日志参数配置程序运行GC日志打印解析GC日志数据分析指定其他垃圾收集器CMSG1GC分析工具JVM参数汇总查看命令如何分析GC日志 参数配置 对于java应用我们可以通过一些配置把程序运行过程中的gc日志全部打印出来&#xff0c;然后分析gc日志得到关键性指标&#xff…

目标检测算法——遥感影像数据集资源汇总(附下载链接)

关注”PandaCVer“公众号 深度学习资料&#xff0c;第一时间送达 目录 一、用于 2-5 分类问题 1.UCAS-AOD 遥感影像数据集 2.Inria Aerial Image Labeling Dataset 3.RSOD-Dataset 物体检测数据集 二、用于 5-10 分类问题 1.RSSCN7 DataSet 遥感图像数据集 2.NWPU…