专栏首页Coder小黑Spring Cache 缺陷,我好像有解决方案了

Spring Cache 缺陷,我好像有解决方案了

Spring Cache 缺陷

Spring Cache 是一个非常优秀的缓存组件。

但是在使用 Spring Cache 的过程当中,小黑同学也遇到了一些痛点。

比如,现在有一个需求:通过多个 userId 来批量获取用户信息。

方案 1

此时,我们的代码可能是这样:

List<User> users = ids.stream().map(id -> {
    return getUserById(id);
})
.collect(Collectors.toList());

@Cacheable(key = "#p0", unless = "#result == null")
public User getUserById(Long id) {
 // ···
}

这种写法的缺点在于:

在 for 循环中操作 redis。如果数据命中缓存还好,一旦缓存没有命中,则会访问数据库。

方案 2

也有的同学可能会这样做:

@Cacheable(key = "#ids.hash")
public Collection<User> getUsersByIds(Collection<Long> ids) {
 // ···
}

这种做法的问题是:

缓存是基于 id 列表的 hashcode ,只有在 id 列表的 hashcode 值相等的情况下,缓存才会命中。而且,一旦列表中的其中一个数据被修改,整个列表缓存都要被清除。

例如: 第一次请求 id 列表是 1,2,3, 第二次请求的 id 列表为 1,2,4 在这种情况下,前后两次的缓存不能共享。 如果 id 为 1 的数据发生了改变,那么,这两次请求的缓存都要被清空

看看 Spring 官方是怎么说的

Spring Issue:

https://github.com/spring-projects/spring-framework/issues/24139 https://github.com/spring-projects/spring-framework/issues/23221

image.png

简单翻译一下,具体内容读者可以自行查阅相关 issue。

译文: 谢谢你的报告。缓存抽象没有这种状态的概念,如果你返回一个集合,那就是你要求在缓存中存储的东西。也没有什么强迫您为给定的缓存保留相同的项类型,所以这种假设并不适合这样的高级抽象。

我的理解是,对于 Spring Cache 这种高级抽象框架来说,Cache 是基于方法的,如果方法返回 Collection,那整个 Collection 就是需要被缓存的内容。

我的解决方案

纠结了好久,小黑同学还是决定自己来造个轮子。

那我想要达到什么样的效果呢?

我希望对于这种根据多个 key 批量获取缓存的操作,可以先根据单个 key 从缓存中查找,如果缓存中不存在,就去加载数据,同时再将数据放到缓存中。

简单介绍一下整体的思路:

  • 核心接口
    • com.github.shenjianeng.easycache.core.Cache
    • com.github.shenjianeng.easycache.core.MultiCacheLoader

Cache 接口

Cache 接口定义了一些通用的缓存操作。和大部分 Cache 框架不同是,这里支持根据 key 批量获取缓存。

/**
 * 根据 keys 缓存中获取,缓存中不存在,则返回null
 */
@NonNull
Map<K, V> getIfPresent(@NonNull Iterable<K> keys);


/**
 * 根据 keys 从缓存中获取,如果缓存中不存在,调用 {@link MultiCacheLoader#loadCache(java.util.Collection)} 加载数据,并添加到缓存中
 */
@NonNull
Map<K, V> getOrLoadIfAbsent(@NonNull Iterable<K> keys);

MultiCacheLoader 接口

@FunctionalInterface
public interface MultiCacheLoader<K, V> {

    @NonNull
    Map<K, V> loadCache(@NonNull Collection<K> keys);

    default V loadCache(K key) {
        Map<K, V> map = loadCache(Collections.singleton(key));
        if (CollectionUtils.isEmpty(map)) {
            return null;
        }
        return map.get(key);
    }
}

MultiCacheLoader 是一个函数式接口。在调用 Cache#getOrLoadIfAbsent 方法时,如果缓存不存在,就会通过 MultiCacheLoader 来加载数据,然后加数据放到缓存中。

RedisCache

RedisCache 是现在 Cache 接口的唯一实现。正如其类名一样,这是基于 redis 的缓存实现。

先说一下大致的实现思路:

  1. 使用 redis 的 mget 命令,批量获取缓存。为了保证效率,每次最多批量获取 20 个。
  2. 如果有数据不在缓存中,则判断是否需要自动加载数据,如果需要则通过 MultiCacheLoader 加载数据
  3. 将数据存放到缓存中。同时通过维护一个 zset 来保存已知的 cache key,用于清除缓存使用。

废话不多说,直接上源码。

private Map<K, V> doGetOrLoadIfAbsent(Iterable<K> keys, boolean loadIfAbsent) {
    List<String> cacheKeyList = buildCacheKey(keys);
    List<List<String>> partitions = Lists.partition(cacheKeyList, MAX_BATCH_KEY_SIZE);

    List<V> valueList = Lists.newArrayListWithExpectedSize(cacheKeyList.size());

    for (List<String> partition : partitions) {
        // Get multiple keys. Values are returned in the order of the requested keys.
        List<V> values = (List<V>) redisTemplate.opsForValue().multiGet(partition);
        valueList.addAll(values);
    }

    List<K> keysList = Lists.newArrayList(keys);
    List<K> missedKeyList = Lists.newArrayList();

    Map<K, V> map = Maps.newHashMapWithExpectedSize(partitions.size());


    for (int i = 0; i < valueList.size(); i++) {
        V v = valueList.get(i);
        K k = keysList.get(i);
        if (v != null) {
            map.put(k, v);
        } else {
            missedKeyList.add(k);
        }
    }

    if (loadIfAbsent) {
        Map<K, V> missValueMap = multiCacheLoader.loadCache(missedKeyList);

        put(missValueMap);

        map.putAll(missValueMap);
    }

    return map;
}

缓存清除方法实现:

public void evictAll() {
    Set<Serializable> serializables = redisTemplate.opsForZSet().rangeByScore(knownKeysName, 0, 0);

    if (!CollectionUtils.isEmpty(serializables)) {
        List<String> cacheKeys = Lists.newArrayListWithExpectedSize(serializables.size());
        serializables.forEach(serializable -> {
            if (serializable instanceof String) {
                cacheKeys.add((String) serializable);
            }
        });
        redisTemplate.delete(cacheKeys);
        redisTemplate.opsForZSet().remove(knownKeysName, cacheKeys);
    }
}

本文分享自微信公众号 - Coder小黑(gh_5c7abf11d864),作者:coder小黑

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Full @Configuration vs lite @Bean mode

    Full @Configuration和lite @Bean mode 是 Spring Java Config 中两个非常有意思的概念。

    Coder小黑
  • Spring5 源码学习 (4) refresh 方法

    接上文 Spring5源码分析(三)refresh方法 中已经讲到了refresh()中的postProcessBeanFactory(beanFactory)...

    Coder小黑
  • 你真的会用Spring吗?如何在单例Bean中注入原型Bean

    假设单例 BeanA 需要使用原型 BeanB(BeanB 可能是 BeanA 的一个属性值)。可是容器仅创建一次单例 BeanA,因此只有一次机会来设置属性 ...

    Coder小黑
  • 伪共享(false sharing),并发编程无声的性能杀手

    在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素。前段时间学习了一个牛X...

    周三不加班
  • MESI协议:Cache 一致性协议

    把所有的读写请求都通过总线(Bus)广播给所有的 CPU 核心,然后让各个核心去“嗅探”这些请求,再根据本地的情况进行响应。

    斯武丶风晴
  • 缓存穿透、并发和雪崩那些事 转

    作者:李艳鹏,阿里资深技术专家!著有《可伸缩服务架构》,《分布式服务架构》等作品,在区块链,聚合支付,电商等领域有一定的积累!

    wuweixiang
  • 浏览器缓存机制

    解释:在用户浏览网络的时候,同一个域名下的网页大部分是有很多共同文件的,比如第三方js文件,css文件,所以我们不可能当用户每浏览一个网页或者刷新同一个网页时,...

    IT人一直在路上
  • 使用Redis和Java进行数据库缓存

    您在数据库中获得的信息越多,随着时间的推移它就越慢。即使是为支持许多并发请求而精心设计的数据库管理系统也将最终达到极限。

    银河1号
  • 分布式系统关注点——先写DB还是「缓存」?

    在前一篇《360°全方位解读「缓存」》中,我们聊了运用缓存的三种思路,以及在一个完整的系统中可以设立缓存的几个位置,并且分享了关于浏览器缓存、CDN缓存、网关(...

    Zachary_ZF
  • 缓存穿透、缓存击穿、缓存雪崩看这篇就够了,文末还送福利哦!

    当我们进行架构设计时,缓存是提高高性能的最重要也是最常用的组件之一。数据库的瓶颈在于磁盘I/O,虽然现如今关系数据库的部分应用场景采用了NoSQL作为替代,但依...

    码农神说

扫码关注云+社区

领取腾讯云代金券