使用OpenResty、Redis、Caffeine、Canal构成多级缓存

传统缓存问题
在这里插入图片描述
解决方案,在tomcat之前再添加缓存,使得部分请求没有经过tomcat,提升tomcat的并发处理能力。在各个处理环节都添加上缓存,一层一层的进行过滤,减少后面的压力,也能防止部分缓存不可用能有其他缓存抵上

在这里插入图片描述
将性能较高的nginx挡在大量动态请求的第一关,在它内部进行缓存的查取,进行高并发的处理能力,只有缓存未查找到,过滤出的部分请求到达tomcat,因为tomcat性能较弱,这样由nginx挡住大量请求,减轻tomcat压力

初始化准备

设计数据库
在这里插入图片描述
配置nginx

在这里插入图片描述
到此处已完成
在这里插入图片描述

JVM进程缓存

jvm内存缓存即编写java代码,在运行时保存一些数据作为缓存,运行在tomcat中。jvm的缓存形式有很多,例如定义一个变量 map,将产生的数据放入map中,即在运行时,放在JVM进程的缓存

进程缓存与分布式缓存比较

  • 分布式缓存:例redis集群
    • 优点: 存储容量大,可靠性好,可在在集群中共享 :因为分布式缓存不依赖于jvm,可以单独在一个服务器部署,可以使用多种不同的技术接入。当jvm宕机不会影响缓存。高可用、高性能
    • 缺点:因为不是在本地,需要用网络进行连接,就存在网络的时间开销。
    • 使用场景:需要在集群间进行共享,对可靠性要求较高(分布式缓存有集群,可持久化),数据量大(缓存数据量大,单独使用一台服务器进行部署,避免发生影响)
  • 本地进程缓存:例:hashMap
    • 优点:放在本地,使用方便
    • 缺点:不能存过多的数据,JVM部署的服务器有限。宕机即失,无法共享
    • 使用场景:小数据

Caffeine

一款进程缓存类技术,命中率高,性能高,使用类似与map

使用方式:

  1. 导入依赖
<dependency>
   <groupId>com.github.ben-manes.caffeine</groupId>
   <artifactId>caffeine</artifactId>
</dependency>
  1. 创建对象,并可以在创建对象时进行缓存策略配置
    • 基于大小进行配置
    • 基于有效期进行配置
  2. 使用方式:
    • 类似于map,现手动存入数据,使用时使用数据

在这里插入图片描述

例:
在这里插入图片描述
创建对象

在这里插入图片描述
使用缓存
在这里插入图片描述

nginx业务集群缓存

tomcat中 使用 java 执行业务,nginx+lua 执行业务

LUA的基本使用

lua是一种脚本语言,类似于python,使用c语言编写,通常能够嵌入c语言编写的程序。

linux内置lua环境
在这里插入图片描述
在这里插入图片描述

变量的定义

lua的数据类型

数据类型 说明
nil 相当于一个null 在条件语句中,nil=false
boolean true、false
number 类似java的double
string 就是String,可以用 " 也可以用
function 函数,类似于js的函数声明
table 一些map,list,数组

变量定义上与java的一些区别

  • lua使用local进行本地变量的声明,是一种弱类型语言。若不加 类型 为全局变量,类似于 let
  • 字符之间使用 .. 进行拼接
  • 编号从1 开始
  • 输出控制台 print()
    例:
    在这里插入图片描述

数组、集合的遍历

没有大括号用来标志开始、结束。使用do标志开始,使用end标志结束

遍历数组的格式:
在这里插入图片描述

在这里插入图片描述
效果
在这里插入图片描述

map的遍历

在这里插入图片描述

在这里插入图片描述
效果

在这里插入图片描述

条件控制

与java的不同在于 条件判断不能使用 符号 ,只能使用单词。 &->and 、| ->or、 !->not

在这里插入图片描述

创建函数、调用函数
在这里插入图片描述

函数的调用
在这里插入图片描述

例 :定义一个函数,可以打印table,当参数为nil时,打印错误信息
在这里插入图片描述

OpenResty

一款基于于nginx平台与lua脚本的web服务器,是对nginx的一种补充,为nginx提供了大量脚本用于业务处理,使得nginx结合lua脚本 到达 tomcat服务器的效果,但性能远高于tomcat

特点:

  • 具备nginx全部功能
  • 可以使用lua语言进行编写业务逻辑代码
  • 提供了大量的lua库与第三方模块

使得nginx可以向tomcat一样处理web业务

安装过程

  1. 安装OpenResty的依赖开发库 yum install -y pcre-devel openssl-devel gcc --skip-broken
  2. 安装OpenResty仓库 yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
  3. 安装OpenResty yum install -y openresty
  4. 安装opm 管理工具 yum install -y openresty-opm

默认 安装在了 /usr/local/openresty
在这里插入图片描述

OpenResty的本质就是在nginx的基础上进行的补充,因此使用nginx的命令即可操作(自身提供的命令也是调用nginx

例 :
在这里插入图片描述

可以将nginx的命令配置到环境变量,使得命令在任意位置可用

打开系统配置文件
vi /etc/profile

加上
export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${
    
    NGINX_HOME}/sbin:$PATH
重新加载
source /etc/profile

附加:nginx配置文件简化版

worker_processes  1;
error_log  logs/error.log;

events {
    
    
    worker_connections  1024;
}

http {
    
    
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
    
    
        listen       80;
        server_name  localhost;
        location / {
    
    
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
    
    
            root   html;
        }
    }
}

OpenResty业务流程

将win中的nginx用作反向代理,将vm中的openResty用作处理业务
配置方式:win中的nginx监听浏览器请求,将请求代理到vm中的nginx(openResty),vm中的openResty接收请求处理业务

代理的nginx配置
在这里插入图片描述

openResty配置

在这里插入图片描述
到这里只是相当于配置了总的Mapping,还需要配置controller以及controller内部调用的service

进一步补充controller
在这里插入图片描述
由于已经指定了lua文件的位置(默认的根路径是指nginx目录),接下来只需要编写被调用的 item.lua(service)

在这里插入图片描述

进行测试
在这里插入图片描述

整体流程

controller层 vs nginx.conf
在这里插入图片描述
service vs lua脚本
在这里插入图片描述
到达他们的请求都是由浏览器发出,到达nginx代理服务器,然后由代理服务器发出的,可以实现负载均衡

OpenResty获取请求参数:

由于OpenResty是业务的处理,肯定有的业务需要参数。参数由浏览器发出,再经过代理服务器,将参数传给lua脚本进行业务处理
参数五种传递方式

在这里插入图片描述
测试路径传参

  1. 修改nginx.conf
    在这里插入图片描述
  2. 修改最终负责处理业务,接收请求的lua脚本
    在这里插入图片描述
  3. 重启

免去方向代理,直接访问路径测试更方便
在这里插入图片描述

发起请求到tomcat

要注意:两个主机如果不在一个服务器可能有防火墙问题。这里由于我的OpenResty是在vm中,而tomcat是在win中,为了方便,将tomcat修改端口运行在vm中。
将项目进行打包,然后本次测试通过后,放入vm中运行,这样就不会有防火墙问题
但是:我的vm中的jdk8版本运行不了在win中9打包的jar,更新或使用docker又过于麻烦,所以采用在win运行,关闭防火墙的方式
在这里插入图片描述
在vm中使用 curl 地址的方式进行验证
在这里插入图片描述

前面只是关闭了防火墙,并未进行修改,下一步需要使用openResty去访问tomcat,即修改lua脚本(OpenResty只负责映射反向代理来的请求,然后交给lua脚本执行业务)
例:

在这里插入图片描述

  • 编写案例前的补充
    由于使用的OpenResty它的最大特点就是在nginx基础上封装了许多API供脚本使用
    例:
    在这里插入图片描述

    脚本的请求最终会发给自身的nginx,再将nginx的请求进行反向代理发送给对用服务器

    在这里插入图片描述
    整体流程
    在这里插入图片描述
    由于频繁需要发起请求,执行固定的代码,可以将脚本中有关请求的代码进行封装,然后引用

    进行封装的代码可以在lualib目录下创建一个包,包下创建每一功能封装成一个lua文件,在文件中将封装后提供的Api进行暴露
    在这里插入图片描述

    在脚本中引用
    在这里插入图片描述

  • 开始完成案例
    由于发送的请求是将参数在路径中进行传递(/api/item/10001),要想获取请求参数,要将openResty中的映射路径进行修改

由于lua脚本中向外发送的api也需要被nginx进行捕获,所以配置反向代理部分,将脚本中的请求送达到tomcat服务器
在这里插入图片描述

编辑脚本,引用api模块,获取参数,然后发送请求,解析参数

![在这里插入图片描述](https://img-blog.csdnimg.cn/d8053a14e3f44a3facb1223734b35ef7.png

然后重启openResty即可通过浏览器发送请求,通过反向代理找到OpenResty,在由OpenResty通过lua脚本访问tomcat即可获取到数据

将tomcat升级为集群

在这里插入图片描述

问题:由于采用集群,如果是负载均衡的方式,那么由于每次请求产生的JVM进程缓存在本地,而请求下一次将访问另一台机器。由于JVM的进程缓存不共享导致缓存命中率特别低。
解决思路:每一个请求只访问一台主机,在JVM的进程缓存才可以重复利用。在nginx的反向代理中,对各个集群不采用负载均衡的方式,而是获取请求的hash值,除以集群的个数找到对应的服务器。由于hash值永远是固定的,且不同的请求对应的hash值不同,这样就使得每个请求可能访问不同的主机,但相同的请求一定会访问相同的主机

做法: 只需要在定义集群时添加添加 hash $request_uri; 即可
在这里插入图片描述

这样就可以使得lua脚本查询的是tomcat集群,且相同的请求会访问到同一台服务器,使得进程缓存有效

连接redis

Redis的冷启动
第一次查询需要从数据库中,将查询结果在放入缓存
Redis的热启动

事先判断哪些数据会被频繁访问,在启动时就将他们放入redis缓存中。这样即使是第一次访问,也可以在缓存中查找到,提升效率

在这里插入图片描述
在lua脚本中连接redis,同tomcat一样也需要引入openResty提供的依赖库,这里需要引入'resty.redis 模块,获取redis对象,然后创建连接池。
例:

在这里插入图片描述
最后在脚本下面 read_redis("127.0.0.1",6379,"item:id:10001")
测试失败,通过查看日志可知错误是redis有密码,而脚本中没有指定。可以在其脚本中加上 red:auth(password)或者直接先关闭掉redis的密码
最后直接访问OpenResty业务集群
与tomcat不同的是,脚本中发起的是连接,而不是路径请求。因此无需在nginx.conf中配置反向代理
在这里插入图片描述

为了使用方便,同样将其封装成一个脚本,只暴露连接使用的函数。可以与之前的common脚本放在一起,这个脚本就向外提供两个方法,分别是发起tomcat请求查询redis缓存
可以引入封装的函数,通过条件判断语句,使得先去查询redis,若结果为空,在去查询tomcat达到二级缓存的效果,这样就将redis的查询挪到了tomcat之前,减少tomcat的压力和并发量

  1. 封装common脚本
-- 连接redis
local redis = require('resty.redis')
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)

local function close_redis(red)
    local pool_max_idle_time = 10000 
    local pool_size = 100 
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
    if not ok then
        ngx.log(ngx.ERR, "放入redis连接池失败: ", err)
    end
end

local function read_redis(ip, port, key)
    local ok, err = red:connect(ip, port)
    if not ok then
        ngx.log(ngx.ERR, "连接redis失败 : ", err)
        return nil
    end
    local resp, err = red:get(key)
    if not resp then
        ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
    end
    if resp == ngx.null then
        resp = nil
        ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
    end
    close_redis(red)
    return resp
end

-- 向tomcat发起请求

local function read_http(path, params)
    local resp = ngx.location.capture(path,{
    
    
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        ngx.log(ngx.ERR, "http查询失败, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 将两个功能对外暴露
local _M = {
    
      
    read_http = read_http,
    read_redis = read_redis
}  
return _M
  1. 在lua目录中的业务脚本中引入封装的方法,进行数据获取
    将获取数据功能再次封装成一个方法
    在这里插入图片描述
    真正执行的业务
    在这里插入图片描述
OpenResty本地缓存

OpenResty同样提供了方法库(shard dict功能)使得通过脚本可以使用本地缓存(缓存集群可共享)
本地缓存使用只需要 获取 ngx.shared.item_cache的对象,可以配置其大小,结构类似于 Redis的K_V键值对,且在指定键值对时要设置过期时间 ,0代表永不过期
使用例子:

  1. 在缓存中开启共享缓存

在这里插入图片描述
2. 在脚本中获取缓存对象即可存放入、取出在这里插入图片描述

有了OpenResty本地缓存、redis缓存、tomcat线程缓存,将他们组合一起即可实现多级缓存。浏览器发送的请求被反向代理到OpenResty,然后OpenResty先查询本地是否有,若没有再去查询redis,若没有再去查询JVM线程缓存,若没有再去查询数据库。请求经历了三层缓存,大大较少了到达数据库的请求,使得响应更快,系统并发量更高
在这里插入图片描述
对应的业务脚本

-- 导入自己编写的common模块  其中有发起请求,查询redis 两种功能方法
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入cjson库
local cjson = require('cjson')
-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache

-- 封装查询函数
function read_data(key, expire, path, params)
    -- 查询本地缓存
    local val = item_cache:get(key)
    if not val then
        ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询Redis, key: ", key)
        -- 查询redis
        val = read_redis("127.0.0.1", 6379, key)
        -- 判断查询结果
        if not val then
            ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)
            -- redis查询失败,去查询http
            val = read_http(path, params)
        end
    end
    -- 查询成功,把数据写入本地缓存
    item_cache:set(key, val, expire)
    -- 返回数据
    return val
end

-- 获取路径参数
local id = ngx.var[1]

-- 查询商品信息
local itemJSON = read_data("item:id:" .. id, 1800,  "/item/" .. id, nil)
-- 查询库存信息
local stockJSON = read_data("item:stock:id:" .. id, 60, "/item/stock/" .. id, nil)

-- JSON转化为lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item))

本地缓存修改不方便,应方一些对一致性不敏感且不经常修改的数据

缓存同步

所有缓存在提升性能的同时,都面临一个问题:数据一致性
缓存常见三种同步策略

  • 缓存过期
    添加缓存时设置有效期,使得缓存过期后,再次查询时进行写入,达到一个更新的效果
    • 缺点:在有效期间内如果发生了数据修改,而缓存却不能及时更新,造成了 时长做多在有效期内 的数据不一致问题
    • 适用场景:对数据一致性要求不那么严格,更新频率较低
  • 同步双写
    在修改数据时,也要同步将缓存进行修改,这样能够保证缓存与数据库的强一致性
    • 缺点:修改缓存与其他业务进行紧耦合,较为麻烦。
  • 异步通知
    当有数据发生修改时,将修改的消息发送到消息队列中,使得另一端负责缓存修改的业务接收到,实现异步缓存修改。
    • 缺点:数据可能出现短暂的不一致,可能有少量代码侵入主业务
      使用mq方式

在这里插入图片描述
使用cannal方式
在这里插入图片描述

Canal的安装及配置

Canal是阿里开源的一款基于Mysql binlog文件的订阅消费组件。相比于mq,它能够自动监听mysql数据的变化,而不用编写业务手动放入队列。监听数据变化后能自动 信息发送至队列,在消费者端,只需要编写代码获取队列中修改数据的信息,然后更新缓存
能够做到代码零侵入

监听数据库原理
在这里插入图片描述
由于mysql的主从复制原理就是,从节点通过监听biglog文件变化来实时更新自己的数据,达到主从数据一致的效果
而Canal伪装成一个slave,就可以具有从节点的功能,获取数据的实时变化,然后放入通知队列

Canal的安装配置:这里使用docker

  1. 根据Canal的实现原理,要想能够发挥作用,最重要的就是mysql要开启主从同步

开启mysql主从同步的master端
找到mysql的 conf/my.cnf文件
在这里插入图片描述
在这里插入图片描述
创建用于同步数据的账户
在这里插入图片描述

create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
FLUSH PRIVILEGES;

重新启动服务器,然后运行 show master status;命令
在这里插入图片描述
2. 创建一个docker网络,将mysql放入 docker network create xxxx(自定义的网络名)
在这里插入图片描述
3. 安装canal
在这里插入图片描述

下载出镜像后,运行命令,将canal与要监控的mysql绑定
在这里插入图片描述

docker run -p 11111:11111 --name canal \
-e canal.destinations=canal-test \
-e canal.instance.master.address=mysql:3307 \
-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  \
-e canal.instance.filter.regex=canal-test\\..* \
--network test-canal-net \
-d canal/canal-server:v1.1.5

安装完成

Canal的消费者端使用

由于官方的较为麻烦,使用第三方的客户端依赖

<dependency>
    <groupId>top.javatool</groupId>
    <artifactId>canal-spring-boot-starter</artifactId>
    <version>1.2.1-RELEASE</version>
</dependency>

进行配置
在这里插入图片描述
创建一个包 名为canal,在创建一个类实现 接口 EntryHandler<pojo类>,专门用于监听数据变化后进行处理

在这里插入图片描述
Canal常使用三个注解

  • @Id: 指定属性对应的字段是表的id
  • @Transient:该属性没有与表对应的字段
  • @Column(name="xxx"): 指明该属性对应表的名字

在这里插入图片描述

例:当数据库中的表发生变化时,会自动执行

在这里插入图片描述
这里只对redis与JVM线程的缓存的进行更新,因为tomcat中无法直接操作OpenResty中的缓存。OpenResty中放置数据一致性较低的缓存。

猜你喜欢

转载自blog.csdn.net/m0_52889702/article/details/127782141