前言:在我们的应用中,有一些数据是通过rpc获取的远端数据,该数据不会经常变化,允许客户端在本地缓存一定时间。

该场景逻辑简单,缓存数据较小,不需要持久化,所以不希望引入其他第三方缓存工具加重应用负担,非常适合使用Spring Cache来实现。

但有个问题是,我们希望将这些rpc结果数据缓存起来,并在一定时间后自动删除,以实现在一定时间后获取到最新数据。类似Redis的过期时间。

接下来是我的调研步骤和开发过程。

Spring Cache 是什么?

Spring Cache 是 Spring 的一个缓存抽象层,作用是在方法调用时自动缓存返回结果,以提高系统性能和响应速度。

目标是简化缓存的使用,提供一致的缓存访问方式,使开发人员能够轻松快速地将缓存添加到应用程序中。

应用于方法级别,在下次调用相同参数的方法时,直接从缓存中获取结果,而不必执行实际的方法体。

适用场景?

包括但不限于:

  • 频繁访问的方法调用,可以通过缓存结果来提高性能
  • 数据库查询结果,可以缓存查询结果以减少数据库访问
  • 外部服务调用结果,可以缓存外部服务的响应结果以减少网络开销
  • 计算结果,可以缓存计算结果以加快后续计算速度

优缺点

优点:

  • 提高应用的性能,避免重复计算或查询。
  • 减少对底层资源的访问,如数据库或远程服务,从而减轻负载。
  • 简化代码,通过注解的方式实现缓存逻辑,而不需要手动编写缓存代码。

缺点:

  • 需要占用一定的内存空间来存储缓存数据。
  • 可能导致数据不一致问题,如果缓存的数据发生变化,但缓存没有及时更新,可能会导致脏数据的问题。(所以需要及时更新缓存)
  • 可能引发缓存穿透问题,当大量请求同时访问一个不存在于缓存中的键时,会导致请求直接落到底层资源,增加负载。

重要组件

  1. CacheManager:缓存管理器,用于创建、配置和管理缓存对象。可以配置具体的缓存实现,如 Ehcache、Redis。

  2. Cache:缓存对象,用于存储缓存数据,提供了读取、写入和删除缓存数据的方法。

  3. 常用注解:

    • @Cacheable:被调用时,会检查缓存中是否已存在,若有,则直接返回缓存结果,否则执行方法并将结果存入缓存,适用于只读操作。
    • @CachePut:则每次都会执行方法体,并将结果存入缓存,即每次都会更新缓存中的数据,适用于写操作。
    • @CacheEvict:被调用时,Spring Cache 会清除对应的缓存数据。

使用方式

  1. 配置缓存管理器(CacheManager):使用 @EnableCaching 注解启用缓存功能,并配置具体的缓存实现。
  2. 在方法上添加缓存注解:使用 @Cacheable@CacheEvict@CachePut 等注解标记需要被缓存的方法。
  3. 调用被缓存的方法:当调用被标记为缓存的方法时,Spring Cache 会检查缓存中是否已有该方法的缓存结果。
  4. 根据缓存结果返回数据:如果缓存中已有结果,则直接从缓存中返回;否则,执行方法并将结果存入缓存。
  5. 根据需要清除或更新缓存:使用 @CacheEvict@CachePut 注解可以在方法调用后清除或更新缓存。

    通过以上步骤,Spring Cache 可以自动管理缓存的读写操作,从而简化缓存的使用和管理。

Spring Boot默认使用哪种实现,及其优缺点:

Spring Boot默认使用ConcurrentMapCacheManager作为缓存管理器的实现,适用于简单的、单机的、对缓存容量要求较小的应用场景。

  • 优点:

    1. 简单轻量:没有外部依赖,适用于简单的应用场景。
    2. 内存存储:缓存数据存储在内存中的ConcurrentMap中,读写速度快,适用于快速访问和频繁更新的数据。
    3. 多缓存实例支持:支持配置多个命名缓存实例,每个实例使用独立的ConcurrentMap存储数据,可以根据不同的需求配置多个缓存实例。
  • 缺点:

    1. 单机应用限制:ConcurrentMapCacheManager适用于单机应用,缓存数据存储在应用的内存中,无法实现分布式缓存。
    2. 有限的容量:由于缓存数据存储在内存中,ConcurrentMapCacheManager的容量受限于应用的内存大小,对于大规模数据或高并发访问的场景可能存在容量不足的问题。
    3. 缺乏持久化支持:ConcurrentMapCacheManager不支持将缓存数据持久化到磁盘或其他外部存储介质,应用重启后缓存数据会丢失。

如何让ConcurrentMapCacheManager支持过期自动删除

前言也提到了,我们的场景逻辑简单,缓存数据较小,不需要持久化,不希望引入其他第三方缓存工具加重应用负担,适合使用ConcurrentMapCacheManager。所以扩展下ConcurrentMapCacheManager也许是最简单的实现。

方案设计

为此,我设计了三种方案:

  1. 开启定时任务,扫描缓存,定时删除所有缓存;该方式简单粗暴,统一定时删除,但不能针对单条数据进行过期操作。
  2. 开启定时任务,扫描缓存,并将单条过期的缓存数据删除。
  3. 访问缓存数据之前,判断是否过期,若过期则重新执行方法体,并将结果覆盖原缓存数据。

上述2、3方案都更贴近目标,且都有一个共同的难点,即如何判断该缓存是否过期?或如何存放缓存的过期时间?

既然没有好办法,那就走一波源码找找思路吧!

源码解析

ConcurrentMapCacheManager 中定义了一个cacheMap(如下代码),用于存储所有缓存名及对应缓存对象。

private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);

cacheMap 中的存放的Cache的具体类型为ConcurrentMapCache

ConcurrentMapCache的内部定义了一个store(如下代码),用于存储该缓存下所有key、value,即真正的缓存数据。

private final ConcurrentMap<Object, Object> store;

其关系图为:

以下为测试代码,为一个查询增加缓存操作:cacheName=getUsersByName,key为参数name的值,value为查询用户集合。

@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper; @Override
@Cacheable(value = "getUsersByName", key = "#name")
public List<GyhUser> getUsersByName(String name) {
return userMapper.getUsersByName(name);
}
}

当程序调用到此方法前,会自动进入缓存拦截器CacheInterceptor,进而进入ConcurrentMapCacheManagergetCache方法,获取对应的缓存实例,若不存在,则生成一个。



然后从缓存实例中查找缓存数据,找到则返回,找不到则执行目标方法。

执行完目标方法后,将返回结果放到缓存中。

实现自动过期删除

根据上面的代码跟踪可以发现,缓存数据key/value存放在具体的缓存实例ConcurrentMapCachestore中,且get和put前后,有我可以操作的空间。

  1. 那么,如果我将value重新包装一下,将缓存时间封装进去,并在get和put前后,将真正的缓存数据解析出来,供开发者使用,是否可以实现呢?说干就干!
/**
* 缓存数据包装类,保证缓存数据及插入时间
*/
public class ExpireCacheWrap {
/**
* 缓存数据
*/
private final Object value;
/**
* 插入时间
*/
private final Long insertTime; public ExpireCacheWrap(Object value, Long insertTime) {
this.value = value;
this.insertTime = insertTime;
} public Object getValue() {
return value;
} public Long getInsertTime() {
return this.insertTime;
}
}
  1. 自定义一个Cache类,继承ConcurrentMapCache,扩展get、put方法,实现对缓存时间的记录和解析
/**
* 缓存过期删除
*/
public class ExpireCache extends ConcurrentMapCache {
public ExpireCache(String name) {
super(name);
} @Override
public ValueWrapper get(Object key) {
// 解析缓存对象时,拿到value,去掉插入时间。对于业务中缓存的使用逻辑无感知无侵入,无需调整相关代码
ValueWrapper valueWrapper = super.get(key);
if (valueWrapper == null) {
return null;
}
Object storeValue = valueWrapper.get();
storeValue = storeValue != null ? ((ExpireCacheWrap) storeValue).getValue() : null;
return super.toValueWrapper(storeValue);
} @Override
public void put(Object key, @Nullable Object value) {
// 插入缓存对象时,封装对象信息:缓存内容+插入时间
value = new ExpireCacheWrap(value, System.currentTimeMillis());
super.put(key, value);
}
}
  1. 自定义缓存管理器,将自定义的ExpireCache,替换默认的ConcurrentMapCache
/**
* 缓存管理器
*/
public class ExpireCacheManager extends ConcurrentMapCacheManager {
@Override
protected Cache createConcurrentMapCache(String name) {
return new ExpireCache(name);
}
}
  1. 将自定义的缓存管理器ExpireCacheManager注入到容器中
@Configuration
class ExpireCacheConfiguration {
@Bean
public ExpireCacheManager cacheManager() {
ExpireCacheManager cacheManager = new ExpireCacheManager();
return cacheManager;
}
}
  1. 开启定时任务,自动删除过期缓存
/**
* 定时执行删除过期缓存
*/
@Component
@Slf4j
public class ExpireCacheEvictJob { @Autowired
private ExpireCacheManager cacheManager;
/**
* 缓存名与缓存时间
*/
private static Map<String, Long> cacheNameExpireMap;
// 可以优化到配置文件或字典中
static {
cacheNameExpireMap = new HashMap<>(5);
cacheNameExpireMap.put("getUserById", 180000L);
cacheNameExpireMap.put("getUsersByName", 300000L);
} /**
* 5分钟执行一次
*/
@Scheduled(fixedRate = 300000)
public void cacheEvict() {
Long now = System.currentTimeMillis();
// 获取所有缓存
Collection<String> cacheNames = cacheManager.getCacheNames();
for (String cacheName : cacheNames) {
// 该类缓存设置的过期时间
Long expire = cacheNameExpireMap.get(cacheName);
// 获取该缓存的缓存内容集合
Cache cache = cacheManager.getCache(cacheName);
ConcurrentMap<Object, Object> store = (ConcurrentMap) cache.getNativeCache();
Set<Object> keySet = store.keySet();
// 循环获取缓存键值对,根据value中存储的插入时间,判断key是否已过期,过期则删除
keySet.stream().forEach(key -> {
// 缓存内容包装对象
ExpireCacheWrap value = (ExpireCacheWrap) store.get(key);
// 缓存内容插入时间
Long insertTime = value.getInsertTime();
if ((insertTime + expire) < now) {
cache.evict(key);
log.info("key={},insertTime={},expire={},过期删除", key, insertTime, expire);
}
});
} }
}

通过以上操作,实现了让ConcurrentMapCacheManager支持过期自动删除,并且对开发者

基本无感知无侵入,只需要在配置文件中配置缓存时间即可。

但是如果我的项目已经支持了第三方缓存如Redis,秉着不用白不用的原则,又该如何将该功能嫁接到Redis上呢?

正正好我们的项目最近在引入R2m,就试着搞一下吧-

未完待续~ Thanks~

作者:京东科技 郭艳红

来源:京东云开发者社区

Spring缓存是如何实现的?如何扩展使其支持过期删除功能?的更多相关文章

  1. 扩展 StackExchange.Redis 支持实体

    一.StackExchange.Redis StackExchange.Redis是由Stack Overflow开发的C#语言Redis客户端,使用广泛,本文针对 StackExchange.Red ...

  2. spring源码学习之容器的扩展(二)

    六 BeanFactory的后处理BeanFactory作为spring容器的基础,用于存放所有已经加载的bean,为了保证程序上的高扩展性,spring针对BeanFactory做了大量的扩展,比如 ...

  3. Spring缓存机制的理解

    在spring缓存机制中,包括了两个方面的缓存操作:1.缓存某个方法返回的结果:2.在某个方法执行前或后清空缓存. 下面写两个类来模拟Spring的缓存机制: package com.sin90lzc ...

  4. 【Java EE 学习 78 上】【数据采集系统第十天】【Service使用Spring缓存模块】

    一.需求分析 调查问卷中或许每一个单击动作都会引发大量的数据库访问,特别是在参与调查的过程中,只是单击“上一页”或者“下一页”的按钮就会引发大量的查询,必须对这种问题进行优化才行.使用缓存策略进行查询 ...

  5. Spring缓存框架原理浅谈

    运维在上线,无聊写博客.最近看了下Spring的缓存框架,这里写一下 1.Spring 缓存框架 原理浅谈 2.Spring 缓存框架 注解使用说明 3.Spring 缓存配置 + Ehcache(默 ...

  6. Spring4.1新特性——Spring缓存框架增强(转)

    目录 Spring4.1新特性——综述 Spring4.1新特性——Spring核心部分及其他 Spring4.1新特性——Spring缓存框架增强 Spring4.1新特性——异步调用和事件机制的异 ...

  7. 使用Spring缓存的简单Demo

    使用Spring缓存的简单Demo 1. 首先创建Maven工程,在Pom中配置 <dependency> <groupId>org.springframework</g ...

  8. spring缓存

    Spring Cache使用详解   复制过来时的地址:http://blog.csdn.net/xiaoyu411502/article/details/48901555 标签: spring-bo ...

  9. spring缓存Ehcache(入门2)源码解读

    Ehcache缓存: 解读: Ehcache缓存是在继承spring缓存核心类CacheManager的基础上实现的. 常用类: EhCacheCacheManager:继承自CacheManager ...

  10. Spring缓存注解@Cacheable、@CacheEvict、@CachePut使用(转)

    原文地址:https://www.cnblogs.com/fashflying/p/6908028.html 从3.1开始,Spring引入了对Cache的支持.其使用方法和原理都类似于Spring对 ...

随机推荐

  1. 解读 SSDB、LevelDB 和 RocksDB 到 GaussDB(for Redis) 的迁移

    摘要:本期将详细介绍 SSDB.LevelDB 和 RocksDB 到 GaussDB(for Redis)的迁移. 本文分享自华为云社区<华为云PB级数据库GaussDB(for Redis) ...

  2. 关于单元测试的那些事儿,Mockito 都能帮你解决

    摘要:相信每一个程序猿在写Unit Test的时候都会碰到一些令人头疼的问题:如何测试一个rest接口:如何测试一个包含客户端调用服务端的复杂方法:如何测试一个包含从数据库读取数据的复杂方法...这些 ...

  3. 通过windows自带管理工具、系统命令行、快捷键等快速操作

    windows自带管理工具 我们win+R 输入一些命令,可以快速打开一些界面,比如: sysdm.cpl win10.win11 我电脑,属性与之前win7不同了,我希望打开之前的属性打不开了 通过 ...

  4. 1g云主机升级centos8不满足centos 8 至少2g内存要求,linux虚拟内存来凑

    centos8 官方说,至少2g内存,推荐4g内存,像我的个人博客,zhoulujun.cn ,这种个人博客有不赚钱,丢个5美金一个月的1g内存,1核cpu,就够了. 强制升级到centos8,ngi ...

  5. 提升源代码安全性的C#和Java深度混淆工具——IpaGuard

    提升源代码安全性的C#和Java深度混淆工具--IpaGuard 摘要 Ipa Guard是一款功能强大的IPA混淆工具,通过对iOS IPA文件进行混淆加密,保护其代码.资源和配置文件,降低破解反编 ...

  6. IAST 初探:博采众长、精准定位、DevOps友好

    之前的文章中,我们了解了 SAST 和 DAST,本文将介绍将两者优势相结合的安全测试技术--IAST. ✦ ✦ 交互式应用安全测试(IAST)是一个自动识别和诊断应用程序和 API 漏洞的技术,它结 ...

  7. 【JAVA基础】Swagger使用

    Swagger使用 刷新权限 自定标签名称

  8. Java Socket Demo

    服务端: package com.sux.demo; import java.io.*; import java.net.ServerSocket; import java.net.Socket; i ...

  9. AtCoder Beginner Contest 172 (C题前缀和 + 二分,D题筛因子,E题容斥定理)

    AB水题, C - Tsundoku 题目描述 有两摞书,一摞有 $n$ 本,从上至下每本需阅读 $a_i$ 分钟,一摞有 $m$ 本,从上至下每本需阅读 $b_i$ 分钟,问最多能在 $k$ 分钟内 ...

  10. 《3D编程模式》写书-第6次记录

    大家好,这段时间我完成了对初稿的第二轮修改,已经把稿子提交给编辑了 这里是所有的的写书记录: <3D编程模式>写书记录 本轮修改主要进行了下面的修改: 修改UML描述 增加依赖关系 角色之 ...