Bootstrap

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


一、多级缓存概念

在这里插入图片描述
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻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/

在这里插入图片描述

第二步:配置隧道

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

;