微服务高级篇(四):多级缓存:Nginx本地缓存 --- Redis缓存 --- 进程缓存

文章目录

  • 一、多级缓存概念
  • 二、准备工作【导入案例,并搭建Nginx反向代理】
    • 2.1 导入商品案例
      • 2.1.1 安装MySQL
      • 2.1.2 导入SQL
      • 2.1.3 导入Demo工程
      • 2.1.4 启动
      • 2.1.5 导入商品查询页面
  • 三、JVM进程缓存【第三级缓存】
    • 3.1 本地进程缓存与分布式缓存的区别
    • 3.2 本地进程缓存:Caffeine
    • 3.3 案例
  • 四、Nginx编程:Lua语法
    • 4.1 初识Lua
    • 4.2 变量
    • 4.3 循环
    • 4.4 条件控制和函数
  • 五、多级缓存
    • 5.1 安装OpenResty
      • 5.1.1 安装
      • 5.1.2 启动和运行
    • 5.2 OpenResty快速入门
      • 上述的总结流程
    • 5.3 请求参数处理
    • 5.4 查询Tomcat
      • 5.4.1 nginx发送http请求
      • 5.4.2 nginx发出请求后,反向代理给tomcat
      • 5.4.3 编写item.lua业务,获取从本地tomcat响应请求结果
    • 5.5 Tomcat集群的负载均衡
    • 5.6 Redis的冷启动与缓存预热
    • 5.7 查询Rdeis缓存【第二级缓存】
    • 5.8 Nginx本地缓存【第一级缓存】
  • 六、缓存同步策略
    • 6.1 常见缓存策略
    • 6.2 安装Canal
      • 6.2.1 开启MySQL主从
      • 6.2.2 安装Canal
    • 6.3 监听Canal
  • 八、多级缓存总结
  • 九、额外说明:cpolar内网穿透(将私网暴露成公网供外部使用)


一、多级缓存概念

在这里插入图片描述
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能。
用作缓存的Nginx是业务Nginx,需要部署为集群,再有专门的Nginx用来做反向代理。

在这里插入图片描述

二、准备工作【导入案例,并搭建Nginx反向代理】

本章实现橙色部分
在这里插入图片描述

2.1 导入商品案例

2.1.1 安装MySQL

后期做数据同步需要用到MySQL的主从功能,所以需要大家在虚拟机中,利用Docker来运行一个MySQL容器。

为了方便后期配置MySQL,我们先准备两个目录,用于挂载容器的数据和配置文件目录:

# 进入/tmp目录
cd /tmp
# 创建文件夹
mkdir mysql
# 进入mysql目录
cd mysql

进入mysql目录后,执行下面的Docker命令:

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=123456 \--privileged \-d \mysql:5.7.25

在/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

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

docker restart mysql

2.1.2 导入SQL

接下来,利用Navicat客户端连接MySQL,然后导入课前资料提供的sql文件:item.sql
在这里插入图片描述

其中包含两张表:

  • tb_item:商品表,包含商品的基本信息
  • tb_item_stock:商品库存表,包含商品的库存信息

之所以将库存分离出来,是因为库存是更新比较频繁的信息,写操作较多。而其他信息修改的频率非常低。

2.1.3 导入Demo工程

下面导入课前资料提供的工程:item-service

项目结构如图所示:

其中的业务包括:

  • 分页查询商品
  • 新增商品
  • 修改商品
  • 修改库存
  • 删除商品
  • 根据id查询商品
  • 根据id查询库存

业务全部使用mybatis-plus来实现,如有需要请自行修改业务逻辑。

  1. 分页查询商品

com.heima.item.web包的ItemController中可以看到接口定义:

  1. 新增商品

com.heima.item.web包的ItemController中可以看到接口定义:

  1. 修改商品

com.heima.item.web包的ItemController中可以看到接口定义:

  1. 修改库存

com.heima.item.web包的ItemController中可以看到接口定义:

  1. 删除商品

com.heima.item.web包的ItemController中可以看到接口定义:

这里是采用了逻辑删除,将商品状态修改为3

  1. 根据id查询商品

com.heima.item.web包的ItemController中可以看到接口定义:

这里只返回了商品信息,不包含库存

  1. 根据id查询库存

com.heima.item.web包的ItemController中可以看到接口定义:

2.1.4 启动

注意修改application.yml文件中配置的mysql地址信息:

需要修改为自己的虚拟机地址信息、还有账号和密码。
在这里插入图片描述

修改后,启动服务,访问:http://localhost:8081/item/10001即可查询数据

2.1.5 导入商品查询页面

商品查询是购物页面,与商品管理的页面是分离的。

部署方式如图:

在这里插入图片描述

我们需要准备一个反向代理的nginx服务器,如上图红框所示,将静态的商品页面放到nginx目录中。

页面需要的数据通过ajax向服务端(nginx业务集群)查询。

  1. 运行nginx服务

这里我已经给大家准备好了nginx反向代理服务器和静态资源。

我们找到课前资料的nginx目录:nginx-1.18.0

将其拷贝到一个非中文目录下,运行这个nginx服务。

运行命令:

start nginx.exe

然后访问 http://localhost/item.html?id=10001即可:

在这里插入图片描述

  1. 反向代理

现在,页面是假数据展示的。我们需要向服务器发送ajax请求,查询商品数据。

打开控制台,可以看到页面有发起ajax查询数据:

在这里插入图片描述

而这个请求地址同样是80端口,所以被当前的nginx反向代理了。

查看nginx的conf目录下的nginx.conf文件:

在这里插入图片描述

其中的关键配置如下:

在这里插入图片描述

完整内容如下:


#user  nobody;
worker_processes  1;events {worker_connections  1024;
}http {include       mime.types;default_type  application/octet-stream;sendfile        on;#tcp_nopush     on;keepalive_timeout  65;upstream nginx-cluster{server 192.168.150.101:8081;}server {listen       80;server_name  localhost;location /api {proxy_pass http://nginx-cluster;}location / {root   html;index  index.html index.htm;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}}
}

三、JVM进程缓存【第三级缓存】

本章实现红色框部分
在这里插入图片描述

3.1 本地进程缓存与分布式缓存的区别

在这里插入图片描述

3.2 本地进程缓存:Caffeine

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

第一步:引入依赖

        <dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId></dependency>

第二步:基本用法存/取数据

    /**基本用法测试*/@Testvoid testBasicOps() {// 1.创建缓存对象Cache<String, String> cache = Caffeine.newBuilder().build();// 2.存数据cache.put("gf", "迪丽热巴");// 3.取数据,不存在则返回nullString gf = cache.getIfPresent("gf");System.out.println("gf = " + gf);// 4.取数据,不存在则去数据库查询String defaultGF = cache.get("defaultGF", key -> {// 这里可以去数据库根据 key查询valuereturn "柳岩";});System.out.println("defaultGF = " + defaultGF);}

第三步:缓存驱逐策略

在这里插入图片描述

    /**基于大小设置驱逐策略:*/@Testvoid 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"));}结果:
gf1: null
gf2: null
gf3: 迪丽热巴/**基于时间设置驱逐策略:*/@Testvoid testEvictByTime() throws InterruptedException {// 创建缓存对象Cache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(1)) // 设置缓存有效期为 1 秒.build();// 存数据cache.put("gf", "柳岩");// 获取数据System.out.println("gf: " + cache.getIfPresent("gf"));// 休眠一会儿Thread.sleep(1200L);System.out.println("gf: " + cache.getIfPresent("gf"));}
}
结果:
gf: 柳岩
gf: null

3.3 案例

在这里插入图片描述
第一步:新建一个Config类

/*** 初始化本地缓存Caffeine*/
@Configuration
public class CaffeineConfig {/*** item商品的缓存:* 缓存初始大小100* 缓存上限10000*/@Beanpublic Cache<Long, Item> itemCache(){return Caffeine.newBuilder().initialCapacity(100).maximumSize(10_000).build();}/*** stock库存的缓存:* 缓存初始大小100* 缓存上限10000*/@Beanpublic Cache<Long, ItemStock> stockCache(){return Caffeine.newBuilder().initialCapacity(100).maximumSize(10_000).build();}
}

第二步:编写业务代码

@RestController
@RequestMapping("item")
public class ItemController {@Autowiredprivate IItemService itemService;@Autowiredprivate IItemStockService stockService;@Autowiredprivate Cache<Long, Item> itemCache;@Autowiredprivate Cache<Long, ItemStock> stockCache;@GetMapping("/{id}")public Item findById(@PathVariable("id") Long id) {// 优先根据item缓存的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) {// 优先根据stock缓存的id查,没有再去去数据库查return stockCache.get(id, key -> stockService.getById(id));}
}

第三步:启动服务,第一次查询 http://localhost:8081/item/10001 ,控制台会出现查询语句日志,再次查询,并没有查询语句日志,说明数据已经到缓存中了。

四、Nginx编程:Lua语法

4.1 初识Lua

在这里插入图片描述
CenOS自带Loa,因此不用安装

在这里插入图片描述
可以使用lua命令直接打开编辑

4.2 变量

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

上面的local表示局部变量

字符串拼接是用..,例如:local str = 'hello ' .. 'world!'

4.3 循环

在这里插入图片描述

4.4 条件控制和函数

在这里插入图片描述

在这里插入图片描述

local arr = {'java','lua'}
local arr1local function printArr(arr)if (not arr) thenprint('数组不能为空')return nilendfor i,val in ipairs(arr)doprint(val)end
endprintArr(arr)
printArr(arr1)

输出:

[root@iZ2ze1r1nnqykr8zfme6cjZ tmp]# vi hello.lua
[root@iZ2ze1r1nnqykr8zfme6cjZ tmp]# lua hello.lua
java
lua
数组不能为空

五、多级缓存

本章实现红色框部分
在这里插入图片描述

5.1 安装OpenResty

在这里插入图片描述

5.1.1 安装

首先你的Linux虚拟机必须联网

  1. 安装开发库

首先要安装OpenResty的依赖开发库,执行命令:

yum install -y pcre-devel openssl-devel gcc --skip-broken
  1. 安装OpenResty仓库

你可以在你的 CentOS 系统中添加 openresty 仓库,这样就可以便于未来安装或更新我们的软件包(通过 yum check-update 命令)。运行下面的命令就可以添加我们的仓库:

yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

如果提示说命令不存在,则运行:

yum install -y yum-utils 

然后再重复上面的命令

  1. 安装OpenResty

然后就可以像下面这样安装软件包,比如 openresty

yum install -y openresty
  1. 安装opm工具

opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。

如果你想安装命令行工具 opm,那么可以像下面这样安装 openresty-opm 包:

yum install -y openresty-opm
  1. 目录结构

默认情况下,OpenResty安装的目录是:/usr/local/openresty

在这里插入图片描述

看到里面的nginx目录了吗,OpenResty就是在Nginx基础上集成了一些Lua模块。

  1. 配置nginx的环境变量

打开配置文件:

vi /etc/profile

在最下面加入两行:

export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH

NGINX_HOME:后面是OpenResty安装目录下的nginx的目录

然后让配置生效:

source /etc/profile

5.1.2 启动和运行

OpenResty底层是基于Nginx的,查看OpenResty目录的nginx目录,结构与windows中安装的nginx基本一致:

所以运行方式与nginx基本一致:

# 启动nginx
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stop

nginx的默认配置文件注释太多,影响后续我们的编辑,这里将nginx.conf中的注释部分删除,保留有效部分。

修改/usr/local/openresty/nginx/conf/nginx.conf文件,内容如下:


#user  nobody;
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       8081;server_name  localhost;location / {root   html;index  index.html index.htm;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}}
}

在Linux的控制台输入命令以启动nginx:

nginx

然后通过ps -ef | grep nginx查看:

在这里插入图片描述

然后访问页面:http://192.168.150.101:8081,注意ip地址替换为你自己的虚拟机IP:

在这里插入图片描述

5.2 OpenResty快速入门

在这里插入图片描述
第一步:修改nginx.conf文件

  1. 加载OpenResty的lua模块:
#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块     
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";  
  1. 在nginx.conf的server下面,添加对/api/item这个路径的监听
location /api/item{#响应类型,这里返回jsondefault_type application/json;#响应数据由 lua/item.lua这个文件来决定content_by_lua_file lua/item.lua;
}

nginx.conf

#user  nobody;
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;# lua 模块lua_package_path "/usr/local/openresty/lualib/?.lua;;";# c模块     lua_package_cpath "/usr/local/openresty/lualib/?.so;;";server {listen       8081;server_name  localhost;# 监听反向代理来的请求:/api/itemlocation /api/item{# 响应类型,这里返回jsondefault_type application/json;# 响应数据由 lua/item.lua这个文件来决定content_by_lua_file lua/item.lua;}location / {root   html;index  index.html index.htm;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}}
}

第二步:编写item.lua的代码

  1. 在nginx目录下创建一个lua/item.lua文件
[root@iZ2ze1r1nnqykr8zfme6cjZ openresty]# cd /usr/local/openresty/nginx
[root@iZ2ze1r1nnqykr8zfme6cjZ nginx]# mkdir lua
[root@iZ2ze1r1nnqykr8zfme6cjZ nginx]# touch lua/item.lua
  1. 编写业务内容
-- 返回假数据,这里的ngx.say()函数,就是写数据到Response中
ngx.say('{"id":10001,"name":"SALSA AIR","title":"RIMOWA 2666寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":17900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')
  1. 重新加载配置
nginx -s reload
  1. 刷新http://localhost/item.html?id=10001查看页面数据已经修改,如下:

在这里插入图片描述

如果不能成功,检查本机和虚拟机的配置nginx配置文件,然后重启启动nginx,命令为:start nginx【windows】或者nginx【CentOS】

上述的总结流程

在这里插入图片描述

5.3 请求参数处理

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

修改nginx.conf:注意location ~ /api/item/(\d+)

#user  nobody;
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;# lua 模块lua_package_path "/usr/local/openresty/lualib/?.lua;;";# c模块     lua_package_cpath "/usr/local/openresty/lualib/?.so;;";server {listen       8081;server_name  localhost;# 监听反向代理来的请求:/api/itemlocation ~ /api/item/(\d+){# 响应类型,这里返回jsondefault_type application/json;# 响应数据由 lua/item.lua这个文件来决定content_by_lua_file lua/item.lua;}location / {root   html;index  index.html index.htm;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}}
}

修改item.lua解释:id = ngx.var[1]获取http://localhost/item.html?id=10003的参数,"id":' .. id ..'将10003使用..拼接并返回给页面

-- 获取路径参数
local id = ngx.var[1]
-- 返回结果
ngx.say('{"id":' .. id ..',"name":"SALSA AIR","title":"RIMOWA 2666寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":17900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')

在这里插入图片描述

5.4 查询Tomcat

本节实现红色部分
在这里插入图片描述
在这里插入图片描述

5.4.1 nginx发送http请求

在这里插入图片描述

我们可以把http查询的请求封装为一个函数,放到0penResty函数库中,方便后期使用

  1. 在/usr/local/openresty/lualib目录下创建common.lua文件:
    vi /usr/local/openresty/lualib/common.lua

  2. 在common.lua中封装http查询的函数,发起http请求
    common.lua

-- 封装函数,发送http请求,并解析响应
local function read_http(path, params)-- 发送http请求local resp = ngx.location.capture(path,{method = ngx.HTTP_GET,args = params,})if not resp then-- 记录错误信息,返回404ngx.log(ngx.ERR, "http查询失败,路径为: ", path , ", args: ", args)ngx.exit(404)endreturn resp.body
end
-- 将方法导出
local _M = {  read_http = read_http
}  
return _M

5.4.2 nginx发出请求后,反向代理给tomcat

在nginx.conf的server下增加,记得此IP地址要是你电脑主机的IP地址

		# 反向代理给tomcatlocation /item {proxy_pass http://192.168.150.1:8081;}

一定要注意:如果你的主机IP与服务器IP不属于同一个局域网,那么nginx无法访问你的地址,因为你的地址是内网地址。因此要做cpolar内网穿透,并将上述IP地址改成经内网穿透够的外网地址

5.4.3 编写item.lua业务,获取从本地tomcat响应请求结果

将从tomcat查询到的数据进行拼接,然后序列化返回给前端页面

-- 案例3
-- 导入common函数库【自己编写的】,common.lua在/usr/local/openresty/lualib目录下
local common = require('common')
local read_http = common.read_http
-- 导入cjson解析库,也是在/usr/local/openresty/lualib目录下,默认就有此文件
local cjson = require('cjson')-- 获取路径参数
local id = ngx.var[1]
-- 查询商品信息
local itemJSON = read_http("/item/" .. id, nil)
-- 查询库存信息
local stockJSON = read_http("/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))

输入http://localhost/item.html?id=10001,可以看到从本地tomcat查到数据并显示了

在这里插入图片描述

5.5 Tomcat集群的负载均衡

在这里插入图片描述

修改nginx.conf添加tomcat集群,并使用hash $request_uri哈希运算,保证每次查询同一个值到同一个tomcat中访问。

#user  nobody;
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;# lua 模块lua_package_path "/usr/local/openresty/lualib/?.lua;;";# c模块     lua_package_cpath "/usr/local/openresty/lualib/?.so;;";# 定义tomcat集群upstream tomcat-cluster {hash $request_uri;server 198.168.101.1:8081;server 198.168.101.1:8082;}server {listen       8081;server_name  localhost;# 反向代理给tomcat集群,tomcat-cluster在上面定义location /item {proxy_pass http://tomcat-cluster;}# 监听反向代理来的请求:/api/itemlocation ~ /api/item/(\d+){# 响应类型,这里返回jsondefault_type application/json;# 响应数据由 lua/item.lua这个文件来决定content_by_lua_file lua/item.lua;}location / {root   html;index  index.html index.htm;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}}
}

5.6 Redis的冷启动与缓存预热

冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。

缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。

  1. 利用Docker安装Redis
docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes
  1. 在item-service服务中引入Redis依赖
        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
  1. 配置Redis地址
spring:redis:host: 192.168.150.101
  1. 编写初始化类
@Component
public class RedisHandler implements InitializingBean {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate IItemService itemService;@Autowiredprivate IItemStockService stockService;// jason处理工具private static final ObjectMapper MAPPER = new ObjectMapper();@Overridepublic void afterPropertiesSet() throws Exception {// 初始化缓存// 1.查询商品信息List<Item> itemList = itemService.list();// 2.放入缓存for (Item item : itemList) {// 2.1.item序列化为JSONString json = MAPPER.writeValueAsString(item);// 2.2.存入redisredisTemplate.opsForValue().set("item:id:" + item.getId(), json);}// 3.查询商品库存信息List<ItemStock> stockList = stockService.list();// 4.放入缓存for (ItemStock stock : stockList) {// 2.1.item序列化为JSONString json = MAPPER.writeValueAsString(stock);// 2.2.存入redisredisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);}}
}
  1. 查看redis,数据已经放入缓存中

在这里插入图片描述

5.7 查询Rdeis缓存【第二级缓存】

本节实现红色部分
在这里插入图片描述

OpenResty提供了操作Redis的模块,我们只需要引入该模块即可:在/usr/local/openresty/lualib/common.lua中

  1. 引入redis模块,并初始化redis对象
  2. 封装函数,用来释放redis连接,其实是放入连接池
  3. 封装函数,从redis读数据并返回

演示:关闭本地server服务,因为上面redis缓存预热已经将数据放入到了redis中,因此访问http://localhost/item.html?id=10005可以查到数据【从redis缓存中查的】。
common.lua

-- 1.引入redis模块,/usr/local/openresty/lualib/resty/redis.lua
local redis = require('resty.redis')
-- 初始化redis对象
local red = redis:new()
-- 设置redis超时时间:建立请求 发送请求 响应请求的超时时间
red:set_timeouts(1000,1000,1000)-- 2.关闭redis连接的工具方法,其实是放入连接池
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 thenngx.log(ngx.ERR, "放入redis连接池失败: ", err)end
end-- 3.建立redis连接,读数据
-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port, key)-- 获取一个连接local ok, err = red:connect(ip, port)if not ok thenngx.log(ngx.ERR, "连接redis失败 : ", err)return nilend-- 查询redislocal resp, err = red:get(key)-- 查询失败处理if not resp thenngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)end--得到的数据为空处理if resp == ngx.null thenresp = nilngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)end-- 释放连接,放入连接池close_redis(red)return resp
end-- 4.封装函数,发送http请求,并解析响应
local function read_http(path, params)local resp = ngx.location.capture(path,{method = ngx.HTTP_GET,args = params,})if not resp then-- 记录错误信息,返回404ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)ngx.exit(404)endreturn resp.body
end
-- 5.将方法导出
local _M = {  read_http = read_http,  -- 记得加逗号read_redis = read_redis
}  
return _M

item.lua

-- 案例4:封装一个read_data,实现先查询redis,未命中再查tomact-- 1.导入common函数库【自己编写的】,common.lua在/usr/local/openresty/lualib目录下
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 2.导入cjson解析库,也是在/usr/local/openresty/lualib目录下,默认就有此文件
local cjson = require('cjson')-- 3.封装查询函数
-- reids请求参数,tomcat的http请求路径,参数
function read_data(key, path, params)-- 查询redislocal resp = read_redis("127.0.0.1", 6379, key)-- 判断查询结果if not resp thenngx.log("redis查询失败,尝试查询http,key:", key)-- redis查询失败,去查询httpresp = read_http(path, params)endreturn resp
end-- 获取路径参数
local id = ngx.var[1]
-- 查询商品信息
local itemJSON = read_data("item:id:" .. id, "/item/" .. id, nil)
-- 查询库存信息
local stockJSON = read_data("item:stock:id:" .. id, "/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))

5.8 Nginx本地缓存【第一级缓存】

0penResty为Nginx提供了shard dict的功能,可以在nginx的多个worker之间共享数据,实现缓存功能。

  1. 开启共享字典,在nginx.conf的http下添加配置:
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m; 
  1. 操作共享字典:
-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache
-- 存储,指定key、value、过期时间,单位s,默认为0表示永不过期
item_cache:set('key','value',1000)
-- 读取
local val = item_cache:get('key')

实战:

nginx.conf中加入nginx本地缓存

http {# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150mlua_shared_dict item_cache 150m;
}

编写item.lua

-- 案例5:
-- 1)实现先查询nginx本地缓存,未命中再查redis,未命中再查tomact
-- 2)查询redis或tomcat成功后,将数据写入本地缓存,并设置有效期
-- 3)商品的基本信息有效期30分钟,库存信息有效期1分钟-- 1.导入common函数库【自己编写的】,common.lua在/usr/local/openresty/lualib目录下
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入共享词典,nginx本地缓存
-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache-- 2.导入cjson解析库,也是在/usr/local/openresty/lualib目录下,默认就有此文件
local cjson = require('cjson')-- 3.封装查询函数
-- reids请求参数,tomcat的http请求路径,参数
function read_data(key, expire, path, params)-- 1)查询nginx本地缓存local val = item_cache:get(key)if not val thenngx.log(ngx.ERR, "本地缓存查询失败,尝试查询redis,key:", key)-- 2)查询redisval = read_redis("127.0.0.1", 6379, key)-- 判断查询结果if not val thenngx.log(ngx.ERR, "redis查询失败,尝试查询http,key:", key)-- 3)redis查询失败,去查询httpval = read_http(path, params)endend-- 查询成功,把数据写入本地缓存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))

查看日志:第一次会将redis的数据放到nginx本地缓存,当再次查询时,直接从本地缓存中查询

在这里插入图片描述

六、缓存同步策略

当数据库进行修改时,缓存的内容也要进行相应的修改,因此需要完成数据同步。

6.1 常见缓存策略

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

6.2 安装Canal

下面我们就开启mysql的主从同步机制,让Canal来模拟salve

6.2.1 开启MySQL主从

Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。

这里以之前用Docker运行的mysql为例:

  1. 开启binlog

打开mysql容器挂载的日志文件,我的在/tmp/mysql/conf目录:

修改文件:

vi /tmp/mysql/conf/my.cnf

添加内容:

log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima

配置解读:

  • log-bin=/var/lib/mysql/mysql-bin:设置binary log文件的存放地址和文件名,叫做mysql-bin
  • binlog-do-db=heima:指定对哪个database记录binary log events,这里记录heima这个库

最终效果:/tmp/mysql/conf/my.cnf

[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000
log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima

重启mysql容器,可以看到多了一个mysql-bin.000001

在这里插入图片描述

  1. 设置用户权限

接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对heima这个库的操作权限。

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

可以看到创建了一个canal用户:

在这里插入图片描述

重启mysql容器即可

docker restart mysql

测试设置是否成功:在mysql控制台,或者Navicat中,输入命令:

show master status;

在这里插入图片描述

6.2.2 安装Canal

  1. 创建网络

我们需要创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:

docker network create heima

让mysql加入这个网络:

docker network connect heima mysql
  1. 安装Canal

课前资料中提供了canal的镜像压缩包:

大家可以上传到虚拟机,然后通过命令导入:

docker load -i canal.tar

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

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

说明:

  • -p 11111:11111:这是canal的默认监听端口
  • -e canal.destinations=heima:所属集群名称
  • -e canal.instance.master.address=mysql:3306:数据库地址和端口,因为mysql与canal同属一个网络,因此可以用mysql代替IP地址。如果不知道mysql容器地址,可以通过docker inspect 容器id来查看
  • -e canal.instance.dbUsername=canal:数据库用户名
  • -e canal.instance.dbPassword=canal :数据库密码
  • -e canal.instance.filter.regex=:要监听的表名称
  • --network heima \:将canal放入heima这个网络中

表名称监听支持的语法:

mysql 数据解析关注的表,Perl正则表达式.
多个正则之间以逗号(,)分隔,转义符需要双斜杠(\\) 
常见例子:
1.  所有表:.*   or  .*\\..*
2.  canal schema下所有表: canal\\..*
3.  canal下的以canal打头的表:canal\\.canal.*
4.  canal schema下的一张表:canal.test1
5.  多个规则组合使用然后以逗号隔开:canal\\..*,mysql.test1,mysql.test2 

通过docker logs -f canal查看日志,是否启动成功
在这里插入图片描述

  1. Canal与mysql是否建立连接

通过docker exec -it canal bash进入canal容器内部

通过tail -f canal-server/logs/canal/canal.log查看canal运行日志

在这里插入图片描述

通过tail -f canal-server/logs/heima/heima.log查看其他运行日志

在这里插入图片描述
最后通过exit退出容器

6.3 监听Canal

在这里插入图片描述

Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。不过这里我们会使用GitHub上的第三方开源的canal-starter。地址:https://github.com/NormanGyllenhaal/canal-client

  1. 引入依赖:
<dependency><groupId>top.javatool</groupId><artifactId>canal-spring-boot-starter</artifactId><version>1.2.1-RELEASE</version>
</dependency>
  1. 编写配置:
canal:destination: heima # canal实例名称,要跟虚拟机上设置的destination一致server: 39.107.236.163:11111 # canal地址
  1. 编写监听器,监听canal消息

在这里插入图片描述

@CanalTable("tb_item")
@Component
public class ItemHandler implements EntryHandler<Item> {@Autowiredprivate RedisHandler redisHandler;@Autowiredprivate Cache<Long, Item> itemCache;@Overridepublic void insert(Item item) {// 写数据到JVM进程缓存itemCache.put(item.getId(), item);// 写数据到redisredisHandler.saveItem(item);}@Overridepublic void update(Item before, Item after) {// 写数据到JVM进程缓存itemCache.put(after.getId(), after);// 写数据到redisredisHandler.saveItem(after);}@Overridepublic void delete(Item item) {// 删除数据到JVM进程缓存itemCache.invalidate(item.getId());// 删除数据到redisredisHandler.deleteItemById(item.getId());}
}

RedisHandler.java

@Component
public class RedisHandler implements InitializingBean {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate IItemService itemService;@Autowiredprivate IItemStockService stockService;// jason处理工具private static final ObjectMapper MAPPER = new ObjectMapper();@Overridepublic void afterPropertiesSet() throws Exception {// 初始化缓存// 1.查询商品信息List<Item> itemList = itemService.list();// 2.放入缓存for (Item item : itemList) {// 2.1.item序列化为JSONString json = MAPPER.writeValueAsString(item);// 2.2.存入redisredisTemplate.opsForValue().set("item:id:" + item.getId(), json);}// 3.查询商品库存信息List<ItemStock> stockList = stockService.list();// 4.放入缓存for (ItemStock stock : stockList) {// 2.1.item序列化为JSONString json = MAPPER.writeValueAsString(stock);// 2.2.存入redisredisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);}}public void saveItem(Item item) {try {String json = MAPPER.writeValueAsString(item);redisTemplate.opsForValue().set("item:id:" + item.getId(), json);} catch (JsonProcessingException e) {throw new RuntimeException(e);}}public void deleteItemById(Long id) {redisTemplate.delete("item:id:" + id);}
}
  1. Canal推送给canal-client的是被修改的这一行数据(row),而我们引入的canal-client则会帮我们把行数据封装到ltem实体类中。这个过程中需要知道数据库与实体的映射关系,要用到PA的几个注解:

在这里插入图片描述

@Data
@TableName("tb_item")
public class Item {@TableId(type = IdType.AUTO)@Idprivate Long id;//商品id@Column(name = "name")private String name;//商品名称private String title;//商品标题private Long price;//价格(分)private String image;//商品图片private String category;//分类名称private String brand;//品牌名称private String spec;//规格private Integer status;//商品状态 1-正常,2-下架private Date createTime;//创建时间private Date updateTime;//更新时间@TableField(exist = false)@Transientprivate Integer stock;@TableField(exist = false)@Transientprivate Integer sold;
}
  1. 测试,修改10001的价格,发现本机控台日志消息变化,并且访问http://localhost/item.html?id=10001也发生变化

在这里插入图片描述

八、多级缓存总结

在这里插入图片描述

九、额外说明:cpolar内网穿透(将私网暴露成公网供外部使用)

第一步:下载并注册账号,cpolar官方:https://www.cpolar.com/

在这里插入图片描述

第二步:配置隧道

在这里插入图片描述
第二步:查看公网地址
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/769896.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DC-4靶机

一.环境搭建 1.下载地址 靶场下载地址:https://download.vulnhub.com/dc/DC-4.zip 下载不下来用迅雷下载 2.虚拟机配置 切换为nat模式 开启靶机&#xff0c;遇到所有的错误直接点重试或者是&#xff0c;开启后呈现为下图即可 二.开始渗透 1.信息收集 老规矩&#xff0c;…

【排序算法】插入排序与选择排序详解

文章目录 &#x1f4dd;选择排序是什么&#xff1f;&#x1f320;选择排序思路&#x1f309; 直接选择排序&#x1f320;选择排序优化&#x1f320;优化方法&#x1f309;排序优化后问题 &#x1f320;选择排序效率特性 &#x1f309;插入排序&#x1f320;插入排序实现 &#…

简单了解单例模式

什么是单例模式 对于一个类&#xff0c;只有一个实例化的对象&#xff0c;我们构建单例模式一般有两种&#xff1a;饿汉式和懒汉式 饿汉式 优点是无线程安全问题&#xff0c;类加载就创建对象缺点是占内存 class Singleton01{private static Singleton01 instance new Sing…

【JavaScript】JavaScript 程序流程控制 ⑥ ( while 循环概念 | while 循环语法结构 )

文章目录 一、while 循环1、while 循环概念2、while 循环语法结构 二、while 循环 - 代码示例1、打印数字2、计算 1 - 10 之和 一、while 循环 1、while 循环概念 在 JavaScript 中 , while 循环 是一种 " 循环控制语句 " , 使用该语句就可以 重复执行一段代码块 , …

瑞_Redis_商户查询缓存_什么是缓存

文章目录 项目介绍1 短信登录2 商户查询缓存2.1 什么是缓存2.1.1 缓存的应用场景2.1.2 为什么要使用缓存2.1.3 Web应用中缓存的作用2.1.4 Web应用中缓存的成本 附&#xff1a;缓存封装工具类 &#x1f64a; 前言&#xff1a;本文章为瑞_系列专栏之《Redis》的实战篇的商户查询缓…

C语言---------strlen的使用和模拟实现

字符串是以‘\0’作为结束标志&#xff0c;strlen函数的返回值是‘\0’前面的字符串的个数&#xff08;不包括‘\0’&#xff09; 注意 1&#xff0c;参数指向的字符串必须以‘\0’结束 2&#xff0c;函数的返回值必须以size_t,是无符号的 使用代码 ​ #include<stdio.…

学习刷题-13

3.23 hw机试【二叉树】 剑指offer32 剑指 offer32&#xff08;一、二、三&#xff09;_剑指offer 32-CSDN博客 从上到下打印二叉树I 一棵圣诞树记作根节点为 root 的二叉树&#xff0c;节点值为该位置装饰彩灯的颜色编号。请按照从 左 到 右 的顺序返回每一层彩灯编号。 输…

学习vue3第十一节(依赖注入:provide/inject)

本机介绍&#xff1a;provide/inject 注意&#xff1a;大家在看此小节时候&#xff0c;默认大家已经了解一些组件的使用方法 1、依赖注入的用途&#xff1a; 当嵌套层级多的时候&#xff0c;某个子组件需要较远层级的父组件数据时候&#xff0c;如果我们依然使用props 传递数…

关系型数据库mysql(6)备份与恢复

一.数据备份的重要性 &#xff08;1&#xff09;在生产环境中&#xff0c;数据的安全性至关重要 &#xff08;2&#xff09;任何数据的丢失都可能产生严重的后果 &#xff08;3&#xff09;造成数据丢失的原因 程序错误人为操作失误运算错误磁盘故障灾难&#xff08;如火灾…

背景减除(1)--bgslibrary Windows编译和使用

入侵监控领域中&#xff0c;在固定场景下&#xff0c;需要检测和监控的入侵物体种类繁多&#xff0c;无法具体穷尽。传统的CV算法提取的特征应用场景有限&#xff0c;无法完成大量物体的监控&#xff1b;深度学习目标检测方法没法收集到无穷无尽的物体种类&#xff0c;因此监督…

C语言----strcpy和strcat的使用和模拟实现

一&#xff0c;strcpy()函数 strcpy() 函数是 C语言中一个非常重要的字符串处理函数&#xff0c;其功能是将一个字符串复制到另一个字符串中。该函数原型如下&#xff1a; char*strcpy(char*dest,const char*src) 其中&#xff0c;dest 表示目标字符串&#xff0c;即将被复制到…

【HDFS】DatanodeAdminBackoffMonitor退役节点极慢的问题定位

一、现象: 下节点特别慢。10台节点,每台大约需要退役60w个块。但是3个小时才退役了3000多个块。 NN侧如下日志,可以看到30秒只退役了512-494 = 20个块,这要是退役600w个块,得猴年马月? 2024-03-19 14:44:42,952 INFO org.apache.hadoop.hdfs.server.blockmanagement.D…

浅析扩散模型与图像生成【应用篇】(十一)——DDIBs

11. Dual Diffusion Implicit Bridges for Image-to-Image Translation 该文提出一种双扩散隐式桥&#xff08;Dual Diffusion Implicit Bridges&#xff0c; DDIBs&#xff09;方法用于图像转换&#xff0c;其最大的特点在于处理源域图像的模型和处理目标域图像的模型是彼此分…

华为防火墙二层墙(VAN/SVI/单臂路由)

二层墙只能做地址池形式的NAT。 交换机安全策略防火墙二层墙 路由器安全策略防火墙三层墙 交换机的光口是不能直接插线的&#xff0c;光模块&#xff0c;包括进和出 长距离&#xff1a;单模 短距离&#xff1a;多模 防火墙自身的ping流量需要单独配置

一篇复现Docker镜像操作与容器操作

华子目录 Docker镜像操作创建镜像方式1docker commit示例 方式2docker import示例1&#xff1a;从本地文件系统导入示例2&#xff1a;从远程URL导入注意事项 方式3docker build示例1&#xff1a;构建镜像并指定名称和标签示例2&#xff1a;使用自定义的 Dockerfile 路径构建镜像…

Unity连接MySQL踩坑,问题处理记录

用的unity2021版本&#xff0c;MySQL是官方下载的最新版8.0.36. 安装MySQL时&#xff0c;过去如果安装过&#xff0c;一定要删干净&#xff0c;单纯的卸载不行&#xff0c;网上有很多教程。 MySQL安装完成后&#xff0c;将安装目录的MySql.Data.dll文件放入unity项目的Plugin…

数据运营常用的8大模型

✅作者简介&#xff1a;《数据运营&#xff1a;数据分析模型撬动新零售实战》作者、《数据实践之美》作者、数据科技公司创始人、多次参加国家级大数据行业标准研讨及制定、高端企培合作讲师。 &#x1f338;公众号&#xff1a;风姑娘的数字视角&#xff0c;免费分享数据应用相…

202447读书笔记|《围炉夜话》——多记先正格言,胸中方有主宰 闲看他人行事,眼前即是规箴

202447读书笔记|《围炉夜话》——多记先正格言&#xff0c;胸中方有主宰&#xff1b;闲看他人行事&#xff0c;眼前即是规箴 围炉夜话 《围炉夜话&#xff08;读客三个圈经典文库&#xff09;》作者王永彬。读《围炉夜话》&#xff0c;可以掌握君子安身立业的大智慧&#xff01…

【Canvas与艺术】暗蓝网格汽车速度仪表盘

【关键点】 采用线性渐变色&#xff0c;使上深下浅的圆有凹下效果&#xff0c;使上浅下深的圆有凸起效果&#xff0c;两者结合就有立体圆钮的感觉。 【图例】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type&quo…

2015年认证杯SPSSPRO杯数学建模C题(第二阶段)荒漠区动植物关系的研究全过程文档及程序

2015年认证杯SPSSPRO杯数学建模 C题 荒漠区动植物关系的研究 原题再现&#xff1a; 环境与发展是当今世界所普遍关注的重大问题, 随着全球与区域经济的迅猛发展, 人类也正以前所未有的规模和强度影响着环境、改变着环境, 使全球的生命支持系统受到了严重创伤, 出现了全球变暖…