前言:在我们的应用中,有一些数据是通过rpc获取的远端数据,该数据不会经常变化,允许客户端在本地缓存一定时间。
该场景逻辑简单,缓存数据较小,不需要持久化,所以不希望引入其他第三方缓存工具加重应用负担,非常适合使用Spring Cache来实现。
但有个问题是,我们希望将这些rpc结果数据缓存起来,并在一定时间后自动删除,以实现在一定时间后获取到最新数据。类似Redis的过期时间。
接下来是我的调研步骤和开发过程。
Spring Cache 是什么?
Spring Cache 是 Spring 的一个缓存抽象层,作用是在方法调用时自动缓存返回结果,以提高系统性能和响应速度。
目标是简化缓存的使用,提供一致的缓存访问方式,使开发人员能够轻松快速地将缓存添加到应用程序中。
应用于方法级别,在下次调用相同参数的方法时,直接从缓存中获取结果,而不必执行实际的方法体。
适用场景?
包括但不限于:
- 频繁访问的方法调用,可以通过缓存结果来提高性能
- 数据库查询结果,可以缓存查询结果以减少数据库访问
- 外部服务调用结果,可以缓存外部服务的响应结果以减少网络开销
- 计算结果,可以缓存计算结果以加快后续计算速度
优缺点
优点:
- 提高应用的性能,避免重复计算或查询。
- 减少对底层资源的访问,如数据库或远程服务,从而减轻负载。
- 简化代码,通过注解的方式实现缓存逻辑,而不需要手动编写缓存代码。
缺点:
- 需要占用一定的内存空间来存储缓存数据。
- 可能导致数据不一致问题,如果缓存的数据发生变化,但缓存没有及时更新,可能会导致脏数据的问题。(所以需要及时更新缓存)
- 可能引发缓存穿透问题,当大量请求同时访问一个不存在于缓存中的键时,会导致请求直接落到底层资源,增加负载。
重要组件
-
CacheManager:缓存管理器,用于创建、配置和管理缓存对象。可以配置具体的缓存实现,如 Ehcache、Redis。
-
Cache:缓存对象,用于存储缓存数据,提供了读取、写入和删除缓存数据的方法。
-
常用注解:
- @Cacheable:被调用时,会检查缓存中是否已存在,若有,则直接返回缓存结果,否则执行方法并将结果存入缓存,适用于只读操作。
- @CachePut:则每次都会执行方法体,并将结果存入缓存,即每次都会更新缓存中的数据,适用于写操作。
- @CacheEvict:被调用时,Spring Cache 会清除对应的缓存数据。
使用方式
- 配置缓存管理器(CacheManager):使用
@EnableCaching
注解启用缓存功能,并配置具体的缓存实现。 - 在方法上添加缓存注解:使用
@Cacheable
、@CacheEvict
、@CachePut
等注解标记需要被缓存的方法。 - 调用被缓存的方法:当调用被标记为缓存的方法时,Spring Cache 会检查缓存中是否已有该方法的缓存结果。
- 根据缓存结果返回数据:如果缓存中已有结果,则直接从缓存中返回;否则,执行方法并将结果存入缓存。
- 根据需要清除或更新缓存:使用
@CacheEvict
、@CachePut
注解可以在方法调用后清除或更新缓存。
通过以上步骤,Spring Cache 可以自动管理缓存的读写操作,从而简化缓存的使用和管理。
Spring Boot默认使用哪种实现,及其优缺点:
Spring Boot默认使用ConcurrentMapCacheManager
作为缓存管理器的实现,适用于简单的、单机的、对缓存容量要求较小的应用场景。
-
优点:
- 简单轻量:没有外部依赖,适用于简单的应用场景。
- 内存存储:缓存数据存储在内存中的
ConcurrentMap
中,读写速度快,适用于快速访问和频繁更新的数据。 - 多缓存实例支持:支持配置多个命名缓存实例,每个实例使用独立的
ConcurrentMap
存储数据,可以根据不同的需求配置多个缓存实例。
-
缺点:
- 单机应用限制:
ConcurrentMapCacheManager
适用于单机应用,缓存数据存储在应用的内存中,无法实现分布式缓存。 - 有限的容量:由于缓存数据存储在内存中,
ConcurrentMapCacheManager
的容量受限于应用的内存大小,对于大规模数据或高并发访问的场景可能存在容量不足的问题。 - 缺乏持久化支持:
ConcurrentMapCacheManager
不支持将缓存数据持久化到磁盘或其他外部存储介质,应用重启后缓存数据会丢失。
- 单机应用限制:
如何让ConcurrentMapCacheManager
支持过期自动删除
前言也提到了,我们的场景逻辑简单,缓存数据较小,不需要持久化,不希望引入其他第三方缓存工具加重应用负担,适合使用ConcurrentMapCacheManager
。所以扩展下ConcurrentMapCacheManager
也许是最简单的实现。
方案设计
为此,我设计了三种方案:
- 开启定时任务,扫描缓存,定时删除所有缓存;该方式简单粗暴,统一定时删除,但不能针对单条数据进行过期操作。
- 开启定时任务,扫描缓存,并将单条过期的缓存数据删除。
- 访问缓存数据之前,判断是否过期,若过期则重新执行方法体,并将结果覆盖原缓存数据。
上述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
,进而进入ConcurrentMapCacheManager
的getCache
方法,获取对应的缓存实例,