Spring Cache 是一个很是优秀的缓存组件。java
可是在使用 Spring Cache 的过程中,小黑同窗也遇到了一些痛点。git
好比,如今有一个需求:经过多个 userId 来批量获取用户信息。github
此时,咱们的代码多是这样:redis
List<User> users = ids.stream().map(id -> {
return getUserById(id);
})
.collect(Collectors.toList());
@Cacheable(key = "#p0", unless = "#result == null")
public User getUserById(Long id) {
// ···
}
复制代码
这种写法的缺点在于:spring
在 for 循环中操做 redis。若是数据命中缓存还好,一旦缓存没有命中,则会访问数据库。数据库
也有的同窗可能会这样作:缓存
@Cacheable(key = "#ids.hash")
public Collection<User> getUsersByIds(Collection<Long> ids) {
// ···
}
复制代码
这种作法的问题是:框架
缓存是基于 id 列表的 hashcode ,只有在 id 列表的 hashcode 值相等的状况下,缓存才会命中。并且,一旦列表中的其中一个数据被修改,整个列表缓存都要被清除。less
例如:函数
第一次请求 id 列表是
1,2,3,
第二次请求的 id 列表为
1,2,4
在这种状况下,先后两次的缓存不能共享。
若是 id 为 1 的数据发生了改变,那么,这两次请求的缓存都要被清空
Spring Issue:
简单翻译一下,具体内容读者能够自行查阅相关 issue。
译文:
谢谢你的报告。缓存抽象没有这种状态的概念,若是你返回一个集合,那就是你要求在缓存中存储的东西。也没有什么强迫您为给定的缓存保留相同的项类型,因此这种假设并不适合这样的高级抽象。
个人理解是,对于 Spring Cache 这种高级抽象框架来讲,Cache 是基于方法的,若是方法返回 Collection,那整个 Collection 就是须要被缓存的内容。
纠结了很久,小黑同窗仍是决定本身来造个轮子。
那我想要达到什么样的效果呢?
我但愿对于这种根据多个 key 批量获取缓存的操做,能够先根据单个 key 从缓存中查找,若是缓存中不存在,就去加载数据,同时再将数据放到缓存中。
废话很少说,直接上源码:
简单介绍一下总体的思路:
核心接口
com.github.shenjianeng.easycache.core.Cache
com.github.shenjianeng.easycache.core.MultiCacheLoader
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);
复制代码
@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 是如今 Cache 接口的惟一实现。正如其类名同样,这是基于 redis 的缓存实现。
先说一下大体的实现思路:
废话很少说,直接上源码。
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);
}
}
复制代码
更多源码细节,若是读者感兴趣,能够自行阅读源码:easy-cache
欢迎你们 fork 体验,或者评论区留言探讨,写的很差,请多多指教~~
将来计划: