• 多级缓存自用


    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本地缓存、Redis、Tomcat的查询

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

    其中Nginx编程则会用到OpenResty框架结合Lua这样的语言。

    2.JVM进程缓存

    为了演示多级缓存的案例,我们先准备一个商品查询的业务。

    2.1.初识Caffeine

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

    • 分布式缓存,例如Redis:

      • 优点:存储容量更大、可靠性更好、可以在集群间共享

      • 缺点:访问缓存有网络开销

      • 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享

    • 进程本地缓存,例如HashMap、GuavaCache:

      • 优点:读取本地内存,没有网络开销,速度更快

      • 缺点:存储容量有限、可靠性较低、无法共享

      • 场景:性能要求较高,缓存数据量较小

    我们今天会利用Caffeine框架来实现JVM进程缓存。

    Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。

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

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

    缓存使用的基本API:

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

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

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

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

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

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

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

    2.2.实现JVM进程缓存

    2.2.1.需求

    利用Caffeine实现下列需求:

    • 给根据id查询商品的业务添加缓存,缓存未命中时查询数据库

    • 给根据id查询商品库存的业务添加缓存,缓存未命中时查询数据库

    • 缓存初始大小为100

    • 缓存上限为10000

    2.2.2.实现

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

    1. @Configuration
    2. public class CaffeineConfig {
    3. @Bean
    4. public Cache itemCache(){
    5. return Caffeine.newBuilder()
    6. .initialCapacity(100)
    7. .maximumSize(10_000)
    8. .build();
    9. }
    10. @Bean
    11. public Cache stockCache(){
    12. return Caffeine.newBuilder()
    13. .initialCapacity(100)
    14. .maximumSize(10_000)
    15. .build();
    16. }
    17. }

    1. @RestController
    2. @RequestMapping("item")
    3. public class ItemController {
    4. @Autowired
    5. private IItemService itemService;
    6. @Autowired
    7. private IItemStockService stockService;
    8. @Autowired
    9. private Cache itemCache;
    10. @Autowired
    11. private Cache stockCache;
    12. // ...其它略
    13. @GetMapping("/{id}")
    14. public Item findById(@PathVariable("id") Long id) {
    15. return itemCache.get(id, key -> itemService.query()
    16. .ne("status", 3).eq("id", key)
    17. .one()
    18. );
    19. }
    20. @GetMapping("/stock/{id}")
    21. public ItemStock findStockById(@PathVariable("id") Long id) {
    22. return stockCache.get(id, key -> stockService.getById(key));
    23. }
    24. }

    3.Lua语法入门

    Nginx编程需要用到Lua语言,因此我们必须先入门Lua的基本语法。

    3.1.初识Lua

    Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。官网:The Programming Language Lua

    Lua经常嵌入到C语言开发的程序中,例如游戏开发、游戏插件等。

    Nginx本身也是C语言开发,因此也允许基于Lua做拓展。

    3.1.HelloWorld

    CentOS7默认已经安装了Lua语言环境,所以可以直接运行Lua代码。

    1)在Linux虚拟机的任意目录下,新建一个hello.lua文件

    2)添加下面的内容

    print("Hello World!")  

    3)运行

    任意地方输出lua便可进入lua控制台

    3.2.变量和循环

    学习任何语言必然离不开变量,而变量的声明必须先知道数据的类型。

    3.2.1.Lua的数据类型

    Lua中支持的常见数据类型包括:

    另外,Lua提供了type()函数来判断一个变量的数据类型

    3.2.2.声明变量

    Lua声明变量的时候无需指定数据类型,而是用local来声明变量为局部变量

    -- 声明字符串,可以用单引号或双引号,

    local str = 'hello'

    -- 字符串拼接可以使用 ..

    local str2 = 'hello' .. 'world'

    -- 声明数字

    local num = 21

    -- 声明布尔类型

    local flag = true

    Lua中的table类型既可以作为数组,又可以作为Java中的map来使用。数组就是特殊的table,key是数组角标而已:

    -- 声明数组 ,key为角标的 table

    local arr = {'java', 'python', 'lua'}

    -- 声明table,类似java的map

    local map = {name='Jack', age=21}

    Lua中的数组角标是从1开始,访问的时候与Java中类似:

    -- 访问数组,lua数组的角标从1开始

    Lua中的table可以用key来访问:

    -- 访问table
    print(map['name'])
    print(map.name)

    3.2.3.循环

    对于table,我们可以利用for循环来遍历。不过数组和普通table遍历略有差异。

    遍历数组:

    -- 声明数组 key为索引的 table          数组——ipairs

    local arr = {'java', 'python', 'lua'}

    -- 遍历数组

    for index,value in ipairs(arr) do

            print(index, value)

    end

    遍历普通table

    -- 声明map,也就是table          map-pairs
    local map = {name=

  • 相关阅读:
    Git回滚代码到某个commit(图文讲解 仅需2步)
    HI3559AV100 GPIO配置和操作(二)
    SpringBoot结合Vue.js+axios框架实现增删改查功能+网页端实时显示数据库数据(包括删除多条数据)
    一大突破!清华大学研制出首颗忆阻器存算一体芯片
    存储模型:大端和小端
    消息中间件RabbitMQ
    css 渐变下划线实现 移入移出 动画
    泛型的小结
    华为云云耀云服务器L实例评测|在Docker环境下部署PrestaShop电子商务系统
    多年锤炼,迈向Kata 3.0 !走进开箱即用的安全容器体验之旅
  • 原文地址:https://blog.csdn.net/weixin_50458070/article/details/134697520