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 體驗,或者評論區留言探討,寫的很差,請多多指教~~
將來計劃: