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

news2024/11/15 2:00:26

文章目录

  • 一、多级缓存概念
  • 二、准备工作【导入案例,并搭建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>

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

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

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

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

        // 4.取数据,不存在则去数据库查询
        String defaultGF = cache.get("defaultGF", key -> {
            // 这里可以去数据库根据 key查询value
            return "柳岩";
        });
        System.out.println("defaultGF = " + defaultGF);
    }

第三步:缓存驱逐策略

在这里插入图片描述

    /**
     基于大小设置驱逐策略:
     */
    @Test
    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"));
    }

结果:
gf1: null
gf2: null
gf3: 迪丽热巴

    /**
     基于时间设置驱逐策略:
     */
    @Test
    void 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
     */
    @Bean
    public Cache<Long, Item> itemCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }

    /**
     * stock库存的缓存:
     * 缓存初始大小100
     * 缓存上限10000
     */
    @Bean
    public Cache<Long, ItemStock> stockCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }
}

第二步:编写业务代码

@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) {
        // 优先根据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 arr1

local function printArr(arr)
  if (not arr) then
   print('数组不能为空')
   return nil
  end
  for i,val in ipairs(arr)do
   print(val)
  end
end

printArr(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{
	#响应类型,这里返回json
	default_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/item
        location /api/item{
            # 响应类型,这里返回json
            default_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/item
        location ~ /api/item/(\d+){
            # 响应类型,这里返回json
            default_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
        -- 记录错误信息,返回404
        ngx.log(ngx.ERR, "http查询失败,路径为: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 将方法导出
local _M = {  
    read_http = read_http
}  
return _M

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

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

		# 反向代理给tomcat
        location /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/item
        location ~ /api/item/(\d+){
            # 响应类型,这里返回json
            default_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 {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    // jason处理工具
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void afterPropertiesSet() throws Exception {
        // 初始化缓存
        // 1.查询商品信息
        List<Item> itemList = itemService.list();
        // 2.放入缓存
        for (Item item : itemList) {
            // 2.1.item序列化为JSON
            String json = MAPPER.writeValueAsString(item);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        }

        // 3.查询商品库存信息
        List<ItemStock> stockList = stockService.list();
        // 4.放入缓存
        for (ItemStock stock : stockList) {
            // 2.1.item序列化为JSON
            String json = MAPPER.writeValueAsString(stock);
            // 2.2.存入redis
            redisTemplate.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 then
        ngx.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 then
        ngx.log(ngx.ERR, "连接redis失败 : ", err)
        return nil
    end
    -- 查询redis
    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


-- 4.封装函数,发送http请求,并解析响应
local function read_http(path, params)
    local resp = ngx.location.capture(path,{
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        -- 记录错误信息,返回404
        ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return 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)
    -- 查询redis
    local resp = read_redis("127.0.0.1", 6379, key)
    -- 判断查询结果
    if not resp then
        ngx.log("redis查询失败,尝试查询http,key:", key)
        -- redis查询失败,去查询http
        resp = read_http(path, params)
    end
    return 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,大小150m
    lua_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 then
        ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询redis,key:", key)
        -- 2)查询redis
        val = read_redis("127.0.0.1", 6379, key)
        -- 判断查询结果
        if not val then
            ngx.log(ngx.ERR, "redis查询失败,尝试查询http,key:", key)
            -- 3)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))

查看日志:第一次会将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> {

    @Autowired
    private RedisHandler redisHandler;
    @Autowired
    private Cache<Long, Item> itemCache;

    @Override
    public void insert(Item item) {
        // 写数据到JVM进程缓存
        itemCache.put(item.getId(), item);
        // 写数据到redis
        redisHandler.saveItem(item);
    }

    @Override
    public void update(Item before, Item after) {
        // 写数据到JVM进程缓存
        itemCache.put(after.getId(), after);
        // 写数据到redis
        redisHandler.saveItem(after);
    }

    @Override
    public void delete(Item item) {
        // 删除数据到JVM进程缓存
        itemCache.invalidate(item.getId());
        // 删除数据到redis
        redisHandler.deleteItemById(item.getId());
    }
}

RedisHandler.java

@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    // jason处理工具
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void afterPropertiesSet() throws Exception {
        // 初始化缓存
        // 1.查询商品信息
        List<Item> itemList = itemService.list();
        // 2.放入缓存
        for (Item item : itemList) {
            // 2.1.item序列化为JSON
            String json = MAPPER.writeValueAsString(item);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        }

        // 3.查询商品库存信息
        List<ItemStock> stockList = stockService.list();
        // 4.放入缓存
        for (ItemStock stock : stockList) {
            // 2.1.item序列化为JSON
            String json = MAPPER.writeValueAsString(stock);
            // 2.2.存入redis
            redisTemplate.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)
    @Id
    private 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)
    @Transient
    private Integer stock;
    @TableField(exist = false)
    @Transient
    private Integer sold;
}
  1. 测试,修改10001的价格,发现本机控台日志消息变化,并且访问http://localhost/item.html?id=10001也发生变化

在这里插入图片描述

八、多级缓存总结

在这里插入图片描述

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

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

在这里插入图片描述

第二步:配置隧道

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

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

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

相关文章

厨余垃圾处理设备工业监控PLC连接APP小程序智能软硬件开发之功能原理篇

接着上一篇《厨余垃圾处理设备工业监控PLC连接APP小程序智能软硬件开发之功能结构篇》继续总结一下厨余垃圾处理设备智能软硬件统的原理。所有的软硬件系统全是自己一人独自开发&#xff0c;看法和角度难免有局限性。希望抛砖引玉&#xff0c;将该智能软硬件系统分享给更多有类…

字节算法岗二面,凉凉。。。

节前&#xff0c;我们星球组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、参加社招和校招面试的同学&#xff0c;针对算法岗技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何准备、面试常考点分享等热门话题进行了深入的讨论。 汇总…

电脑如何更新AMD独立显卡驱动?安装官方驱动的方法来了!

前言 有小伙伴在电脑上安装了独立显卡之后&#xff0c;总会用驱动人生或者驱动精灵等软件给独立显卡安装驱动。这种安装方法并不能说是错的&#xff0c;反正能用就行。 安装官方驱动的办法其实很简单&#xff0c;现在独立显卡一共就那么几家&#xff0c;最常见的显卡就是Nvidi…

【Maven】高效入门

Maven依赖管理项目构建工具 目录 文章目录 Maven依赖管理项目构建工具目录一、Maven简介1、为什么学习Maven1.1、Maven是一个依赖管理工具1.2、Maven是一个构建工具1.3、结论 2. Maven介绍3. Maven软件工作原理模型图&#xff08;了解&#xff09; 二、Maven安装和配置1. Maven…

iOS-UIFont 实现三方字体的下载和使用

UIFont 系列传送门 第一弹加载本地字体:iOS UIFont-新增第三方字体 第二弹加载线上字体:iOS-UIFont 实现三方字体的下载和使用 前言 在上一章我们完成啦如何加载使用本地的字体。如果我们有很多的字体可供用户选择,我们当然可以全部使用本地字体加载方式,可是这样就增加了…

移动硬盘盒结合PD技术为电脑供电:一种便携高效的供电新方案

在数字化时代&#xff0c;电脑已经成为我们生活和工作中不可或缺的工具。而在电脑的使用过程中&#xff0c;供电问题一直是我们需要关注的重要方面。近年来&#xff0c;随着技术的不断进步&#xff0c;移动硬盘盒子与PD&#xff08;Power Delivery&#xff09;技术的结合&#…

技术周刊 117 期:Visual Copilot、INP、Kimi 支持 200 万字上下文、Grok 开源、Figure 01、Open Sora 开源

美味值&#xff1a;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f; 口味&#xff1a;金骏眉 大家好&#xff0c;我是童欧巴。老规矩&#xff0c;咱们先来看技术资讯。 技术资讯 前端 VitePress (早就应该) 1.0 发布MistCSS&#xff0c;只使用 CSS 来…

QT环境搭建

学习QT 一、QT环境搭建二、QT的SDK下载三、认识QT SDK 中自带的一些程序 一、QT环境搭建 QT开发环境&#xff0c;需要安装三个部分。 c编译器&#xff08;gcc、cl.exe……不是visual studio&#xff09;QT SDK&#xff08;QT SDK里面已经内置了C编译器&#xff1b;SDK就是软件…

【网络爬虫】(1) 网络请求,urllib库介绍

各位同学好&#xff0c;今天开始和各位分享一下python网络爬虫技巧&#xff0c;从基本的函数开始&#xff0c;到项目实战。那我们开始吧。 1. 基本概念 这里简单介绍一下后续学习中需要掌握的概念。 &#xff08;1&#xff09;http 和 https 协议。http是超文本传输&#xf…

【考研数学二】线性代数重点笔记

目录 第一章 行列式 1.1 行列式的几何意义 1.2 什么是线性相关&#xff0c;线性无关 1.3 行列式几何意义 1.4 行列式求和 1.5 行列式其他性质 1.6 余子式 1.7 对角线行列式 1.8 分块行列式 1.9 范德蒙德行列式 1.10 爪形行列式的计算 第二章 矩阵 2.1 初识矩阵 2…

基于nodejs+vue企业人才引进服务平台python-flask-django-php

本文通过采用MySQL数据库以及nodejs语言、express框架&#xff0c;结合国内线上管理现状&#xff0c;开发了一个基于node的企业人才引进服务平台。系统分为多个功能模块&#xff1a;用户信息、企业信息、招聘信息、应聘信息等。通过系统测试&#xff0c;本系统实现了系统设计目…

ffmpeg拉流并解码

流程 注意事项 版本不同导致的api差异资源安全释放

C# 将 Word 转文本存储到数据库并进行管理

目录 前言 1. 创建数据库表格 2. 安装必需的 NuGet 包 3. 转换 Word 文档为文本 4. 将文本存储到数据库 5. 完整示例 前言 C# 是一种通用的编程语言&#xff0c;可以用于开发各种类型的应用程序&#xff0c;包括处理文本和数据库管理。在这篇文章中&#xff0c;我将向您…

10.2024

使用选择排序将{2,9,5,0,1,3,6,8}排序 代码&#xff1a; public class 第十题 {public static void main(String[] args) {int a[]{2,9,5,0,1,3,6,8};int begin0;int enda.length-1;while (begin<end){int mina[begin];int tbegin;for (int ibegin;i<end;i){if(min>…

瑞吉外卖实战学习--完善登录功能

完善登录功能 1、创建自定义过滤器LoginCheckFiler1.1通过WebFilter创建过滤器1.2 验证是否可以拦截请求1.3 代码 2、在启动类加入注解ServletComponentScan 用来扫描过滤器触发所有的过滤器ServletComponentScan 3、完善过滤器的处理逻辑3.1判断是否需要是要放行的请求3.2判断…

基于nodejs+vue考研互助交流系统python-flask-django-php

同时还能为用户提供一个方便实用的考研互助交流系统&#xff0c;使得用户能够及时地找到合适自己的考研信息。管理员在使用本系统时&#xff0c;可以通过后台管理员界面管理用户的信息&#xff0c;也可以发布系统公告&#xff0c;让用户及时了解考研信息。这样&#xff0c;用户…

javaSwing超级玛丽游戏

一、摘要 摘要 近年来&#xff0c;Java作为一种新的编程语言&#xff0c;以其简单性、可移植性和平台无关性等优点&#xff0c;得到了广泛地应用。J2SE称为Java标准版或Java标准平台。J2SE提供了标准的SDK开发平台。利用该平台可以开发Java桌面应用程序和低端的服务器应用程序…

EPSON推出RX8804CE为工业机器提供稳定且精准的时钟信号

工业机器是智能工厂的关键主体组成部分&#xff0c;工业机器的时钟稳定性和精确性直接关系到整个系统的生产高效性和稳定性。EPSON推出了面向工业机器、手持设备、室外机等领域的实时时钟模块RX8804CE&#xff0c;可选择32.768 KHz、1024 Hz、1 Hz三种频率输出&#xff0c;温度…

Android视角看鸿蒙第九课-鸿蒙的布局

鸿蒙的四大布局 导读 前面八篇文章描述了鸿蒙app的配置文件&#xff0c;关于版本号&#xff0c;开发版本&#xff0c;桌面图标等等配置方式。从这一篇文章开始学习鸿蒙的UI使用方式。 前面我们学习到鸿蒙有ability和page的区分&#xff0c;ability类似Activity但又不完全一样…

获取高德安全码SHA1

高德开发者平台上给的三种方法 获取安全码SHA1&#xff0c;这里我自己使用的是第三种方法。 1、通过Eclipse编译器获取SHA1 使用 adt 22 以上版本&#xff0c;可以在 eclipse 中直接查看。 Windows&#xff1a;依次在 eclipse 中打开 Window -> Preferances -> Androi…