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

文章目录

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

一、多级缓存概念

多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能。

用作缓存的Nginx是业务Nginx,需要部署为集群,再有专门的Nginx用来做反向代理。

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

本章实现橙色部分

2.1 导入商品案例

2.1.1 安装MySQL

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

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

sh 复制代码
# 进入/tmp目录
cd /tmp
# 创建文件夹
mkdir mysql
# 进入mysql目录
cd mysql

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

sh 复制代码
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的配置文件:

sh 复制代码
# 创建文件
touch /tmp/mysql/conf/my.cnf

文件的内容如下:

ini 复制代码
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000

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

sh 复制代码
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服务。

运行命令:

powershell 复制代码
start nginx.exe

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

  1. 反向代理

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

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

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

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

其中的关键配置如下:

完整内容如下:

nginx 复制代码
#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

第一步:引入依赖

java 复制代码
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>

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

java 复制代码
    /**
      基本用法测试
     */
    @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);
    }

第三步:缓存驱逐策略

java 复制代码
    /**
     基于大小设置驱逐策略:
     */
    @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类

java 复制代码
/**
 * 初始化本地缓存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();
    }
}

第二步:编写业务代码

java 复制代码
@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 条件控制和函数

java 复制代码
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)

输出:

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

五、多级缓存

本章实现红色框部分

5.1 安装OpenResty

5.1.1 安装

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

  1. 安装开发库

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

sh 复制代码
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

bash 复制代码
yum install -y openresty
  1. 安装opm工具

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

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

bash 复制代码
yum install -y openresty-opm
  1. 目录结构

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

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

  1. 配置nginx的环境变量

打开配置文件:

sh 复制代码
vi /etc/profile

在最下面加入两行:

sh 复制代码
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基本一致:

sh 复制代码
# 启动nginx
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stop

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

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

nginx 复制代码
#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:

sh 复制代码
nginx

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

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

5.2 OpenResty快速入门

第一步:修改nginx.conf文件

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

nginx.conf

java 复制代码
#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文件
java 复制代码
[root@iZ2ze1r1nnqykr8zfme6cjZ openresty]# cd /usr/local/openresty/nginx
[root@iZ2ze1r1nnqykr8zfme6cjZ nginx]# mkdir lua
[root@iZ2ze1r1nnqykr8zfme6cjZ nginx]# touch lua/item.lua
  1. 编写业务内容
java 复制代码
-- 返回假数据,这里的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. 重新加载配置
java 复制代码
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+)

java 复制代码
#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使用..拼接并返回给页面

java 复制代码
-- 获取路径参数
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

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地址

lua 复制代码
		# 反向代理给tomcat
        location /item {
            proxy_pass http://192.168.150.1:8081;
        }

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

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

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

lua 复制代码
-- 案例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中访问。

java 复制代码
#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
java 复制代码
docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes
  1. 在item-service服务中引入Redis依赖
java 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  1. 配置Redis地址
java 复制代码
spring:
  redis:
    host: 192.168.150.101
  1. 编写初始化类
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);
        }
    }
}
  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

java 复制代码
-- 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

java 复制代码
-- 案例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下添加配置:
nginx 复制代码
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m; 
  1. 操作共享字典:
nginx 复制代码
-- 获取本地缓存对象
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本地缓存

java 复制代码
http {
    # 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
    lua_shared_dict item_cache 150m;
}

编写item.lua

java 复制代码
-- 案例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目录:

修改文件:

sh 复制代码
vi /tmp/mysql/conf/my.cnf

添加内容:

ini 复制代码
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

ini 复制代码
[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这个库的操作权限。

mysql 复制代码
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网络中:

sh 复制代码
docker network create heima

让mysql加入这个网络:

sh 复制代码
docker network connect heima mysql
  1. 安装Canal

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

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

复制代码
docker load -i canal.tar

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

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

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的几个注解:
java 复制代码
@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/

第二步:配置隧道

第二步:查看公网地址

相关推荐
小七-七牛开发者2 天前
TokenPilot:让 LLM Agent 长会话成本降 60%+ 的上下文管理
缓存·agent·token·context·上下文·推理成本
蝎子莱莱爱打怪3 天前
XZLL-IM干货系列 04|Netty 长连接实战:Pipeline 怎么排、心跳怎么跳、连接怎么管
后端·微服务·面试
Avan_菜菜3 天前
FRP 内网穿透完整实战:从 HTTP 映射到 HTTPS 自签代理
运维·nginx·https
SamDeepThinking4 天前
Java微服务练习方式
java·后端·微服务
米丘7 天前
微前端之 Web Components 完全指南
微服务·html
ping某7 天前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
霸道流氓气质9 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
ofoxcoding9 天前
在AI API聚合平台配置DeepSeek V3.2提示词缓存实战:快速接入与成本优化指南
人工智能·spring·缓存·ai
難釋懷9 天前
Nginx反向代理中的容错机制
运维·nginx
bloglin999999 天前
Nginx高危漏洞CVE-2021-23017及配置样例
运维·nginx