ToB企服应用市场:ToB评测及商务社交产业平台

标题: Spring缓存是如何实现的?如何扩展使其支持过期删除功能? [打印本页]

作者: 我可以不吃啊    时间: 2023-8-30 17:33
标题: Spring缓存是如何实现的?如何扩展使其支持过期删除功能?
前言:在我们的应用中,有一些数据是通过rpc获取的远端数据,该数据不会经常变化,允许客户端在本地缓存一定时间。
该场景逻辑简单,缓存数据较小,不需要持久化,所以不希望引入其他第三方缓存工具加重应用负担,非常适合使用Spring Cache来实现。
但有个问题是,我们希望将这些rpc结果数据缓存起来,并在一定时间后自动删除,以实现在一定时间后获取到最新数据。类似Redis的过期时间。
接下来是我的调研步骤和开发过程。
Spring Cache 是什么?

Spring Cache 是 Spring 的一个缓存抽象层,作用是在方法调用时自动缓存返回结果,以提高系统性能和响应速度。
目标是简化缓存的使用,提供一致的缓存访问方式,使开发人员能够轻松快速地将缓存添加到应用程序中。
应用于方法级别,在下次调用相同参数的方法时,直接从缓存中获取结果,而不必执行实际的方法体。
适用场景?

包括但不限于:
优缺点

优点:
缺点:
重要组件

使用方式

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

Spring Boot默认使用ConcurrentMapCacheManager作为缓存管理器的实现,适用于简单的、单机的、对缓存容量要求较小的应用场景。
如何让ConcurrentMapCacheManager支持过期自动删除

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

为此,我设计了三种方案:
上述2、3方案都更贴近目标,且都有一个共同的难点,即如何判断该缓存是否过期?或如何存放缓存的过期时间?
既然没有好办法,那就走一波源码找找思路吧!
源码解析

ConcurrentMapCacheManager 中定义了一个cacheMap(如下代码),用于存储所有缓存名及对应缓存对象。
  1. private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
复制代码
cacheMap 中的存放的Cache的具体类型为ConcurrentMapCache,
而ConcurrentMapCache的内部定义了一个store(如下代码),用于存储该缓存下所有key、value,即真正的缓存数据。
  1. private final ConcurrentMap<Object, Object> store;
复制代码
其关系图为:

以下为测试代码,为一个查询增加缓存操作:cacheName=getUsersByName,key为参数name的值,value为查询用户集合。
  1. @Service
  2. public class UserServiceImpl implements UserService {
  3.     @Autowired
  4.     private UserMapper userMapper;
  5.     @Override
  6.     @Cacheable(value = "getUsersByName", key = "#name")
  7.     public List<GyhUser> getUsersByName(String name) {
  8.         return userMapper.getUsersByName(name);
  9.     }
  10. }
复制代码
当程序调用到此方法前,会自动进入缓存拦截器CacheInterceptor,进而进入ConcurrentMapCacheManager的getCache方法,获取对应的缓存实例,若不存在,则生成一个。

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

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

实现自动过期删除

根据上面的代码跟踪可以发现,缓存数据key/value存放在具体的缓存实例ConcurrentMapCache的store中,且get和put前后,有我可以操作的空间。
  1. /**
  2. * 缓存数据包装类,保证缓存数据及插入时间
  3. */
  4. public class ExpireCacheWrap {
  5.     /**
  6.      * 缓存数据
  7.      */
  8.     private final Object value;
  9.     /**
  10.      * 插入时间
  11.      */
  12.     private final Long insertTime;
  13.     public ExpireCacheWrap(Object value, Long insertTime) {
  14.         this.value = value;
  15.         this.insertTime = insertTime;
  16.     }
  17.     public Object getValue() {
  18.         return value;
  19.     }
  20.     public Long getInsertTime() {
  21.         return this.insertTime;
  22.     }
  23. }
复制代码
  1. /**
  2. * 缓存过期删除
  3. */
  4. public class ExpireCache extends ConcurrentMapCache {
  5.     public ExpireCache(String name) {
  6.         super(name);
  7.     }
  8.     @Override
  9.     public ValueWrapper get(Object key) {
  10.         // 解析缓存对象时,拿到value,去掉插入时间。对于业务中缓存的使用逻辑无感知无侵入,无需调整相关代码
  11.         ValueWrapper valueWrapper = super.get(key);
  12.         if (valueWrapper == null) {
  13.             return null;
  14.         }
  15.         Object storeValue = valueWrapper.get();
  16.         storeValue = storeValue != null ? ((ExpireCacheWrap) storeValue).getValue() : null;
  17.         return super.toValueWrapper(storeValue);
  18.     }
  19.     @Override
  20.     public void put(Object key, @Nullable Object value) {
  21.         // 插入缓存对象时,封装对象信息:缓存内容+插入时间
  22.         value = new ExpireCacheWrap(value, System.currentTimeMillis());
  23.         super.put(key, value);
  24.     }
  25. }
复制代码
  1. /**
  2. * 缓存管理器
  3. */
  4. public class ExpireCacheManager extends ConcurrentMapCacheManager {
  5.     @Override
  6.     protected Cache createConcurrentMapCache(String name) {
  7.         return new ExpireCache(name);
  8.     }
  9. }
复制代码
  1. @Configuration
  2. class ExpireCacheConfiguration {
  3.     @Bean
  4.     public ExpireCacheManager cacheManager() {
  5.         ExpireCacheManager cacheManager = new ExpireCacheManager();
  6.         return cacheManager;
  7.     }
  8. }
复制代码
  1. /**
  2. * 定时执行删除过期缓存
  3. */
  4. @Component
  5. @Slf4j
  6. public class ExpireCacheEvictJob {
  7.     @Autowired
  8.     private ExpireCacheManager cacheManager;
  9.     /**
  10.      * 缓存名与缓存时间
  11.      */
  12.     private static Map<String, Long> cacheNameExpireMap;
  13.     // 可以优化到配置文件或字典中
  14.     static {
  15.         cacheNameExpireMap = new HashMap<>(5);
  16.         cacheNameExpireMap.put("getUserById", 180000L);
  17.         cacheNameExpireMap.put("getUsersByName", 300000L);
  18.     }
  19.     /**
  20.      * 5分钟执行一次
  21.      */
  22.     @Scheduled(fixedRate = 300000)
  23.     public void cacheEvict() {
  24.         Long now = System.currentTimeMillis();
  25.         // 获取所有缓存
  26.         Collection<String> cacheNames = cacheManager.getCacheNames();
  27.         for (String cacheName : cacheNames) {
  28.             // 该类缓存设置的过期时间
  29.             Long expire = cacheNameExpireMap.get(cacheName);
  30.             // 获取该缓存的缓存内容集合
  31.             Cache cache = cacheManager.getCache(cacheName);
  32.             ConcurrentMap<Object, Object> store = (ConcurrentMap) cache.getNativeCache();
  33.             Set<Object> keySet = store.keySet();
  34.             // 循环获取缓存键值对,根据value中存储的插入时间,判断key是否已过期,过期则删除
  35.             keySet.stream().forEach(key -> {
  36.                 // 缓存内容包装对象
  37.                 ExpireCacheWrap value = (ExpireCacheWrap) store.get(key);
  38.                 // 缓存内容插入时间
  39.                 Long insertTime = value.getInsertTime();
  40.                 if ((insertTime + expire) < now) {
  41.                     cache.evict(key);
  42.                     log.info("key={},insertTime={},expire={},过期删除", key, insertTime, expire);
  43.                 }
  44.             });
  45.         }
  46.     }
  47. }
复制代码
通过以上操作,实现了让ConcurrentMapCacheManager支持过期自动删除,并且对开发者
基本无感知无侵入,只需要在配置文件中配置缓存时间即可。
但是如果我的项目已经支持了第三方缓存如Redis,秉着不用白不用的原则,又该如何将该功能嫁接到Redis上呢?
正正好我们的项目最近在引入R2m,就试着搞一下吧-。
未完待续~  Thanks~
作者:京东科技 郭艳红
来源:京东云开发者社区

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4