前言:什么是多级缓存?
缓存的作用是减轻数据库的压力,缩短服务相应的时间,从而提高整个并发的能力,Redis单节并发以及很高了,但是依然有上限,随着互联网的发展,用户体量越来越大,比如淘宝京东的流量能达到数亿级别的流量。那么多级缓存就是为了应对多级缓存高并发。
1、传统缓存的问题:
用户请求到达Tomcat服务器,然后优先查询redis。未命中就访问数据库。 存在以下问题:
- 请求要经过tomcat处理,Tomcat的并发性能成为了整个系统的瓶颈。
- redis有淘汰策略。当redis缓存失效时,会对数据库产生冲击。
2、多级缓存方案
多级缓存就是充分利用请求处理的每个缓存,分别添加缓存,减轻Tomcat的压力,提升服务性能:
-
第一级缓存:用户通过手机访问浏览器得到渲染。 浏览器缓存。
因为浏览器可以把返回的静态资源缓存到本地的,那么下次用户访问服务器时,只需要检查有没有变化,没有变化服务器直接返回304状态码,不用返回数据了。304:说明本地有。直接渲染本地存着的页面。 减少数据的传输,提高渲染和相应的速度。 -
第二级缓存:Nginx本地缓存。
浏览器本地缓存未成功,请求Nginx服务器。Ngin之前是用来做请求代理。在这里形成第二级缓存,称为Nginx本地缓存。 也可以做业务的编写。那么将数据缓存到nging本地,用户请求来了,如果有直接返回,不用到达Tomcat。
-
第三级缓存:redis缓存。
-
第四级缓存:Tomcat进程缓存。 (JVM进程缓存)
-
第五级缓存:最后到达数据库缓存。
一、JVM进程缓存(Tomcat内部编写进程缓存)
Tomcat服务内部添加缓存, 业务进来以后优先查询进程缓存,缓存未命中,在去查数据库。
1、初识Caffeine
缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:
- 分布式缓存:例如redis
- 优点:存储量更大、可靠性更好、可以在集群间共享。
- 缺点:访问缓存有网咯开销。Tomcat访问redis时要发起网络请求,所以说有网络开销。
- 场景:缓存数量较大、可靠性要求高、需要在集群间共享
- 进程本地缓存:例如hashMap、GuavaCache
- 优点:读取本地内存,没有网络开销,速度更快。
- 缺点:存储容量有限,可靠性较低、无法共享
- 场景:性能要求较高,缓存数据量较小
(1)Caffeine介绍
Caffeine是一个基于java8开发的,提供了近乎最佳命中率的高性能的本地缓存。目前Spring内部的缓存使用的就是Caffeine。官网:https://github.com/ben-manes/caffeine
官方读写性能测试:
(2)Caffeine示例
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.7.0</version>
</dependency>
@Test
void testBasicOps() {
// 创建缓存对象。 工厂模式,在build构建出缓存对象。
Cache<String, String> cache = Caffeine.newBuilder().build();
// 存数据
cache.put("gf", "小红");
//两种取数据方式
// 第一种:取数据,不存在则返回null
String gf = cache.getIfPresent("gf");
System.out.println("gf = " + gf);
// 第二种:取数据,不存在则去数据库查询
String defaultGF = cache.get("defaultGF", key -> {
// 这里可以去数据库根据 key查询value
return "小绿";
});
System.out.println("defaultGF = " + defaultGF);
}
(3)缓存加载
缓存加载是Caffeine 的最基础特性,其支持四种加载策略:手工加载、同步加载、异步加载、异步手动加载。
- 方法一:手工加载
public void test() {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS) //基于时间:设置缓存的有效时间
.maximumSize(100).build(); //基于容量:设置缓存的数量上限
cache.put("gf", "小红");
String gf = cache.getIfPresent("gf");
System.err.println(gf);
String gf1 = cache.get("gf", key -> "小绿");
System.err.println(gf1);
}
- 方法二:同步加载
使用手工加载的方式可以给我们带来更大的灵活性,但是总是手动去加载缓存,有时未免有些不便,这种情况下,我们可以使用自动同步加载的方式。
LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
- 方法三:异步手动加载
AsyncCache 是 Cache 的一个变体类,其内部使用一个Executor进行实现,通过get()方法返回一个CompletableFuture类,通过这个方法可以构建响应式编程模型。
内部默认使用的executor 是 ForkJoinPool.commonPool(),可以通过重写Caffeine.executor(Executor)来自行指定线程池。
AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.buildAsync();
CompletableFuture<Graph> graph = cache.get(key, k -> createExpensiveGraph(key));
- 方法四:异步加载
异步加载是通过AsyncLoadingCache接口实现的,构建方式基本与同步加载相同,只不过需要注意的是,其额外提供了buildAsync((key, executor))的构造方式,可以支持传入Executor执行器。
AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// Either: Build with a synchronous computation that is wrapped as asynchronous
.buildAsync(key -> createExpensiveGraph(key));
// Or: Build with a asynchronous computation that returns a future
.buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
(4)缓存驱逐策略
- 方法一:基于容量:设置缓存的数量上限
- 方法二:基于时间:设置缓存的有效时间
- 方法三:基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。
注意:在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。
(5)缓存移除
刚刚我们了解了缓存的淘汰策略,淘汰策略是我们在构建缓存结构时,进行设定的,同时,我们也可以手动的移除缓存。
Caffeine 为我们提供了方法,来灵活操控移除缓存元素。
// 移除指定的key
cache.invalidate(key)
// 移除指定的key列表
cache.invalidateAll(keys)
// 移除全部key
cache.invalidateAll()
(6)缓存更新
在非常多的场景下,我们希望缓存数据是需要在一定周期范围内能自动更新的,当底层的数据源变更后,缓存也可以进行相应的更新,这时,我们可以通过Caffeine 提供的refreshAfterWrite()方法,来进行实现:
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
refreshAfterWrite()可以指定时间周期,在数据写入后,在指定时间后进行更新。这里的更新,与我们上面提到的淘汰机制有所不同,刷新数据,是一个异步行为,当在刷新数据的过程中,旧的缓存值依旧可以被读取到,而对于淘汰策略,当缓存元素失效后,必须等到新的数据写入完成后,新的缓存数据才可以被读取的到。
对比于expireAfterWrite()方法,refreshAfterWrite()方法是一个轻量级的数据更新,刷新的行为只有当一个元素被查询的时候,才进行触发。我们可以在构建缓存时,同时指定expireAfterWrite()方法与refreshAfterWrite()方法,这样的话,只有当数据具备刷新条件的时候才会去刷新数据,不会盲目去执行刷新操作。如果数据在刷新后就一直没有被再次查询,那么该数据也会过期。
2、实现进程缓存
(1)添加Bean对象Cache
首先,我们需要定义两个Caffeine的缓存对象,分别保存商品、库存的缓存数据。
(2)修改controller类
先进行缓存中拿数据,未中再去数据库拿数据,如果拿到数据后放到缓存后,在返回用户数据。
然后,修改item-service中的com.heima.item.web包下的ItemController类,添加缓存逻辑:
(3)测试
当添加缓存之前,每当刷新一下页面,就会去数据库进行查询一次:
添加缓存之后,只去数据库查询一次,再重新刷新页面,不会去数据库进行查询了,而是直接返回缓存信息即可。