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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介:

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

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

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

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

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

List 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 getUsersByIds(Collection 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

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

译文:

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

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

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

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

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

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

https://github.com/shenjianeng/easy-cache

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

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

/**

  • 根据 keys 缓存中获取,缓存中不存在,则返回null
    */

@NonNull
Map getIfPresent(@NonNull Iterable keys);

/**

  • 根据 keys 从缓存中获取,如果缓存中不存在,调用 {@link MultiCacheLoader#loadCache(java.util.Collection)} 加载数据,并添加到缓存中
    */

@NonNull
Map getOrLoadIfAbsent(@NonNull Iterable keys);
MultiCacheLoader 接口
@FunctionalInterface
public interface MultiCacheLoader {

@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 的缓存实现。

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

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

private Map doGetOrLoadIfAbsent(Iterable 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);
}

}
再多说几句
更多源码细节,如果读者感兴趣,可以自行阅读源码:easy-cache

欢迎大家 fork 体验,或者评论区留言探讨,写的不好,请多多指教~~

未来计划:

支持缓存 null 值
支持 annotation 的声明式缓存
原文地址https://www.cnblogs.com/coderxiaohei/p/12636799.html

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
5月前
|
人工智能 Java Spring
Spring Boot循环依赖的症状和解决方案
Spring Boot循环依赖的症状和解决方案
|
2天前
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo:微服务通信的高效解决方案
【10月更文挑战第15天】随着信息技术的发展,微服务架构成为企业应用开发的主流。Spring Cloud Dubbo结合了Dubbo的高性能RPC和Spring Cloud的生态系统,提供高效、稳定的微服务通信解决方案。它支持多种通信协议,具备服务注册与发现、负载均衡及容错机制,简化了服务调用的复杂性,使开发者能更专注于业务逻辑的实现。
11 2
|
18天前
|
Java 关系型数据库 开发工具
idea创建不了spring2.X版本,无法使用JDK8,最低支持JDK17 , 如何用idea创建spring2.X版本,使用JDK8解决方案
本文提供了解决方案,如何在IDEA中创建Spring 2.X版本的项目并使用JDK8,尽管Spring 2.X已停止维护且IDEA不再直接支持,通过修改pom.xml或使用阿里云的国内源来创建项目。
40 0
idea创建不了spring2.X版本,无法使用JDK8,最低支持JDK17 , 如何用idea创建spring2.X版本,使用JDK8解决方案
|
4月前
|
缓存 NoSQL Java
在 Spring Boot 应用中使用 Spring Cache 和 Redis 实现数据查询的缓存功能
在 Spring Boot 应用中使用 Spring Cache 和 Redis 实现数据查询的缓存功能
245 0
|
1月前
|
Java 开发工具 对象存储
简化配置管理:Spring Cloud Config与Netflix OSS中的动态配置解决方案
简化配置管理:Spring Cloud Config与Netflix OSS中的动态配置解决方案
33 2
|
1月前
|
前端开发 Java Spring
【非降版本解决】高版本Spring boot Swagger 报错解决方案
【非降版本解决】高版本Spring boot Swagger 报错解决方案
|
17天前
|
Java Maven Spring
用Spring导致的无法运行Java文件的问题的解决方案
本文提供了解决在IntelliJ IDEA社区版中使用Spring Initializr插件创建Spring项目后,Java文件无法运行的问题的方法,主要是通过加载Maven项目来解决。
47 0
|
2月前
|
缓存 NoSQL Java
SpringBoot的三种缓存技术(Spring Cache、Layering Cache 框架、Alibaba JetCache 框架)
Spring Cache 是 Spring 提供的简易缓存方案,支持本地与 Redis 缓存。通过添加 `spring-boot-starter-data-redis` 和 `spring-boot-starter-cache` 依赖,并使用 `@EnableCaching` 开启缓存功能。JetCache 由阿里开源,功能更丰富,支持多级缓存和异步 API,通过引入 `jetcache-starter-redis` 依赖并配置 YAML 文件启用。Layering Cache 则提供分层缓存机制,需引入 `layering-cache-starter` 依赖并使用特定注解实现缓存逻辑。
582 1
SpringBoot的三种缓存技术(Spring Cache、Layering Cache 框架、Alibaba JetCache 框架)
|
3月前
|
负载均衡 Java 开发者
Spring Cloud实战:构建分布式系统解决方案
Spring Cloud实战:构建分布式系统解决方案
|
5月前
|
JSON 安全 前端开发
跨域详解及Spring Boot 3中的跨域解决方案
本文介绍了Web开发中的跨域问题,包括概念、原因、影响以及在Spring Boot 3中的解决方案。跨域是由浏览器的同源策略限制引起的,阻碍了不同源之间的数据传输。解决方法包括CORS、JSONP和代理服务器。在Spring Boot 3中,可以通过配置CorsFilter来允许跨域请求,实现前后端分离项目的正常运行。
205 3
 跨域详解及Spring Boot 3中的跨域解决方案