每篇一句
不考虑业务场景的架构都是耍流氓
前言
缓存(Cache)
是计算机领域一个极其重要的概念,它是提高硬件(比如CPU、显卡)、软件运行效率非常重要且有效的一个手段,它的最大特点就一个字:速度非常快
。
缓存就是数据交换的缓冲区(称作:Cache),当要读取数据时,会首先从缓存汇总查询数据,有则直接执行返回,速度飞快。它被运用在计算机领域的各个方面,介绍如下:
- 操作系统磁盘缓存 ——> 减少磁盘机械操作
- Web服务器缓存——>减少应用服务器请求
- 客户端浏览器缓存——>减少对网站的访问
- 应用程序缓存——>减少对数据库的查询
- 数据库缓存——>减少文件系统IO
本文讲解的缓存就是运用在我们应用程序(软件)上的缓存,并且主要指的是在Spring环境下对缓存的使用。随着Spring框架的普及和渗透,在Spring应用中使用缓存,应该成为了当下Java开发者必备的一个基础技能了~
本文主要讲解Spring对缓存的抽象,当然也会对JSR107缓存抽象
进行概念性的介绍。
JSR107缓存抽象:JCache
说起JSR107
或者说是JCache
,估摸大多数小伙伴都会觉得非常的陌生,没用过且还没听过。
JSR107
的草案提得其实是非常的早的,但是第一个Final Release
版本却一直难产到了2014年,如图(本文截自JSR官网):
虽然最终它还是被作为JSR规范提出了,但那时已经4102年了,黄瓜菜早就凉凉~
在还没有缓存规范出来之前,作为Java市场标准制定的强有力竞争者:Spring框架
动作频频,早在2011年就提供了它自己的缓存抽象(Spring3.1
)。这一切依托于Spring的良好生态下,各大缓存厂商纷纷提供了实现产品。
因此目前而言,关于缓存这块业界有个通识:
Spring Cache
缓存抽象已经成了业界实际的标准(几乎所有产品都支持)JSR107
仅仅只是官方的标准而已(支持的产品并不多)
因为JSR107
使用得极少,因此此处对它只做比较简单的一个概念介绍即可。
若要使用JCache,首先我们得额外导包(API包):
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>1.1.1</version>
</dependency>
2019年5月发布的最新的1.1.1版本。1.0.0版本是2014年5月发布的
从这个jar的类里可以看到,它几乎所有都是接口,自己并不提供具体实现(第三方厂商自行实现)。
JCache的实现产品挺少的,
Ehcache3.x
有实现JSR107相关规范接口
它的核心类的层次结构图:
CachingProvider
:创建、配置、获取、管理和控制多个CacheManager
CacheManager
:创建、配置、获取、管理和控制多个唯一命名的Cache
。(一个CacheManager仅被一个CachingProvider所拥有)Cache
:一个类似Map的数据结构。(一个Cache仅被一个CacheManager所拥有)Entry
:一个存储在Cache中的key-value对Expiry
:每一个存储在Cache中的条目有一个定义的有效期,过期后不可访问、更新、删除。缓存有效期可以通过ExpiryPolicy
设置
说实话,我个人认为JCache的这个设计太大而全了,导致我们使用它的复杂度是非常高的,因此难以流行起来。(其实JavaEE的很多设计都有这个通病,标准过于复杂,落地实操性很差~)
我看网上有小伙伴评论说:JSR107的设计简直莫名其妙。
其实啊,针对这种评论一定要辩证性的看待,毕竟JSR是全球顶级专家一起制定的,整体优秀性我觉得是毋庸置疑的,只是它作为标准,它不能对那20%的场景避而不谈,而Spring却可以,这就是差别~
Spring缓存抽象
上面说了JCache
真正发布都到2014年了,而早在2011年Spring3.1
版本就定义了它自己的缓存抽象,旨在帮助开发者简化缓存的开发,并且最终流行开来。
从截图中可以看到,它被定义在spring-context
里面的,作为上下文的核心内容,并不需要额外导包。
Spring的缓存抽象相关类的层次结构非常简单:
CacheManager
:缓存管理器。管理各种缓存(Cache
)组件Cache
:为缓存的组件规范定义,包含缓存的各种操作集合。比如它有很多实现:ConcurrentMapCache
、RedisCache
、EhCacheCache
(额外导包)
说明:看到这个层次结构,很多小伙伴会问为何没有Expire
的定义?
这里我想说:这也是我比较费解的地方之一。Expire
作为缓存非常重要的能力,为何不抽象出来呢???这也是我们经常苦恼的地方:@Cacheable
注解竟然不支持TTL过期时间的设置,着实让人很蛋疼~~~
我个人把Spring没有Expire
这个理解为Spring缓存抽象的一个功能缺失,说不客气点就是Spring的一个Bug。不知各位对此是否和我相同意见呢?欢迎砸我讨论
至于为何它一直都没有“修复”?我感觉是因为Cache属于它对外公布的API,各大产品都自己实现了Expire,而且方式不尽相同,所以最终它想统一就很难了,很难做到最好的兼容性~
CacheManager
和Cache
的使用示例
CacheManager简单描述就是用来存放Cache,Cache用于存放具体的key-value值。
比如一个名为"汽车厂"的Cache,那你就可以通过这个名字从CacheManager
拿出这个Cache,然后往里面缓存汽车。
首先看看CacheManager
这个接口:
// pring's central cache manager SPI. 它是个SPI接口
// @since 3.1
public interface CacheManager {
@Nullable
Cache getCache(String name);
// 管理的所有的Cache的names~
Collection<String> getCacheNames();
}
它的继承树如下(不进行任何额外导包的情况下):
这些实现是Spring内置的最基础的缓存管理器类。
AbstractCacheManager
// @since 3.1 实现了InitializingBean接口~~
public abstract class AbstractCacheManager implements CacheManager, InitializingBean {
// 保存着所有的Cache对象~~key为名字
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
// 此处使用了volatile 关键字
private volatile Set<String> cacheNames = Collections.emptySet();
@Override
public void afterPropertiesSet() {
initializeCaches();
}
// @since 4.2.2 模版方法模式。 abstract方法loadCaches()交给子类实现~~~
public void initializeCaches() {
Collection<? extends Cache> caches = loadCaches();
synchronized (this.cacheMap) {
this.cacheNames = Collections.emptySet();
this.cacheMap.clear();
Set<String> cacheNames = new LinkedHashSet<>(caches.size());
for (Cache cache : caches) {
String name = cache.getName();
// decorateCache是protected方法,交给子类 不复写也无所谓~~~
this.cacheMap.put(name, decorateCache(cache));
cacheNames.add(name);
}
// cacheNames是个只读视图~(框架设计中考虑读写特性~)
this.cacheNames = Collections.unmodifiableSet(cacheNames);
}
}
protected abstract Collection<? extends Cache> loadCaches();
// 根据名称 获取Cache对象。若没有,就返回null
@Override
@Nullable
public Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
if (cache != null) {
return cache;
} else {
// Fully synchronize now for missing cache creation...
synchronized (this.cacheMap) {
cache = this.cacheMap.get(name);
if (cache == null) {
// getMissingCache默认直接返回null,交给子类复写~~~~
// 将决定权交给实现者,你可以创建一个Cache,或者记录日志
cache = getMissingCache(name);
// cache != null,主要靠getMissingCache这个方法了~~~向一个工厂一样创建一个新的~~~
if (cache != null) {
cache = decorateCache(cache);
this.cacheMap.put(name, cache);
// 向全局缓存里面再添加进去一个Cache~~~~
updateCacheNames(name);
}
}
return cache;
}
}
}
@Override
public Collection<String> getCacheNames() {
return this.cacheNames;
}
// @since 4.1
protected final Cache lookupCache(String name) {
return this.cacheMap.get(name);
}
...
}
这个抽象类其实还蛮重要的,它提供了基本的操作,如果已存的CacheManager
们都无法满足你的要求,你可以自己通过继承AbstractCacheManager
实现一个自己的CacheManager
。
比如Redis相关的
RedisCacheManager
就是继承自它的~
SimpleCacheManager
// @since 3.1
public class SimpleCacheManager extends AbstractCacheManager {
private Collection<? extends Cache> caches = Collections.emptySet();
public void setCaches(Collection<? extends Cache> caches) {
this.caches = caches;
}
@Override
protected Collection<? extends Cache> loadCaches() {
return this.caches;
}
}
最简单的一个CacheManager
,它的caches
都必须由调用者手动指定。若交给容器管理会自动执行afterPropertiesSet()
方法,否则需要手动自己调用cacheManager.afterPropertiesSet();
,自己放进去的setCaches
才会生效~
NoOpCacheManager
一种基本的、无操作的CacheManager
实现,适用于禁用缓存,通常用于在没有实际存储的情况下作为缓存声明。
ConcurrentMapCacheManager
(重要)
// @since 3.1
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
// true表示:若为null,就新创建一个缓存添加进去
private boolean dynamic = true;
// 是否允许value为null
private boolean allowNullValues = true;
// 指定此缓存管理器是存储每个条目的副本,还是存储其所有缓存的引用
// 影响到ConcurrentMapCache对value值的存储~~~
// false表示:存储它自己(引用)
// true表示:存储一个副本(对序列化有要求~~~) 很少这么使用~~~
private boolean storeByValue = false;
public ConcurrentMapCacheManager() {
}
public ConcurrentMapCacheManager(String... cacheNames) {
setCacheNames(Arrays.asList(cacheNames));
}
public void setCacheNames(@Nullable Collection<String> cacheNames) {
if (cacheNames != null) {
for (String name : cacheNames) {
this.cacheMap.put(name, createConcurrentMapCache(name));
}
this.dynamic = false; // 手动设置了,就不允许在动态创建了
} else {
this.dynamic = true;
}
}
protected Cache createConcurrentMapCache(String name) {
// isStoreByValue=true需要存储值的副本的时候,才对序列化有要求~~~否则直接存引用即可
SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null);
return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256), isAllowNullValues(), actualSerialization);
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.serialization = new SerializationDelegate(classLoader);
if (isStoreByValue()) {
// 重新创建Cache 因为要副本嘛
recreateCaches();
}
}
private void recreateCaches() {
for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
entry.setValue(createConcurrentMapCache(entry.getKey()));
}
}
@Override
@Nullable
public Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
// dynamic=true 为null的时候会动态创建一个
if (cache == null && this.dynamic) {
synchronized (this.cacheMap) {
cache = this.cacheMap.get(name);
if (cache == null) {
cache = createConcurrentMapCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
...
}
它的缓存存储是基于内存的,所以它的生命周期是与应用关联的,对于生产级别的大型企业级应用程序,这可能并不是理想的选择,但它用于本地自己测试是个很好的选择。
CompositeCacheManager
// @since 3.1
public class CompositeCacheManager implements CacheManager, InitializingBean {
// 内部聚合管理着一批CacheManager
private final List<CacheManager> cacheManagers = new ArrayList<>();
// 若这个为true,则可以结合NoOpCacheManager实现效果~~~
private boolean fallbackToNoOpCache = false;
public CompositeCacheManager() {
}
public CompositeCacheManager(CacheManager... cacheManagers) {
setCacheManagers(Arrays.asList(cacheManagers));
}
// 也可以调用此方法,来自己往里面添加(注意是添加)CacheManager们
public void setCacheManagers(Collection<CacheManager> cacheManagers) {
this.cacheManagers.addAll(cacheManagers);
}
public void setFallbackToNoOpCache(boolean fallbackToNoOpCache) {
this.fallbackToNoOpCache = fallbackToNoOpCache;
}
// 如果fallbackToNoOpCache=true,那么在这个Bean初始化完成后,也就是在末尾添加一个NoOpCacheManager
// 当然fallbackToNoOpCache默认值是false
@Override
public void afterPropertiesSet() {
if (this.fallbackToNoOpCache) {
this.cacheManagers.add(new NoOpCacheManager());
}
}
// 找到一个cache不为null的就return了~
@Override
@Nullable
public Cache getCache(String name) {
for (CacheManager cacheManager : this.cacheManagers) {
Cache cache = cacheManager.getCache(name);
if (cache != null) {
return cache;
}
}
return null;
}
// 可以看到返回的是所有names,并且用的set
@Override
public Collection<String> getCacheNames() {
Set<String> names = new LinkedHashSet<>();
for (CacheManager manager : this.cacheManagers) {
names.addAll(manager.getCacheNames());
}
return Collections.unmodifiableSet(names);
}
}
CompositeCacheManager
要通过一个或更多的缓存管理器来进行配置,它会迭代这些缓存管理器,以查找之前所缓存的值。
看完CacheManager
,再看看Cache
这个接口:
public interface Cache {
String getName();
// 返回本地存储的那个。比如ConcurrentMapCache本地就是用的一个ConcurrentMap
Object getNativeCache();
// 就是用下面的ValueWrapper把值包装了一下而已~
@Nullable
ValueWrapper get(Object key);
@Nullable
<T> T get(Object key, @Nullable Class<T> type);
@Nullable
<T> T get(Object key, Callable<T> valueLoader);
void put(Object key, @Nullable Object value);
// @since 4.1
// 不存在旧值直接put就先去了返回null,否则返回旧值(并且不会把新值put进去)
@Nullable
ValueWrapper putIfAbsent(Object key, @Nullable Object value);
// 删除
void evict(Object key);
// 清空
void clear();
@FunctionalInterface
interface ValueWrapper {
@Nullable
Object get();
}
}
它的继承树非常简单,如下:
AbstractValueAdaptingCache
// @since 4.2.2 出现得还是挺晚的~~~
public abstract class AbstractValueAdaptingCache implements Cache {
private final boolean allowNullValues;
protected AbstractValueAdaptingCache(boolean allowNullValues) {
this.allowNullValues = allowNullValues;
}
// lookup为抽象方法
@Override
@Nullable
public ValueWrapper get(Object key) {
Object value = lookup(key);
return toValueWrapper(value);
}
@Nullable
protected abstract Object lookup(Object key);
// lookup出来的value继续交给fromStoreValue()处理~ 其实就是对null值进行了处理
// 若是null值就返回null,而不是具体的值了~~~
@Override
@SuppressWarnings("unchecked")
@Nullable
public <T> T get(Object key, @Nullable Class<T> type) {
Object value = fromStoreValue(lookup(key));
if (value != null && type != null && !type.isInstance(value)) {
throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value);
}
return (T) value;
}
// 它是protected 方法 子类有复写
@Nullable
protected Object fromStoreValue(@Nullable Object storeValue) {
if (this.allowNullValues && storeValue == NullValue.INSTANCE) {
return null;
}
return storeValue;
}
// 提供给子类使用的方法,对null值进行转换~ 子类有复写
protected Object toStoreValue(@Nullable Object userValue) {
if (userValue == null) {
if (this.allowNullValues) {
return NullValue.INSTANCE;
}
throw new IllegalArgumentException("Cache '" + getName() + "' is configured to not allow null values but null was provided");
}
return userValue;
}
// 把value进行了一层包装为SimpleValueWrapper
@Nullable
protected Cache.ValueWrapper toValueWrapper(@Nullable Object storeValue) {
return (storeValue != null ? new SimpleValueWrapper(fromStoreValue(storeValue)) : null);
}
}
显然该类是后来(Spring4.2.2)插入进来的专门对null值进行的处理。它提供了通用实现,来适配null值的问题。若你自定义Cache的实现,建议继承自此抽象类。
ConcurrentMapCache
public class ConcurrentMapCache extends AbstractValueAdaptingCache {
private final String name;
// 底层存储就是个java.util.concurrent.ConcurrentMap~~
private final ConcurrentMap<Object, Object> store;
...
// 本处只写出这个最全的构造器 它是protected 的,其它的构造器是public的
// 说白了serialization这个类不允许外部知道,子类知道即可~~~
protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues, @Nullable SerializationDelegate serialization) {
super(allowNullValues);
Assert.notNull(name, "Name must not be null");
Assert.notNull(store, "Store must not be null");
this.name = name;
this.store = store;
this.serialization = serialization;
}
// 显然只有指定了序列化器,才有可能true:存储副本
public final boolean isStoreByValue() {
return (this.serialization != null);
}
...
@Override
public final ConcurrentMap<Object, Object> getNativeCache() {
return this.store;
}
// 这是父类的抽象方法
@Override
@Nullable
protected Object lookup(Object key) {
return this.store.get(key);
}
@Override
@Nullable
public <T> T get(Object key, Callable<T> valueLoader) {
return (T) fromStoreValue(this.store.computeIfAbsent(key, r -> {
try {
return toStoreValue(valueLoader.call());
} catch (Throwable ex) {
throw new ValueRetrievalException(key, valueLoader, ex);
}
}));
}
@Override
public void put(Object key, @Nullable Object value) {
this.store.put(key, toStoreValue(value));
}
@Override
@Nullable
public ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
Object existing = this.store.putIfAbsent(key, toStoreValue(value));
return toValueWrapper(existing);
}
@Override
public void evict(Object key) {
this.store.remove(key);
}
@Override
public void clear() {
this.store.clear();
}
...
}
ConcurrentMapCache
它是spring-context
提供的内建唯一缓存实现,它是完全基于本地内存的。
Springboot默认使用的是
SimpleCacheConfiguration
,它配置的是ConcurrentMapCacheManager
来实现缓存,因此对应Cache
实现为ConcurrentMapCache
NoOpCache
NoOpCache
配合NoOpCacheManager
使用~
使用示例
上面介绍了spring-context
自带的一些缓存管理器CacheManager实现以及缓存Cache实现。接下来是骡子是马,现在拉出来遛遛,用个案例介绍它的使用方式:
public static void main(String[] args) {
CacheManager cacheManager = new ConcurrentMapCacheManager(); //使用ConcurrentMapCacheManager可以不用初始化指定,可以get的时候动态创建Cache
//CacheManager cacheManager = new ConcurrentMapCacheManager("car");
// 即使我们上面没有放进去名字为car的Cache,此处也会帮我们自动生成~~~
Cache carCache = cacheManager.getCache("car");
// 向缓存里加数据
carCache.put("benz", "奔驰");
carCache.put("bmw", "宝马");
carCache.put("audi", "奥迪");
System.out.println(carCache.getClass()); //class org.springframework.cache.concurrent.ConcurrentMapCache
// 从缓存里获取数据
System.out.println(carCache.get("benz").get()); //奔驰
System.out.println(carCache.get("benz", String.class)); //奔驰
}
此处我们用ConcurrentMapCacheManager
作为实例,其实还可以使用SimpleCacheManager
这个最为简单的缓存管理器:
public static void main(String[] args) {
CacheManager cacheManager = new SimpleCacheManager();
Cache car = cacheManager.getCache("car");
System.out.println(car); //null
}
可以看到若我们使用SimpleCacheManager
这个缓存管理器,它并不会给我们动态生成Cache对象,一切都变成手动档了:
public static void main(String[] args) {
SimpleCacheManager cacheManager = new SimpleCacheManager();
ConcurrentMapCache carMapCache = new ConcurrentMapCache("car");
cacheManager.setCaches(Collections.singleton(carMapCache));
cacheManager.afterPropertiesSet(); // 这一步是必须的
Cache car = cacheManager.getCache("car");
System.out.println(car); //org.springframework.cache.concurrent.ConcurrentMapCache@19469ea2
}
我们手动准备Cache、手动调用afterPropertiesSet()
才有用~
本例只介绍了单元测试时的使用方式,若和Spring集成,一切就更简单了,各位小伙伴自行实践吧~
CompositeCacheManager
+ NoOpCacheManager
CompositeCacheManager
主要用于集合多个CacheManager
实例,在使用多种缓存容器(比如Redis+EhCache
的组合)时特别有用。
设想这一个场景:当代码中使用@Cacheable
注解指定的cacheNames
中,却没有这个cacheManagers
时,执行时便会报错。但是若此时我们使用的是CompositeCacheManager
并且设置fallbackToNoOpCache=true
,那么它就会没找到也最终进入到NoOpCacheManager
里面去(用NoOpCache
代替~),此时就相当于禁用
掉了缓存,而不抛出相应的异常。
当你的应用中使用到了多个缓存的时候,强烈建议使用
CompositeCacheManager
管理(当然倘若是一个缓存也可以使用它,方便日后更加方便的扩展,这点在缓存注解章节里有深入讲解~)
最后需要注意的是:如果需要让Spring容器中
的缓存可以正常工作,必须配置至少
一个CacheManager。
总结
本文介绍了JSR107的缓存抽象JCache的概念和设计,以及重点介绍了Spring对缓存的抽象,希望各位看官在实操过程中,也需要注重一定概念性东西,更需要关注一下业界规范。需要注意的是,缓存不是Java世界中的一个概念~~~
本文重在概念的介绍、核心接口类的理解。至于如何防止缓存穿透、缓存击穿、缓存雪崩和缓存刷新等高级话题,后面也会加以论述~
关注A哥
Author | A哥(YourBatman) |
---|---|
个人站点 | www.yourbatman.cn |
yourbatman@qq.com | |
微 信 | fsx641385712 |
活跃平台 | ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
公众号 | BAT的乌托邦(ID:BAT-utopia) |
知识星球 | BAT的乌托邦 |
每日文章推荐 | 每日文章推荐 |