Redis10、JVM进程缓存、Lua语法入门、多级缓存、缓存同步策略

缓存的作用其实是减轻对数据库的压力,缩短服务响应的时间,从而提高整个并发的能力,多级缓存就是来应对亿级流量并发

传统缓存

多级缓存 

  • 一级缓存-浏览器客户端缓存:浏览器缓存,用户可以通过手机或浏览器访问服务端,得到数据并进行渲染,这里就可以形成第一级缓存--即:浏览器客户端缓存。因为浏览器是可以把服务器返回的静态资源缓存在本地的,这样一来,下次再去访问这些静态资源时候,我们的服务器只要检查一下数据有误变化,没有变化,直接返回304状态码,不用返回数据,浏览器一看304,说明本地有,那就直接把本地渲染,用户就能直接看到了---这样就能减少很多的数据传输,从而提高渲染和响应的速度;事实上,对于页面的请求,90%的请求都是这样一种静态资源请求,响应速度就会大大提升
  • 二级缓存-nginx本地缓存:对于一些非静态的数据就不得不访问服务端了,比如说请求到达了n'ginx服务端--这里就要形成二级缓存,称为:nginx本地缓存,请求来了看看nginx有没有,有的话直接返回
  • 三级缓存-Redis缓存:如果nginx没有,就直接去redis查,形成三级缓存--即:Redis缓存
  • 四级缓存-tomact进程缓存:如果Redis也没有命中,才会到达tomct,形成第四级缓存--即:tomact进程缓存,我们会在服务器的内部,比如说map这样的形式,形成一个进程缓存,这样的缓存保存在tomact本地。当请求到达以后,会先去读取缓存,进程缓存如果命中了,那直接就返回了。从而解决当Redis缓存失效时,直接打到数据库的问题
  • 如果还未到达,才会请求数据库

形成多级缓存解决了两个问题:

  • 1、请求大多数情况下由nginx处理了,不用tomact,tomact压力就大大减轻了,使得tomact不会成为整个系统的瓶颈
  • 2、当Redis缓存失效时,我们还有tomact进程缓存作为缓冲,不会直接打到数据库,避免了一个对数据库的冲击

 此时所有的压力其实都集中到了nginx,我们需要在nginx内部去实现对于redis访问,对Tomacr访问等等这样的业务编写。其实nginx此时,就不在是一个反向代理服务器了,就真正变成了一个web服务了,在里边去编写业务逻辑了。因此:将来nginx就需要部署成集群,才能应对更高的一个并发,而我们还可以准备一个单独的nginx用来做反向代理。也就是说,请求到达了单独的nginx后,它在反向代理到我们多个这样的本地缓存编写业务的服务器

当然了,我们的redis、tomact、mysql也都可以去做集群

以上就是一个多级缓存的完整架构方案了,对我们服务端 来说,只需要关注nginx开始到mysql

需要掌握:

  • 1、在tomact编写进程缓存---JVM进程缓存
  • 2、nginx内部做编程---Lua语言
  • 3、实现多级缓存方案
  • 4、数据库要与缓存之间还要做数据同步--缓存同步策略

一、JVM进程缓存 

1、导入商品案例 -- 老师是虚拟机安装的,我是本地安装,这步暂时不需要,否则端口冲突

 进入到mysql目录后执行下面docker命令

-v 三个是数据的挂载。1V:配置文件目录、2V:日志文件目录、3V:mysql的数据目录

-e 指定mysql的root账号+密码123V

docker run \
 -p 3306:3306 \
 --name mysql \
 -v $PWD/conf:/etc/mysql/conf.d \
 -v $PWD/logs:/logs \
 -v $PWD/data:/var/lib/mysql \
 -e MYSQL_ROOT_PASSWORD=123 \
 --privileged \
 -d \
 mysql:5.7.17

我这里因为本地装了mysql在执行上面的代码会报Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:3306 -> 0.0.0.0:0:

listen tcp 0.0.0.0:3306: bind: address already in use.,端口已被占用 

1.3.修改配置

在/tmp/mysql/conf目录添加一个my.cnf文件,作为mysql的配置文件:

# 创建文件
touch /tmp/mysql/conf/my.cnf

文件的内容如下:

[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000

1.4.重启

配置修改后,必须重启容器:

docker restart mysql

倒入demo工程 

修改配置文件,启动项目

浏览器输入:http://localhost:8081/item/10001 

mac 安装nginx,利用brew命令:Mac下安装Nginx_danielcaisz的博客-CSDN博客_mac 安装nginx

  • 修改配置文件nginx.conf
    • open /opt/homebrew/etc/nginx/..
  • 将html下的文件,放到自己的html下导向的www下
    • open  /opt/homebrew/Cellar/nginx/..
  • 安装完后,nginx -s reload重新启动
  • 输入:http://localhost/item.html?id=10001即可打开页面
  • nginx 启动
  • nginx -s stop 停止
  • 配置文件位置:利用 which nginx 命令查看配置文件位置
  • 打开nginx 的html位置:open /opt/homebrew/Cellar/nginx

检查nginx是否配置成功,浏览器直接输入localhost,即成功,直接找到了nginx的html

浏览器输入:http://localhost/item.html?id=10001,访问某一个商品,带上商品的id信息。

注意:目前这些数据都是假数据,直接写死的,将来这些数据应该是向服务器去做查询

http://localhost/item.html?id=10001请求后端,返回对象item给前端

前端路由页面请求路径:http://localhost/api/item/10001,找不到报错,因为现在还没有配置集群

如下图,当前端发送请求的时候,因为还没有实现这个接口,没有做集群,这里就报错了。

localhost/api/item/10001,并没有端口,是直接请求到了nginx上了,默认是80端口;也就是说,前端请求被nginx拿到了,但是它不能处理,它就把请求代理到后端的nginx业务集群里去,由nginx业务集群完成后续的业务多级缓存处理;

所以还需要在反向代理服务器完成反向代理的配置,即在nginx的conf里编写反向代理-负载均衡到nginx集群的配置

nginx完成反向代理 -nginx集群的配置如下

server 下,location  /api ,监听的是listen 80端口请求,

当发现路径是 api下的,会反向代理->找到负载均衡下的路径,该server就是集群了,我自己的conf需要改成我本地的,图片上的server ip+端口是老师的

2、初始Caffeine;它·是一个专业的进程缓存技术

本地内存,是存储在tomact下了,重启或服务宕机,数据就丢失了

 咖啡因缓存

 案例:

该案例的方法2就是,去缓存取数据,如果有,直接返回,如果没有那就去查数据库

Caffeine 用github的

/*
      基本用法测试
     */
    @Test
    void testBasicOps() {
        // 创建缓存对象,newBuilder 构建一个工厂,去创建build一个对象
        Cache<String, String> cache = Caffeine.newBuilder().build();

        // 存数据
        cache.put("gf", "迪丽热巴");

        // 取数据,不存在则返回null 取数据方法1
        String gf = cache.getIfPresent("gf");
        System.out.println("gf = " + gf);

        // 取数据,不存在则去数据库查询 取数据方法2
        String defaultGF = cache.get("defaultGF", key -> {
            // 这里可以去数据库根据 key查询value,函数式编程,return 结果会存到cache 缓存对象里。
            // 这个function的作用就是 根据一个key 去找 缓存的值,也就是说可以根据这个key去数据库查询数据了
            // 并且可以返回信息
            return "柳岩";
        });
        System.out.println("defaultGF = " + defaultGF);
    }

设置缓存了,如果不清理的话,一定会满的,所以要设置定期驱逐策略

  • 基于容量策略-清理的是最近不怎么使用的
  • 基于时间

注意,两种缓存都不是立即就清理的,需要一点点时间的

咖啡因缓存可以定义一个静态的工具类,将cache暴露出来,可供别人去使用 

基于大小设置驱逐策略:要给清理的时间,不给时间会全部打印,是直接运行完,JVM就退出了,根本就没有机会;也就是不是立即清理的 

void testEvictByNum() throws InterruptedException {
        // 创建缓存对象
        Cache<String, String> cache = Caffeine.newBuilder()
                // 设置缓存大小上限为 1
                .maximumSize(1)
                .build();
        // 存数据
        cache.put("gf1", "柳岩");
        cache.put("gf2", "范冰冰");
        cache.put("gf3", "迪丽热巴");
        // 延迟10ms,给清理线程一点时间,如果不给时间是全都打印了的,还没来得及清理
        Thread.sleep(10L);
        // 获取数据
        System.out.println("gf1: " + cache.getIfPresent("gf1"));
        System.out.println("gf2: " + cache.getIfPresent("gf2"));
        System.out.println("gf3: " + cache.getIfPresent("gf3"));
    }

基于时间设置驱逐缓存

/*
     基于时间设置驱逐策略:
     */
    @Test
    void testEvictByTime() throws InterruptedException {
        // 创建缓存对象
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(Duration.ofSeconds(1)) // 设置缓存有效期为 10 秒
                .build();
        // 存数据
        cache.put("gf", "柳岩");
        // 获取数据
        System.out.println("gf: " + cache.getIfPresent("gf"));
        // 休眠一会儿
        Thread.sleep(1200L);
        System.out.println("gf: " + cache.getIfPresent("gf"));
    }

3、实现进程缓存

新建缓存配置类 

/**
 * 新建缓存配置类,注入spring里
 * 初始化
 */
@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<Long, Item> itemCaffe() {
        // _ 是分读符号
        return Caffeine.newBuilder().initialCapacity(100).maximumSize(10_000).build();
    }

    @Bean
    public Cache<Long, ItemStock> stockCache() {
        return Caffeine.newBuilder().initialCapacity(100).maximumSize(10000).build();
    }
}

将方法名注入控制类,并修改查询的代码

    @Resource
    private Cache<Long, Item> itemCache;
    @Resource
    private Cache<Long, ItemStock> stockCache;

添加本地缓存 

@GetMapping("/{id}")
    public Item findById(@PathVariable("id") Long id){
        // id 其实就是key
        return itemCache.get(id, key ->
            itemService.query()
                    // key 其实就是id,只不过在lambda表达式重新命名一下,不然就和id冲突了
                    .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));

    }

http://localhost:8081/item/10001 浏览器输入;

再第一次执行查询的时候直接查数据库放入本地缓存,以后再执行的话,就直接从本地缓存里找了;

这样我们的JVM进程缓存就实现了

以上我们就实现了Tom的进程缓存

二、Lua语法入门

实现nginx集群业务缓存,利用新的一种语言Lua

1、初识Lua

首先安装LUa

Lua: download 执行如下命令即可安装Lua

我这里分开执行的,一次执行一行语句,最后一行执行权限不足的话就sudo

curl -R -O http://www.lua.org/ftp/lua-5.4.4.tar.gz
tar zxf lua-5.4.4.tar.gz
cd lua-5.4.4
make all test

字符串可以是单引号,也可以是双引号 

打开控制台,利用vim 命令既可,我们 用touch命令 ,vim编写的时候就创建了

2、变量和循环

table既可以是数组,也可以是map键值对,只不过当以数组的时候,key其实就是下标,不过是从1开始的

输入lua 可以直接进入lua的控制台,方便编写一些命令 

变量

  • 字符串也是可以拼接的,lua中字符串拼接是用 .. 来拼接的 
  • local这里代表的是局部变量,就是控制台一行结束后,就没有办法再访问了,不写local就是全局的

循环

  • do代表循环开始,end代表循环结束
  • iparis代表我要解析这个数组
  • 解析数组的时候要形成键值对--index键 value值,区别在于,数组的键其实就是索引,所以用index来表示
  • 数组用ipairs,map用pairs,index 、key相当于key,自己定义,这样更清晰明了,让人知道数组的key是index索引下标,map的key那就是个key,可以理解成字符串

在lua.hello里编写脚本

 打印,我们发现数组的key就是1,2,3即下标索引

3、条件控制、函数

封装函数。可以返回,也可以不返回 return 

案例: 

为了健壮性,比如防止空指针等,添加逻辑判断

then代表大括号开始,end代表结束

条件判断为nil也代表false

案例:自定义一个函数,可以打印table,当参数为nil时,打印错误信息 

  • 如果传了一个nil,那么not nil 就是true;
  • nil就是一个无效的值,if语句可以作为false 

以上,我们学习Lua的目的就是在Nginx里边去编程,这样就可以实现Nginx里边的业务,比如查询Redis,查询tomact等等,当然这些业务逻辑的编写,我们还需要依赖其他组件,这些组件,我们需要利用到一个工具openResty来帮我们实现 

三、多级缓存

1、安装openResty

openResty也不安装了,以后有需要再说吧。

老师的openResty虚拟机地址192.168.150.101:端口是8081

2、OpenResty快速入门

如何在openResty里接收:localhost/api/item/100001 这个请求呢?

 openResty可以实现查询Redis和tomact

如何访问?得加载模块,以.lua结尾的,就是访问lua模块,固定写法

 

写数据到浏览器,ngx.say(),就相当于利用Response将结果接到浏览器,写一个假数据

 写一个假数据

总结配置文件conf,就是编写一个listen 去监听某一个端口,编写一个location 去监听某一个路径,监听路径以后去做反向代理,这里我们监听以后是交给lua文件去处理。然后再lua文件里去编写业务逻辑,

监听路径--可以理解为一个controller

监听到了以后-- lua文件就相当于业务层service

3、请求参数处理

如何从openResty里去获取请求参数呢?

 1、~ 波浪线代表的就是后边跟的是正则表达式匹配,而且注意两边都要有空格()是一组,如果将来正则表达式有多个,即多个参数,那么数组的值就有多个,如果1个,那么数组就有一个,下标从1开始,利用数组来获取路径中的参数

2、返回值是一个table,键值对,key-value形式

案例

 路径占位符获取参数,正则形式,添加波浪线,添加正则

参数会存到一个变量里,回到lua文件 ,从数组中获取参数,下标从1开始 ,注意,前边两个 ..是拼接前边的字符串,后边的两个 .. 是拼接后边的字符串 

 以上即可实现请求参数的获取

4、查询Tomact

实现对商品数据的查询

1、我们的前端请求映射到nginx上,反向代理到openResty上,准备好了

2、tomact查询数据库本地缓存也准备好了

但是openResty接收到请求,要查询数据,从哪里查呢?按照我们的逻辑是先查缓存,缓存没有了再查tomact。但是呢,我们的缓存数据又从何而来呢?得先查了tomact后,把数据存到 缓存里去

所以我们先不做缓存,先实现对tomct的查询,将来再把查到的数据放到缓存里去

openResty集群是在虚拟机上,而tomact实在windows上,ip地址是不一样的

总结:不管虚拟机地址是什么,只要管它的前3位就行了即:这里的是192.168.150,然后只要把最后一位替换成1,那么一定得到的就是我们windows电脑的地址,前提是防火墙得关闭,一定成立

案例:查询分成了两步,因为是两张表,但是页面上要的所有数据,所以还需要组装这两张表的数据

 capture 捕获,就是捕获一个请求,两个参数

  • 第一个参数path,就是请求的路径
  • 第二个参数是table,注意,get/post方式只能二选一

 

 返回值是一个response的对象resp

 而且需要注意,/path这个请求不包含ip+端口,也就是说它没有发送到任何一个地方,而是被nginx自己监听到了,而我们的最终目的是发个tomact,即是发个tomact所在的ip和端口,然后在反向代理到tomact的ip和端口就ok了;

resp.body就是返回响应内容json的字符串

openResty的niginx 的conf需要再添加一个监听 location  /item 即,所有item的请求都会被我监听到。即:凡是向item请求,一定会到达我们的tomact

 以后凡是请求以item开头,请求一定会到达tomact的,因为如下

 这套代码我们会经常用,最少这里就得两次,查商品一次,查库存一次,那就封装成一个函数

 如下图是封装的lua模块,这个模块下的lua都会被加下,所以我们再lualib里定义一个文件,后缀名是lua,则该文件也一定会被加载 ,而lua文件里的函数,也一定会被加载,这就相当于变成了一个通用的工具了,所以叫common.lua

最后 一步,将方法导出。。ngx.exit(404),退出并返回状态码 

 以上主要是学习,如果在openResty里来发起一个http请求

luaLib下的包都是库,lua库,我们编写的方法就可以放进去,引入库的话,如果是在lualib下直接引入文件名即可,如果还有目录就得再加上它自己的目录

如图,我们的common.lua是直接在lualib下的,所以可以之久拿,如果是在ngx下,还需要加上nvx/文件名 

 拼接两个json需要一个工具包,就是上图的cjson.so,反序列化就是转成table

item.lua文件代码如下

这样销量和库存就都有了

 以上代码即可实现从openResty向tomact发送http请求 ,返回页面完成渲染

tomact实现负载均衡

以上代码是有一台tomact的情况,如果有多台tomact就需要做集群,而tomact集群是有轮巡机制的,就是说某一个请求从第一台开始访问,再来就是第二台,再来就是第三台,而tomact缓存是不共享的,每次来都得重新查库,除非轮巡完了所有的tomact,再次从第一台开始才会拿到缓存

而且多台tomact都缓存一个数据,那冗余就太多了。

如果说来了一个请求,第一次查询以后,永远都有缓存,那么必须保证该请求每次都指向同一台tomact服务器,这样才能保证缓存一直生效

就是说1001请求来了以后第一次走的是第一台服务器,那么它永远都走第一台服务器;1002来了轮巡走第二台服务器,那么它永远都走第二台服务器,(当然也可以一样的服务器)我们现在是轮巡肯定做不到,那么就需要修改nginx负载均衡的算法了

添加负载均衡 添加一行代码:hash $request_uri,request_uri就是请求路径,hash就是对请求路径做hash运算,得到hash值后再对tomact服务器的数量取余,那么只要路径不变,服务器的数量不变那么一个请求访问的永远是同一台服务器了,注意,这里数组都是从1开始

修改配置文件如下,添加tomact集群负载均衡 ,tomact进程缓存就是JVM缓存

 以上配置即可实现对tomact负载均衡,并实现缓存永远生效 

 5、Redis缓存预热

openResty应该优先查询Redis,在Redis缓存未命中时,再去查询tomact

定义一个配置类bean,实现一个spring的一个bean,继承的方法就是在项目启动时,就会执行

 

 项目在启动的时候,就会将数据写到redis当中了

  • initializineBean 凡是实现这个接口的,就要实现afterPropertiesSet这个方法,这个方法会在bean创建完(项目启动的时候创建),@Autowird注入成功后,去执行,他就可以在项目启动的时候去执行了。就可以是实现缓存预热效果了
  • ObjectMapper是spring的默认json序列化工具

6、查询Redis缓存

openResty是如何去操作Redis的呢?

导入redis库,因为不是根目录了,需要加层级目录;和导入cjson是一样的道理

三个1000分别是: 建立请求的、发送响应的、接收请求的超时时间,单位毫秒,也就是1秒

成功返回ok,失败返回的是nil err是返回错误信息 

下边的red:get(key)只能实现最简单的字符串查询 

 定义方法

 方法定义好了需要暴漏出来

将查询商品和库存都封装成一个方法 

 

测试可以将tomact停调,也能正常查询,就是走的redis了 

7、Nginx本地查询

如何在openResty里添加一个本地缓存呢?

注意,该字典只能实现一个openResy的多台nginx共享,如果部署了多个openResty ,那么多个openResty之间是不共享的

添加共享词典

item.lua导入共享词典,业务都是在item.lua写的

 

优先查nginx本地缓存,没有去查redis,没有去查tomact,没有去查db

查到了后将缓存写到nginx里 

 

启动发现报错,记录日志需要加

 修改一下,错误日志都需要家ngx.ERR

 修改参数,添加本地缓存过期时间

 再刷新页面,执行查看id=10001查看日志,第一次nginx肯定没有

 再次刷新页面查看日志,就不会再打这个日志了,说明从nginx本地成功拿到了缓存

四、缓存同步策略

实现了多级缓存,大大提高了性能,但是缓存在提高性能的同时,也带来了一致性的问题

比如说如果数据库进行了修改,那么缓存还用原来的就有了不一致性,如何来保证数据库与缓存的一致性呢?

常见的三种方式:

canal 异步通知,它是0侵入的,不用动代码,效率也高 

我们这里不用MQ,canal可以监听数据库的变化

Canal

原理:是基于mysql的主从同步来实现的

什么是mysql主从同步?

  • 主节点在做数据增删改时,就会去记录到二进制日志文件-就是记录的执行业务sql
  • salve会开启一个线程不断去读区该日志文件,读取来之后,放到自己的日志文件里
  • slave会在开启一个线程去不断重放(执行)这些日志文件里的操作(也就是说主节点做了哪些sql,从节点也会相应去执行这些sql)

 

安装Canal 

 

 

重启以后,发现 多了一个mysql-bin.000001,这个二进制文件会越来越多,编号也会越来越大

 

 主从同步还需要给slave设置一个权限

-- 创建一个全新的用户
create user canal@'%' IDENTIFIED by 'canal';
-- 授权
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
-- 刷新权限
FLUSH PRIVILEGES;

position就是一个便宜量,从库需要知道主库偏移量的位置,必须是小于等于,说明需要同步 

 

 以上即开启了主从同步了

 然后运行命令创建Canal容器:

docker run -p 11111:11111 --name canal \
-- canal 集群名称,canal也是可以搭建集群的
-e canal.destinations=heima \
-- mysql的主节点地址,这里没有写ip是因为上面操作的mysql和canal在同一个网络
-- 而docker容器在同一个网络时可以用容器名字互联
-e canal.instance.master.address=mysql:3306  \

-- 以下是做数据同步时用户名 等 这些信息我们在上边mysql执行创建用户时已经创建好了
-e canal.instance.dbUsername=canal  \
-- 密码
-e canal.instance.dbPassword=canal  \
-- 编码
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false  \
-- canal 将来做主从同步时,监听哪个库, 
-- \..* \黑马库下所有的表
-e canal.instance.filter.regex=heima\\..* \
-- 连上黑马的网络,
--network heima \
-d canal/canal-server:v1.1.5

 

命令行输入:docker logs -f canal 查看日志,启动,启动成功

 启动成功后如何知道canal 与 mysql有没有建立连接呢?

通过:docker exec -it canal bash  进入cannl容器内部,可以去查看日志

 canal如何取通知更新呢?

 destinaltion 集群名称

编写监听器,监听,完成redis缓存的增删改

 

 

 JVM缓存的增删改,也放到这里

先写JVM缓存,JVM缓存效率更高 

课程总结 

 openResty集群也是轮巡操作的,多台不共享,可以增加配置,即:同一个id永远访问的是同一台openResty,和tomact集群配置一个道理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值