人到中年就是一部西游记:悟空的压力,八戒的身材,沙僧的发型,唐僧的唠叨
前面文章大篇幅详细讲解了Spring Cache缓存抽象、三大缓存注解的工作原理等等。若是细心的小伙伴会发现:讲解时的Demo我使用的缓存实现方案
均是Spring
默认提供的:ConcurrentMapCache
。使用它的原因是它是spring-context
内置的,无需额外导包就能使用,非常的方便~
但在实际开发过程中,Spring内建提供的实现显然是满足不了日益复杂的需求的,现实情况是很小有可能直接使用ConcurrentMapCacheManager
和ConcurrentMapCache
去作为存储方案,毕竟它提供的能力非常有限,有如下两个致命的不足:
Expire
就光这两点没有得到满足,在实际开发中就足以有理由抛弃内置实现,而需要引入第三方更为强大的缓存实现方案。
缓存标准方面:一个是JSR107
,一个是Spring Cache
,前面也说了Spring Cache
已经成为了现实中的标准,所以市面上它的实现产品非常丰富,因此本文主要看看基于Spring Cache的实现产品的集成方案。
Spring Cache它也是支持JSR107规范的,可谓非常的友好。(请导入
spring-contextr-support
包)
要想了解常用的、流行的Spring Cache的实现方案有哪些,我推荐一个由SpringBoot
提枚举类CacheType
,它里面收纳得还是比较全面的:
此枚举是SpringBoot提供的供以参考,但本文内容和SpringBoot没有半毛钱关系
public enum CacheType {
GENERIC, // 使用的SimpleCacheManager(自己手动指定Cache,可任意类型Cache实现哦)
JCACHE, // 使用org.springframework.cache.jcache.JCacheCacheManager
EHCACHE, // 使用org.springframework.cache.ehcache.EhCacheCacheManager
HAZELCAST, // 使用com.hazelcast.spring.cache.HazelcastCacheManager
INFINISPAN, // 使用org.infinispan.spring.provider.SpringEmbeddedCacheManager
COUCHBASE, // 使用com.couchbase.client.spring.cache.CouchbaseCacheManager
REDIS, // 使用org.springframework.data.redis.cache.RedisCacheManager,依赖于RedisTemplate进行操作
CAFFEINE, // 使用org.springframework.cache.caffeine.CaffeineCacheManager
@Deprecated
GUAVA, // 使用org.springframework.cache.guava.GuavaCacheManager,已经过期不推荐使用了
SIMPLE, // 使用ConcurrentMapCacheManager
NONE; // 使用NoOpCacheManager,表示禁用缓存
}
这些就是业内最为流行的那些缓存实现,下面做简单的介绍作为参考:
EhCache
:一个纯Java的进程内缓存框架,具有快速、精干等特点。因为它是纯Java进程的,所以也是基于本地缓存的。(注意:EhCache2.x和EhCache3.x差异巨大且不兼容)Hazelcast
:基于内存的数据网格。虽然它基于内存,但是分布式应用程序可以使用Hazelcast
进行分布式缓存、同步、集群、处理、发布/订阅消息等。(如果你正在寻找基于内存的、高速的、可弹性扩展的、支持分布式的、对开发者友好的NoSQL
,Hazelcast
是一个很棒的选择,它的理念是用应用服务的内存换取效率,成本较高)com.hazelcast.spring.cache.HazelcastCacheManager
这个包名中也能看出,是它自己实现的Spring Cache标准,而不是spring-data
帮它实现的(类似MyBatis
集成Spring),但它凭借自己的足够优秀,让Spring
接受了它Infinispan
:基于Apache 2.0协议的分布式键值存储系统,可以以普通java lib
或者独立服务的方式提供服务,支持各种协议(Hot Rod, REST, WebSockets
)。支持的高级特性包括:事务、事件通知、高级查询、分布式处理、off-heap及故障迁移。 它按照署模式分为嵌入式(Embedded)模式(基于本地内存)、Client-Server(C\S)
模式。Couchbase
:是一个非关系型数据库,它实际上是由couchdb+membase
组成,所以它既能像couchdb
那样存储json文档
(类似MongoDB
),也能像membase
那样高速存储键值对。(新一代的NoSql数据库,国外挺火的)Redis
:熟悉得不能再熟悉的分布式缓存,只有Client-Server(C\S)模式,单线程让它天生具有线程安全的特性。Java一般使用Jedis/Luttuce
来操纵~Caffeine(咖啡因)
:Caffeine是使用Java8对Guava缓存的重写版本,一个接近最佳的的缓存库(号称性能最好)。Spring5已经放弃guava,拥抱caffeine
,它的API保持了近乎和guava
一致,但是性能上碾压它。Google Guava
工具包的,使用非常广泛。Caffeine长江后浪推前浪,性能上碾压了Guava
,是它的替代品。SIMPLE
:略首先它哥三都作为进程缓存(本地缓存)的优秀开源产品,那么若我们要使用本地缓存来加速访问,选择哪种呢?下文做一个简单的对比:
EhCache
:是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate、MyBatis
默认的缓存提供。(备注:虽然EhCache3支持到了分布式,但它还是基于Java
进程的缓存)Guava
:它是Google Guava
工具包中的一个非常方便易用的本地化缓存实现,基于LRU
算法实现,支持多种缓存过期策略。它出现得非常早,有点廉颇老矣之感~Caffeine
:是使用Java8对Guava缓存的重写版本,在Spring5中将取代了Guava
,支持多种缓存过期策略。说明:
Caffeine
它在性能上碾压其余两者,它可以完全的替代Guava,因为API上都差不多一致,并且它还提供了Adapter让Guava过度到Caffeine
上来。
Caffeine
被称为进程缓存之王
为何Guava
被放弃了,但EhCache
依旧坚挺?我觉得主要是它有如下特点:
Ehcache3还支持了分布式的缓存
)本文讲解的是Spring Cache和`进程缓存Caffeine和EhCache的整合。
Caffeine
和Spring Cache整合关于Caffeine
的强悍之处,此处就不费笔墨了,总之两个字:优秀。若我们在Spring
应用中需要使用Caffeine
怎么办呢?当然最直接的使用方式是导入Jar包后,直接使用它的API:CacheManager和Cache等等。
当然,这不是本文要讲述的,本文主要是要让它和Spring集成,从而可以使用Spring Cache注解来直接操作缓存~
整合Caffeine
,其实Spring已经有个模块对它提供了支持:spring-context-support
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-context-supportartifactId>
<version>5.1.6.RELEASEversion>
dependency>
此包属于spring-context的支持包,一般建议导入。它的内容如下:
需要注意的是,在Spring5
之前,此包还默认提供了对Guava
的支持,但在Spring5
后彻底移除了,这也侧面证明Guava确实该退休了~。
集成第一步:除了导入support包,当然还得导入咖啡因的包:
<dependency>
<groupId>com.github.ben-manes.caffeinegroupId>
<artifactId>caffeineartifactId>
<version>2.7.0version>
dependency>
实施之前,先简单看看spring-context-support
提供的CaffeineCacheManager
实现:
// @since 4.3 Requires Caffeine 2.1 or higher.显然我们都2.7版本 肯定满足呀
public class CaffeineCacheManager implements CacheManager {
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
// 默认能动态生成Cache,对使用者友好
private boolean dynamic = true;
// 默认使用的builder 可通过setCaffeine来自定这个cacheBuilder
// cacheBuilder.build()得到一个com.github.benmanes.caffeine.cache.Cache 让可以自定义N个参数
private Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
@Nullable
private CacheLoader<Object, Object> cacheLoader;
private boolean allowNullValues = true; // 是否允许null值
// 一样的,两个构造函数。你可以指定,也可以让动态生成
public CaffeineCacheManager() {
}
public CaffeineCacheManager(String... cacheNames) {
setCacheNames(Arrays.asList(cacheNames));
}
...
@Override
@Nullable
public Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
if (cache == null && this.dynamic) {
synchronized (this.cacheMap) {
cache = this.cacheMap.get(name);
if (cache == null) {
cache = createCaffeineCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
}
// CaffeineCache实现了org.springframework.cache.Cache接口
// 内部实现都是委托给com.github.benmanes.caffeine.cache.Cache
protected Cache createCaffeineCache(String name) {
return new CaffeineCache(name, createNativeCaffeineCache(name), isAllowNullValues());
}
...
}
它提供的Cache实现:CaffeineCache
。非常简单,所有工作都委托给com.github.benmanes.caffeine.cache.Cache
了,因此省略。
第二步:准备CacheConfig 配置文件
@EnableCaching
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 方案一(常用):定制化缓存Cache
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.initialCapacity(100)
.maximumSize(10_000))
// 如果缓存种没有对应的value,通过createExpensiveGraph方法同步加载 buildAsync是异步加载
//.build(key -> createExpensiveGraph(key))
;
// 方案二:传入一个CaffeineSpec定制缓存,它的好处是可以把配置方便写在配置文件里
//cacheManager.setCaffeineSpec(CaffeineSpec.parse("initialCapacity=50,maximumSize=500,expireAfterWrite=5s"));
return cacheManager;
}
}
@Service
public class CacheDemoServiceImpl implements CacheDemoService {
@Cacheable(cacheNames = "demoCache", key = "#id")
@Override
public Object getFromDB(Integer id) {
System.out.println("模拟去db查询~~~" + id);
return "hello cache...";
}
}
运行单测:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class, CacheConfig.class})
public class TestSpringBean {
@Autowired
private CacheDemoService cacheDemoService;
@Autowired
private CacheManager cacheManager;
@Test
public void test1() {
cacheDemoService.getFromDB(1);
cacheDemoService.getFromDB(1);
System.out.println("----------验证缓存是否生效----------");
Cache cache = cacheManager.getCache("demoCache");
System.out.println(cache);
System.out.println(cache.get(1, String.class));
}
}
打印结果:
模拟去db查询~~~1
----------验证缓存是否生效----------
org.springframework.cache.caffeine.CaffeineCache@4f74980d
hello cache...
从结果中可以得出结论:缓存生效。
关于
Caffeine
的更多API以及它的高级使用,不是本文讨论的内容,有兴趣的小伙伴可以自行学习和研究~
Ehcache2.x
/Ehcache3.x
和Spring Cache整合Ehcache2.x
和Ehcache3.x
它最大的一个特点是:3.x不向下兼容2.x。从他俩的GAV
坐标也能看出这种差异:
<!-- https://mvnrepository.com/artifact/net.sf.ehcache/ehcache -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.ehcache/ehcache -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.7.1</version>
</dependency>
不仅仅GAV变了,包名也都换了,因此是二进制不兼容的,并且3.x和2.x的API都有非常大的差异。
虽然说2.x也还是维护着(毕竟有非常重的历史包袱),但是活跃度已经远不及3.x了,因此我认为拥抱EhCache3.x是大势所趋
这里有意思的是,spring-context-support
即使在Spring5
后,默认支持的还是EhCache2.x
版本(毕竟有很重的历史包袱在呢),并且没有提供3.x版本的支持,这应该也是为何你看到大多数人还只是在使用EhCache2.x的根本原因吧~
Ehcache2.x
的集成方案几乎同Caffeine
,略过。
2.x配置
CacheManager
的时候,既能全用API方式。当然也能简便的使用ehcache.xml
方式,内容形如:
<ehcache>
<diskStore path="d:/ehcache/">diskStore>
<defaultCache maxElementsInMemory="10000" eternal="false"
timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" />
<cache name="User" maxElementsInMemory="10000" eternal="false"
timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true"/>
ehcache>
关于xml配置文件的更多属性和含义,请参考官方文档的说明
Ehcache3.x
的社区比EhCache2.x
活跃很多,所以拥抱和使用3.x版本似乎是必然的。但是奈何Spring并没有提供内置的CacheManager
对3.x提供支持,因此此处我总结继承它的两种方案:
CacheManager
和Cache
等相关规范接口JCache
(推荐)上面截图我们能看到support包里是有对jcache(JSR107)
的支持,而切好EhCache3.x
它实现了JSR107
规范(但没有实现Spring-Cache),为了集成它,我们就用现成的方案:jcache+EhCache3.x
来实现对Spring的整合。
第一步:先导包
<dependency>
<groupId>javax.cachegroupId>
<artifactId>cache-apiartifactId>
<version>1.1.1version>
dependency>
<dependency>
<groupId>org.ehcachegroupId>
<artifactId>ehcacheartifactId>
<version>3.7.1version>
dependency>
先简单看看jcache
中JCacheCacheManager
的实现:
// @since 3.2
public class JCacheCacheManager extends AbstractTransactionSupportingCacheManager {
// 可见JCacheCacheManager其实就相当于代理,实际做事的是javax.cache.CacheManager
@Nullable
private CacheManager cacheManager;
private boolean allowNullValues = true;
public JCacheCacheManager() {
}
public JCacheCacheManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
...
@Override
public void afterPropertiesSet() {
if (getCacheManager() == null) {
setCacheManager(Caching.getCachingProvider().getCacheManager());
}
super.afterPropertiesSet();
}
// 它使用的是JCacheCache俩把javax.cache.Cache包装起来 类似于适配的效果
@Override
protected Collection<Cache> loadCaches() {
CacheManager cacheManager = getCacheManager();
Assert.state(cacheManager != null, "No CacheManager set");
Collection<Cache> caches = new LinkedHashSet<>();
for (String cacheName : cacheManager.getCacheNames()) {
javax.cache.Cache<Object, Object> jcache = cacheManager.getCache(cacheName);
caches.add(new JCacheCache(jcache, isAllowNullValues()));
}
return caches;
}
@Override
protected Cache getMissingCache(String name) {
CacheManager cacheManager = getCacheManager();
Assert.state(cacheManager != null, "No CacheManager set");
// Check the JCache cache again (in case the cache was added at runtime)
javax.cache.Cache<Object, Object> jcache = cacheManager.getCache(name);
if (jcache != null) {
return new JCacheCache(jcache, isAllowNullValues());
}
return null;
}
}
由此可见,实际上JCache就相当于对JSR107做了一层适配,让所有实现了JSR107
的缓存方案,都能够用在Spring环境中。
第二步:准备配置(集成)方案,此处给出两种配置方案:
一、使用最容易的JCacheManagerFactoryBean
+ ehcache.xml
的方式:
@EnableCaching
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
@Bean
public JCacheManagerFactoryBean cacheManagerFactoryBean() throws IOException {
JCacheManagerFactoryBean factoryBean = new JCacheManagerFactoryBean();
// 配置全部写在ehcache.xml这个配置文件内~~~~
factoryBean.setCacheManagerUri(new ClassPathResource("ehcache.xml").getURI());
return factoryBean;
}
@Bean
public CacheManager cacheManager(javax.cache.CacheManager cacheManager) {
// 它必须要包装一个javax.cache.CacheManager,也就是Eh107CacheManager才行
JCacheCacheManager cacheCacheManager = new JCacheCacheManager();
// 方式一:使用`JCacheManagerFactoryBean` + xml配置文件的方式
cacheCacheManager.setCacheManager(cacheManager);
return cacheCacheManager;
}
ehcache.xml
配置文件如下:
<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns='http://www.ehcache.org/v3'
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.1.xsd">
<cache-template name="heap-cache">
<resources>
<heap unit="entries">2000heap>
<offheap unit="MB">100offheap>
resources>
cache-template>
<cache alias="demoCache" uses-template="heap-cache">
<expiry>
<ttl unit="seconds">40ttl>
expiry>
cache>
config>
运行如上单侧,打印结果如下:
模拟去db查询~~~1
----------验证缓存是否生效----------
org.springframework.cache.jcache.JCacheCache@1cc680e
hello cache...
缓存生效(使用的JCacheCache
)。
它的基本原理是依赖于
Caching.getCachingProvider().getCacheManager()
这句代码来生成CacheManager
。而EhCache提供了EhcacheCachingProvider
实现了CachingProvider
接口从而实现了getCacheManager()
方法~~~
二、使用org.ehcache.config.CacheConfiguration
纯API的方式
略
关于进程缓存这一块,虽然有好几个产品可供选择,但我推荐使用
caffeine
。(有的小伙伴为了简单而使用EhCache2.x
,我个人是不太推荐这种做法的)
本文介绍了进程缓存之王Caffeine Cache
和EhCache
。互联网软件神速发展,用户的体验度是判断一个软件好坏的重要原因,所以缓存是必不可少的一个神器。
我曾经的曾经面试过一个一个小伙,让他说说对Spring缓存的理解,它一直描述Redis
,从沟通细节中甚至一度让我觉得他眼中的Spring缓存就是指的Redis。希望本文能给小伙伴带来一些帮助,不要有这样的误以为,被同行知道了会很尴尬的~
最后我想说:使用分布式缓存Redis
确实能应对非常多的场景(绝大部分都使用Redis这也造成了上面我描述的错觉
),但真正意义上的优化、高速缓存等等都是必须对本地缓存有深入了解的~~
Author | A哥(YourBatman) |
---|---|
个人站点 | www.yourbatman.cn |
[email protected] | |
微 信 | fsx641385712 |
活跃平台 |
|
公众号 | BAT的乌托邦(ID:BAT-utopia) |
知识星球 | BAT的乌托邦 |
每日文章推荐 | 每日文章推荐 |