• 多级缓存之JVM进程缓存


    1.什么是多级缓存

    传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库,如图:

    在这里插入图片描述

    存在下面的问题:

    • 请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈

    • Redis缓存失效时,会对数据库产生冲击

    多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能:

    • 浏览器访问静态资源时,优先读取浏览器本地缓存
    • 访问非静态资源(ajax查询数据)时,访问服务端
    • 请求到达Nginx后,优先读取Nginx本地缓存
    • 如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat
    • 如果Redis查询未命中,则查询Tomcat
    • 请求进入Tomcat后,优先查询JVM进程缓存
    • 如果JVM进程缓存未命中,则查询数据库

    在这里插入图片描述

    在多级缓存架构中,Nginx内部需要编写本地缓存查询、Redis查询、Tomcat查询的业务逻辑,因此这样的nginx服务不再是一个反向代理服务器,而是一个编写业务的Web服务器了

    因此这样的业务Nginx服务也需要搭建集群来提高并发,再有专门的nginx服务来做反向代理,如图:

    在这里插入图片描述

    另外,我们的Tomcat服务将来也会部署为集群模式:

    在这里插入图片描述

    可见,多级缓存的关键有两个:

    • 一个是在nginx中编写业务,实现nginx本地缓存、RedisTomcat的查询

    • 另一个就是在Tomcat中实现JVM进程缓存

    2. 初识Caffeine

    缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:

    • 分布式缓存,例如Redis
      • 优点:存储容量更大、可靠性更好、可以在集群间共享
      • 缺点:访问缓存有网络开销
      • 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
    • 进程本地缓存,例如HashMapGuavaCache
      • 优点:读取本地内存,没有网络开销,速度更快
      • 缺点:存储容量有限、可靠性较低、无法共享
      • 场景:性能要求较高,缓存数据量较小

    利用Caffeine框架来实现JVM进程缓存。

    Caffeine 是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是CaffeineGitHub地址:https://github.com/ben-manes/caffeine

    Caffeine的性能非常好,下图是官方给出的性能对比:

    在这里插入图片描述

    可以看到Caffeine的性能遥遥领先!

    缓存使用的基本API

    @Test
    void testBasicOps() {
        // 构建cache对象
        Cache<String, String> cache = Caffeine.newBuilder().build();
    
        // 存数据
        cache.put("gf", "迪丽热巴");
    
        // 取数据
        String gf = cache.getIfPresent("gf");
        System.out.println("gf = " + gf);
    
        // 取数据,包含两个参数:
        // 参数一:缓存的key
        // 参数二:Lambda表达式,表达式参数就是缓存的key,方法体是查询数据库的逻辑
        // 优先根据key查询JVM缓存,如果未命中,则执行参数二的Lambda表达式
        String defaultGF = cache.get("defaultGF", key -> {
            // 根据key去数据库查询数据
            return "柳岩";
        });
        System.out.println("defaultGF = " + defaultGF);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    Caffeine既然是缓存的一种,肯定需要有缓存的清除策略,不然的话内存总会有耗尽的时候。

    Caffeine提供了三种缓存驱逐策略:

    • 基于容量:设置缓存的数量上限

      // 创建缓存对象
      Cache<String, String> cache = Caffeine.newBuilder()
          .maximumSize(1) // 设置缓存大小上限为 1
          .build();
      
      • 1
      • 2
      • 3
      • 4
    • 基于时间:设置缓存的有效时间

      // 创建缓存对象
      Cache<String, String> cache = Caffeine.newBuilder()
          // 设置缓存有效期为 10 秒,从最后一次写入开始计时 
          .expireAfterWrite(Duration.ofSeconds(10)) 
          .build();
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    • 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

    注意:在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。

    3. 实现JVM进程缓存

    3.1. 需求

    利用Caffeine实现下列需求:

    • 给根据id查询商品的业务添加缓存,缓存未命中时查询数据库
    • 给根据id查询商品库存的业务添加缓存,缓存未命中时查询数据库
    • 缓存初始大小为100
    • 缓存上限为10000

    3.2. 实现

    首先,我们需要定义两个Caffeine的缓存对象,分别保存商品、库存的缓存数据。

    item-servicecom.dcxuexi.item.config包下定义CaffeineConfig类:

    package com.dcxuexi.item.config;
    
    import com.github.benmanes.caffeine.cache.Cache;
    import com.github.benmanes.caffeine.cache.Caffeine;
    import com.dcxuexi.item.pojo.Item;
    import com.dcxuexi.item.pojo.ItemStock;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class CaffeineConfig {
    
        @Bean
        public Cache<Long, Item> itemCache(){
            return Caffeine.newBuilder()
                    .initialCapacity(100)
                    .maximumSize(10_000)
                    .build();
        }
    
        @Bean
        public Cache<Long, ItemStock> stockCache(){
            return Caffeine.newBuilder()
                    .initialCapacity(100)
                    .maximumSize(10_000)
                    .build();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    然后,修改item-service中的com.dcxuexi.item.web包下的ItemController类,添加缓存逻辑:

    @RestController
    @RequestMapping("item")
    public class ItemController {
    
        @Autowired
        private IItemService itemService;
        @Autowired
        private IItemStockService stockService;
    
        @Autowired
        private Cache<Long, Item> itemCache;
        @Autowired
        private Cache<Long, ItemStock> stockCache;
        
        // ...其它略
        
        @GetMapping("/{id}")
        public Item findById(@PathVariable("id") Long id) {
            return itemCache.get(id, key -> itemService.query()
                    .ne("status", 3).eq("id", key)
                    .one()
            );
        }
    
        @GetMapping("/stock/{id}")
        public ItemStock findStockById(@PathVariable("id") Long id) {
            return stockCache.get(id, key -> stockService.getById(key));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
  • 相关阅读:
    基于JavaSwing开发电子琴程序(简约版)(小作业) 课程设计 大作业源码
    Python中的集合
    『Halcon与C#混合编程』009_利用PictureBox实现窗体
    基于MAX-SUM算法的大规模信息系统的协调问题matlab仿真
    文本搜索小程序
    Web3.0与机器学习
    HTML5中的document.visibilityState
    【Javascript】ajax(阿甲克斯)
    每日一题:【Leetcode】561.数组拆分
    世界很大,有人把二维码纹在脖子上
  • 原文地址:https://blog.csdn.net/qq_37726813/article/details/134227963