前言:什么是多级缓存?

缓存的作用是减轻数据库的压力,缩短服务相应的时间,从而提高整个并发的能力,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)测试

当添加缓存之前,每当刷新一下页面,就会去数据库进行查询一次:
在这里插入图片描述

添加缓存之后,只去数据库查询一次,再重新刷新页面,不会去数据库进行查询了,而是直接返回缓存信息即可。
在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐