关于后端:Spring缓存是如何实现的如何扩展使其支持过期删除功能-京东云技术团队

41次阅读

共计 5659 个字符,预计需要花费 15 分钟才能阅读完成。

前言:在咱们的利用中,有一些数据是通过 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~

作者:京东科技 郭艳红

起源:京东云开发者社区

正文完
 0