0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

使用Spring Cache实现缓存

jf_ro2CN3Fa 来源:勇哥java实战分享 2023-05-11 17:40 次阅读

1 硬编码

在学习Spring Cache之前,笔者经常会硬编码的方式使用缓存。

举个例子,为了提升用户信息的查询效率,我们对用户信息使用了缓存,示例代码如下:

@Autowire
privateUserMapperuserMapper;
@Autowire
privateStringCommandstringCommand;
//查询用户
publicUsergetUserById(LonguserId){
StringcacheKey="userId_"+userId;
Useruser=stringCommand.get(cacheKey);
if(user!=null){
returnuser;
}
user=userMapper.getUserById(userId);
if(user!=null){
stringCommand.set(cacheKey,user);
returnuser;
}
//修改用户
publicvoidupdateUser(Useruser){
userMapper.updateUser(user);
StringcacheKey="userId_"+userId.getId();
stringCommand.set(cacheKey,user);
}
//删除用户
publicvoiddeleteUserById(LonguserId){
userMapper.deleteUserById(userId);
StringcacheKey="userId_"+userId.getId();
stringCommand.del(cacheKey);
}
}

相信很多同学都写过类似风格的代码,这种风格符合面向过程的编程思维,非常容易理解。但它也有一些缺点:

代码不够优雅。业务逻辑有四个典型动作:存储读取修改删除。每次操作都需要定义缓存Key ,调用缓存命令的API,产生较多的重复代码

缓存操作和业务逻辑之间的代码耦合度高,对业务逻辑有较强的侵入性。

侵入性主要体现如下两点:

开发联调阶段,需要去掉缓存,只能注释或者临时删除缓存操作代码,也容易出错;

某些场景下,需要更换缓存组件,每个缓存组件有自己的API,更换成本颇高。

2 缓存抽象

首先需要明确一点:Spring Cache不是一个具体的缓存实现方案,而是一个对缓存使用的抽象(Cache Abstraction )。

dc106b70-ed7d-11ed-90ce-dac502259ad0.png

2.1 Spring AOP

Spring AOP是基于代理模式(proxy-based )。

通常情况下,定义一个对象,调用它的方法的时候,方法是直接被调用的。

Pojopojo=newSimplePojo();
pojo.foo();
dc177a50-ed7d-11ed-90ce-dac502259ad0.png

将代码做一些调整,pojo对象的引用修改成代理类。

ProxyFactoryfactory=newProxyFactory(newSimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(newRetryAdvice());

Pojopojo=(Pojo)factory.getProxy();
//thisisamethodcallontheproxy!
pojo.foo();
dc1e6d92-ed7d-11ed-90ce-dac502259ad0.png

调用pojo的foo方法的时候,实际上是动态生成的代理类调用foo方法。

代理类在方法调用前可以获取方法的参数,当调用方法结束后,可以获取调用该方法的返回值,通过这种方式就可以实现缓存的逻辑。

2.2 缓存声明

缓存声明,也就是标识需要缓存的方法以及缓存策略

Spring Cache 提供了五个注解。

@Cacheable:根据方法的请求参数对其结果进行缓存,下次同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法;

@CachePut:根据方法的请求参数对其结果进行缓存,它每次都会触发真实方法的调用;

@CacheEvict:根据一定的条件删除缓存;

@Caching:组合多个缓存注解;

@CacheConfig:类级别共享缓存相关的公共配置。

我们重点讲解:@Cacheable,@CachePut,@CacheEvict三个核心注解。

2.2.1 @Cacheable注解

@Cacheble注解表示这个方法有了缓存的功能。

@Cacheable(value="user_cache",key="#userId",unless="#result==null")
publicUsergetUserById(LonguserId){
Useruser=userMapper.getUserById(userId);
returnuser;
}

上面的代码片段里,getUserById方法和缓存user_cache 关联起来,若方法返回的User对象不为空,则缓存起来。第二次相同参数userId调用该方法的时候,直接从缓存中获取数据,并返回。

▍ 缓存key的生成

我们都知道,缓存的本质是key-value存储模式,每一次方法的调用都需要生成相应的Key, 才能操作缓存。

通常情况下,@Cacheable有一个属性key可以直接定义缓存key,开发者可以使用SpEL语言定义key值。

若没有指定属性key,缓存抽象提供了 KeyGenerator来生成key ,默认的生成器代码见下图:

dc258456-ed7d-11ed-90ce-dac502259ad0.png

它的算法也很容易理解:

如果没有参数,则直接返回SimpleKey.EMPTY

如果只有一个参数,则直接返回该参数;

若有多个参数,则返回包含多个参数的SimpleKey 对象。

当然Spring Cache也考虑到需要自定义Key生成方式,需要我们实现org.springframework.cache.interceptor.KeyGenerator 接口

Objectgenerate(Objecttarget,Methodmethod,Object...params);

然后指定@Cacheable的keyGenerator属性。

@Cacheable(value="user_cache",keyGenerator="myKeyGenerator",unless="#result==null")
publicUsergetUserById(LonguserId)

▍ 缓存条件

有的时候,方法执行的结果是否需要缓存,依赖于方法的参数或者方法执行后的返回值。

注解里可以通过condition属性,通过Spel表达式返回的结果是true 还是false 判断是否需要缓存。

@Cacheable(cacheNames="book",condition="#name.length()< 32")
public Book findBook(String name)

上面的代码片段里,当参数的长度小于32,方法执行的结果才会缓存。

除了condition,unless属性也可以决定结果是否缓存,不过是在执行方法后。

@Cacheable(value="user_cache",key="#userId",unless="#result==null")
publicUsergetUserById(LonguserId){

上面的代码片段里,当返回的结果为null则不缓存。

2.2.2 @CachePut注解

@CachePut注解作用于缓存需要被更新的场景,和 @Cacheable 非常相似,但被注解的方法每次都会被执行。

返回值是否会放入缓存,依赖于condition和unless,默认情况下结果会存储到缓存。

@CachePut(value="user_cache",key="#user.id",unless="#result!=null")
publicUserupdateUser(Useruser){
userMapper.updateUser(user);
returnuser;
}

当调用updateUser方法时,每次方法都会被执行,但是因为unless属性每次都是true,所以并没有将结果缓存。当去掉unless属性,则结果会被缓存。

2.2.3 @CacheEvict注解

@CacheEvict 注解的方法在调用时会从缓存中移除已存储的数据。

@CacheEvict(value="user_cache",key="#id")
publicvoiddeleteUserById(Longid){
userMapper.deleteUserById(id);
}

当调用deleteUserById方法完成后,缓存key等于参数id的缓存会被删除,而且方法的返回的类型是Void ,这和@Cacheable明显不同。

2.3 缓存配置

Spring Cache是一个对缓存使用的抽象,它提供了多种存储集成。

dc2c4d22-ed7d-11ed-90ce-dac502259ad0.png

要使用它们,需要简单地声明一个适当的CacheManager - 一个控制和管理Cache的实体。

我们以Spring Cache默认的缓存实现Simple 例子,简单探索下CacheManager的机制。

CacheManager非常简单:

publicinterfaceCacheManager{
@Nullable
CachegetCache(Stringname);

CollectiongetCacheNames();
}

在CacheConfigurations配置类中,可以看到不同集成类型有不同的缓存配置类。

dc33c98a-ed7d-11ed-90ce-dac502259ad0.png

通过SpringBoot的自动装配机制,创建CacheManager的实现类ConcurrentMapCacheManager。

dc3d0112-ed7d-11ed-90ce-dac502259ad0.png

而ConcurrentMapCacheManager的getCache方法,会创建ConcurrentCacheMap。

dc486b24-ed7d-11ed-90ce-dac502259ad0.png

ConcurrentCacheMap实现了org.springframework.cache.Cache接口。

dc4dc2e0-ed7d-11ed-90ce-dac502259ad0.png

从Spring Cache的Simple 的实现,缓存配置需要实现两个接口:

org.springframework.cache.CacheManager

org.springframework.cache.Cache

3 入门例子

首先我们先创建一个工程spring-cache-demo。

dc5a371e-ed7d-11ed-90ce-dac502259ad0.png

caffeine和Redisson分别是本地内存和分布式缓存Redis框架中的佼佼者,我们分别演示如何集成它们。

3.1 集成caffeine

3.1.1 maven依赖


org.springframework.boot
spring-boot-starter-cache


com.github.ben-manes.caffeine
caffeine
2.7.0

3.1.2 Caffeine缓存配置

我们先创建一个缓存配置类MyCacheConfig。

@Configuration
@EnableCaching
publicclassMyCacheConfig{
@Bean
publicCaffeinecaffeineConfig(){
return
Caffeine.newBuilder()
.maximumSize(10000).
expireAfterWrite(60,TimeUnit.MINUTES);
}
@Bean
publicCacheManagercacheManager(Caffeinecaffeine){
CaffeineCacheManagercaffeineCacheManager=newCaffeineCacheManager();
caffeineCacheManager.setCaffeine(caffeine);
returncaffeineCacheManager;
}
}

首先创建了一个Caffeine对象,该对象标识本地缓存的最大数量是10000条,每个缓存数据在写入60分钟后失效。

另外,MyCacheConfig类上我们添加了注解:**@EnableCaching** 。

3.1.3 业务代码

根据缓存声明 这一节,我们很容易写出如下代码。

@Cacheable(value="user_cache",unless="#result==null")
publicUsergetUserById(Longid){
returnuserMapper.getUserById(id);
}
@CachePut(value="user_cache",key="#user.id",unless="#result==null")
publicUserupdateUser(Useruser){
userMapper.updateUser(user);
returnuser;
}
@CacheEvict(value="user_cache",key="#id")
publicvoiddeleteUserById(Longid){
userMapper.deleteUserById(id);
}

这段代码与硬编码里的代码片段明显精简很多。

当我们在Controller层调用 getUserById方法时,调试的时候,配置mybatis日志级别为DEBUG,方便监控方法是否会缓存。

第一次调用会查询数据库,打印相关日志:

Preparing:select*FROMusertwheret.id=?
Parameters:1(Long)
Total:1

第二次调用查询方法的时候,数据库SQL日志就没有出现了, 也就说明缓存生效了。

3.2 集成Redisson

3.2.1 maven依赖


org.Redisson
Redisson
3.12.0

3.2.2 Redisson缓存配置

@Bean(destroyMethod="shutdown")
publicRedissonClientRedisson(){
Configconfig=newConfig();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6201").setPassword("ts112GpO_ay");
returnRedisson.create(config);
}
@Bean
CacheManagercacheManager(RedissonClientRedissonClient){
Mapconfig=newHashMap();
//create"user_cache"springcachewithttl=24minutesandmaxIdleTime=12minutes
config.put("user_cache",
newCacheConfig(
24*60*1000,
12*60*1000));
returnnewRedissonSpringCacheManager(RedissonClient,config);
}

可以看到,从Caffeine切换到Redisson,只需要修改缓存配置类,定义CacheManager 对象即可。而业务代码并不需要改动。

Controller层调用 getUserById方法,用户ID为1的时候,可以从Redis Desktop Manager里看到:用户信息已被缓存,user_cache缓存存储是Hash数据结构。

dc6323f6-ed7d-11ed-90ce-dac502259ad0.png

因为Redisson默认的编解码是FstCodec , 可以看到key的名称是:xF6x01。

在缓存配置代码里,可以修改编解码器。

publicRedissonClientRedisson(){
Configconfig=newConfig();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6201").setPassword("ts112GpO_ay");
config.setCodec(newJsonJacksonCodec());
returnRedisson.create(config);
}

再次调用 getUserById方法 ,控制台就变成:

dc6cc2da-ed7d-11ed-90ce-dac502259ad0.png

可以观察到:缓存key已经变成了:["java.lang.Long",1],改变序列化后key和value已发生了变化。

3.3 从列表缓存再次理解缓存抽象

列表缓存在业务中经常会遇到。通常有两种实现形式:

整体列表缓存;

按照每个条目缓存,通过redis,memcached的聚合查询方法批量获取列表,若缓存没有命中,则从数据库重新加载,并放入缓存里。

那么Spring cache整合Redisson如何缓存列表数据呢?

@Cacheable(value="user_cache")
publicListgetUserList(ListidList){
returnuserMapper.getUserByIds(idList);
}

执行getUserList方法,参数id列表为:[1,3] 。

wKgZomRcuNWAQQl8AADE0jZKrtw956.jpg

执行完成之后,控制台里可以看到:列表整体直接被缓存起来,用户列表缓存和用户条目缓存并没有共享 ,他们是平行的关系。

这种情况下,缓存的颗粒度控制也没有那么细致。

类似这样的思考,很多开发者也向Spring Framework研发团队提过。

dc7966a2-ed7d-11ed-90ce-dac502259ad0.png

官方的回答也很明确:对于缓存抽象来讲,它并不关心方法返回的数据类型,假如是集合,那么也就意味着需要把集合数据在缓存中保存起来。

还有一位开发者,定义了一个@CollectionCacheable 注解,并做出了原型,扩展了Spring Cache的列表缓存功能。

@Cacheable("myCache")
publicStringfindById(Stringid){
//accessDBbackendreturnitem
}
@CollectionCacheable("myCache")
publicMapfindByIds(Collectionids){
//accessDBbackend,returnmapofidtoitem
}

官方也未采纳,因为缓存抽象并不想引入太多的复杂性

写到这里,相信大家对缓存抽象有了更进一步的理解。当我们想实现更复杂的缓存功能时,需要对Spring Cache做一定程度的扩展。

4 自定义二级缓存

4.1 应用场景

笔者曾经在原来的项目,高并发场景下多次使用多级缓存。多级缓存是一个非常有趣的功能点,值得我们去扩展。

多级缓存有如下优势:

离用户越近,速度越快;

减少分布式缓存查询频率,降低序列化和反序列化的CPU消耗;

大幅度减少网络IO以及带宽消耗。

进程内缓存做为一级缓存,分布式缓存做为二级缓存,首先从一级缓存中查询,若能查询到数据则直接返回,否则从二级缓存中查询,若二级缓存中可以查询到数据,则回填到一级缓存中,并返回数据。若二级缓存也查询不到,则从数据源中查询,将结果分别回填到一级缓存,二级缓存中。

dc91f6ae-ed7d-11ed-90ce-dac502259ad0.png

来自《凤凰架构》缓存篇

Spring Cache并没有二级缓存的实现,我们可以实现一个简易的二级缓存DEMO,加深对技术的理解。

4.2 设计思路

MultiLevelCacheManager :多级缓存管理器;

MultiLevelChannel :封装Caffeine和RedissonClient;

MultiLevelCache :实现org.springframework.cache.Cache接口;

MultiLevelCacheConfig :配置缓存过期时间等;

MultiLevelCacheManager是最核心的类,需要实现getCachegetCacheNames 两个接口。

dc9b76de-ed7d-11ed-90ce-dac502259ad0.png

创建多级缓存,第一级缓存是:Caffeine , 第二级缓存是:Redisson。

dca99c3c-ed7d-11ed-90ce-dac502259ad0.png

二级缓存,为了快速完成DEMO,我们使用Redisson对Spring Cache的扩展类RedissonCache 。它的底层是RMap ,底层存储是Hash。

dcc2aba0-ed7d-11ed-90ce-dac502259ad0.png

我们重点看下缓存的「查询」和「存储」的方法:

@Override
publicValueWrapperget(Objectkey){
Objectresult=getRawResult(key);
returntoValueWrapper(result);
}

publicObjectgetRawResult(Objectkey){
logger.info("从一级缓存查询key:"+key);
Objectresult=localCache.getIfPresent(key);
if(result!=null){
returnresult;
}
logger.info("从二级缓存查询key:"+key);
result=RedissonCache.getNativeCache().get(key);
if(result!=null){
localCache.put(key,result);
}
returnresult;
}

查询 」数据的流程:

先从本地缓存中查询数据,若能查询到,直接返回;

本地缓存查询不到数据,查询分布式缓存,若可以查询出来,回填到本地缓存,并返回;

若分布式缓存查询不到数据,则默认会执行被注解的方法。

下面来看下「存储 」的代码:

publicvoidput(Objectkey,Objectvalue){
logger.info("写入一级缓存key:"+key);
localCache.put(key,value);
logger.info("写入二级缓存key:"+key);
RedissonCache.put(key,value);
}

最后配置缓存管理器,原有的业务代码不变。

dcd7f21c-ed7d-11ed-90ce-dac502259ad0.png

执行下getUserById方法,查询用户编号为1的用户信息。

-从一级缓存查询key:1
-从二级缓存查询key:1
-==>Preparing:select*FROMusertwheret.id=?
-==>Parameters:1(Long)
-<== Total: 1
- 写入一级缓存 key:1
- 写入二级缓存 key:1

第二次执行相同的动作,从日志可用看到从优先会从本地内存中查询出结果。

-从一级缓存查询key:1

等待30s , 再执行一次,因为本地缓存会失效,所以执行的时候会查询二级缓存

-从一级缓存查询key:1
-从二级缓存查询key:1

一个简易的二级缓存就组装完了。

5 什么场景选择Spring Cache

在做技术选型的时候,需要针对场景选择不同的技术。

笔者认为Spring Cache的功能很强大,设计也非常优雅。特别适合缓存控制没有那么细致的场景。比如门户首页,偏静态展示页面,榜单等等。这些场景的特点是对数据实时性没有那么严格的要求,只需要将数据源缓存下来,过期之后自动刷新即可。这些场景下,Spring Cache就是神器,能大幅度提升研发效率。

但在高并发大数据量的场景下,精细的缓存颗粒度的控制上,还是需要做功能扩展。





审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • SQL
    SQL
    +关注

    关注

    1

    文章

    759

    浏览量

    44060
  • AOP
    AOP
    +关注

    关注

    0

    文章

    40

    浏览量

    11088
  • cache技术
    +关注

    关注

    0

    文章

    41

    浏览量

    1047
  • Redis
    +关注

    关注

    0

    文章

    371

    浏览量

    10843

原文标题:使用 Spring Cache 实现缓存,这种方式才叫优雅!

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    CPU Cache是如何保证缓存一致性的?

    我们介绍`CPU Cache`的组织架构及其进行**读操作**时的寻址方式,但是缓存不仅仅只有读操作,还有 **写操作** ,这会带来一个新的问题
    的头像 发表于 12-04 15:05 1323次阅读
    CPU <b class='flag-5'>Cache</b>是如何保证<b class='flag-5'>缓存</b>一致性的?

    java spring教程

    java spring教程理解Spring 实现原理掌握Spring IOC,AOP掌握Spring的基础配置和用法熟练使用SSH开发项目
    发表于 09-11 11:09

    阿里巴巴开源的通用缓存访问框架JetCache介绍

    摘要: JetCache是由阿里巴巴开源的通用缓存访问框架,如果你对Spring Cache很熟悉的话,请一定花一点时间了解一下JetCache,它更好用。JetCache可以做类似Sprin
    发表于 04-24 16:09

    高速缓存(cache)的工作原理是什么?高速缓存可分为哪几类

    存储器系统的层次架构是如何构成的?高速缓存(cache)的工作原理是什么?高速缓存可分为哪几类?
    发表于 12-23 06:18

    高速缓存Cache介绍

    被访问,那么将来它附近的位置也会被访问。比如顺序执行代码,或者使用一个数据结构• 时间局部性:被访问过一次的存储器位置,接下来会被多次引用。比如:循环• 缓存行(cache line)• 逻辑上的一组
    发表于 09-07 08:22

    什么是缓存Cache

    什么是缓存Cache 即高速缓冲存储器,是位于CPU与主内存间的一种容量较小但速度很高的存储器。由于CPU的速度远高于主内存,CPU直接
    发表于 01-23 10:57 886次阅读

    什么是Instructions Cache/IMM/ID

    什么是Instructions Cache/IMM/ID  Instructions Cache: (指令缓存)由于系统主内存的速度较慢,当CPU读取指令的时候,会导致CPU停下来
    发表于 02-04 11:51 626次阅读

    什么是Cache

    什么是Cache  英文缩写: Cache 中文译名: 高速缓存器 分  类: IP与多媒体 解  释: 信息在本地的临时存储
    发表于 02-22 17:26 1010次阅读

    高速缓存(Cache),高速缓存(Cache)原理是什么?

    高速缓存(Cache),高速缓存(Cache)原理是什么? 高速缓存Cache是位于CPU和主
    发表于 03-26 10:49 6818次阅读

    一级缓存、二级缓存、三级缓存区别是什么 详解它们的区分方法

    一级缓存(Level 1 Cache)简称L1 Cache,位于CPU内核的旁边,是与CPU结合最为紧密的CPU缓存,也是历史上最早出现的CPU缓存
    发表于 08-14 09:27 8w次阅读

    你知道linux的cache memory?

    当你读写文件的时候,Linux内核为了提高读写性能与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。即使你的程序运行结束后,Cache Memory
    发表于 04-26 15:49 1232次阅读

    Linux内核Page Cache和Buffer Cache两类缓存的作用及关系如何

    page)即为页缓存(page cache)。块缓存(buffer cache),则是内核为了加速对底层存储介质的访问速度,而构建的一层缓存
    的头像 发表于 07-02 14:25 2699次阅读
    Linux内核Page <b class='flag-5'>Cache</b>和Buffer <b class='flag-5'>Cache</b>两类<b class='flag-5'>缓存</b>的作用及关系如何

    关于Cache的其它内容

    关于Cache的其它内容 上面我们所描述情况,在访问cache前,已经将虚拟地址转换成了物理地址,其实,不一定,也可是是虚拟地址直接访问cache,倒底是使用物理地址还是虚拟地址,这就是翻译方式
    的头像 发表于 11-21 11:12 2344次阅读

    Spring Cache缓存常规配置

    作者最近在开发公司项目时使用到 Redis 缓存,并在翻看前人代码时,看到了一种关于 @Cacheable 注解的自定义缓存有效期的解决方案,感觉比较实用,因此作者自己拓展完善了一番后分享给各位。
    的头像 发表于 11-28 10:44 569次阅读
    <b class='flag-5'>Spring</b> <b class='flag-5'>Cache</b><b class='flag-5'>缓存</b>常规配置

    缓存之美——如何选择合适的本地缓存

    Guava cache是Google开发的Guava工具包中一套完善的JVM本地缓存框架,底层实现的数据结构类似于ConcurrentHashMap,但是进行了更多的能力拓展,包括缓存
    的头像 发表于 11-17 14:24 124次阅读
    <b class='flag-5'>缓存</b>之美——如何选择合适的本地<b class='flag-5'>缓存</b>?