多级缓存的实现

多级缓存

缓存的作用: 减轻数据库的压力,缩短服务响应的时间,提高服务的并发能力。虽然Redis的并发效果已经很好了,但是依然有上限,随着互联网的发展,用户量越来越大,并发量非常庞大时,仅仅靠Redis不能满足庞大的并发需求,我们下面学习的多级缓存正式用来应对亿级流量的并发

注意本篇内容非常多,而且是同一个实验,所以最好是跟着从上到下,不要跳过

1. 多级缓存的意义

传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库,存在下面的问题:

1、请求要经过Tomcat处理,Tomcat的并发能力其实是不如Redis,导致Tomcat的性能成为整个系统的瓶颈

2、Redis缓存失效时,会对数据库产生冲击

img

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

img

在上图中,我们需要学习如何在Tomcat编写进程缓存,还需要学习如何在Nginx内部使用Lua语言进行编程,然后学习Nginx本地缓存、Redis缓存、Tomcat缓存等多级缓存,最后学习数据库与缓存之间的同步策略。下面会逐步学习

2. JVM进程缓存案例搭建

先学习 ‘如何在Tomcat编写进程缓存’ ,也就是 JVM进程缓存,我们会通过一个案例进行学习

为了演示多级缓存,我们先导入一个商品管理的案例,其中包含商品的CRUD功能。我们将来会给查询商品添加多级缓存

img

【部署mysql容器,虽然前面在学习 ‘实用篇-Docker容器’ 学过,但是为避免出现各种问题,下面还是需要再做一次】

删除之前做过的mysql容器

docker rm -f mysql # 删除在docker里面的mysql容器
rm -rf /tmp/mysql # 删除mysql的挂载数据

第一步: 创建三个目录,分别是/tmp/mysql/data、/tmp/mysql/conf,/tmp/mysql/logs

mkdir -p /tmp/mysql/{data,conf,logs}
cd /tmp/mysql && ls

第二步: 在docker部署mysql容器。由于MySQL文件较大,拉取耗时较长,所以下面提供了下载链接,下载到自己Windows电脑,

然后传到虚拟机/tmp/mysql目录

mysql.tar文件快速下载: https://cowtransfer.com/s/b047088bc7f048

第三步: 将mysql.tar文件,上传到虚拟机/tmp/mysql目录,再通过前面学的’镜像命令-导入镜像到docker’,里面学过load命令

cd /tmp/mysql
docker load -i mysql.tar

img

img

第四步: 在/tmp/mysql/conf目录,创建一个文件叫hmy.cnf,写入如下

cd /tmp/mysql/conf
touch hmy.cnf
vi hmy.cnf
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000

第五步: 到此准备工作已结束,正式开始挂载。在Docker大容器(也叫Docker主机、宿主机)使用MySQL镜像来创建并运行MySQL容器

下面命令全部复制执行,注意密码为123

cd /tmp/mysql
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=123 \
 --privileged \
 -d \
 mysql:5.7.25
# docker run: 创建并运行容器
# --name: 给容器起一个名字,例如叫mysql
# -e: MySQL登录密码
# -p: 端口号
# -v: [挂载宿主机的配置文件]:[mysql容器的配置文件]
# -v: [挂载宿主机的用于存放数据的文件]:[mysql容器的用于存放数据的文件]
# -d: 后台运行容器
# mysql:5.7.25: 镜像名称,由于我们的mysql镜像是这个版本,所以镜像名称需要带上镜像版本号

img

第六步: 去连接一下这个mysql容器里面的mysql服务,查看连接是否正常

img

img

第七步: 下次运行这个mysql容器,只要执行如下即可

systemctl start docker # 启动docker服务
docker restart mysql #启动mysql容器

第八步: 在mysql数据库创建名为JvmDataShop的库,并在JvmDataShop库执行创建tb_item商品表、tb_item_stock商品库存表的SQL语句

create database if not exists JvmDataShop;
use JvmDataShop;

CREATE TABLE `tb_item`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品id',
  `title` varchar(264) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '商品标题',
  `name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '商品名称',
  `price` bigint(20) NOT NULL COMMENT '价格(分)',
  `image` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品图片',
  `category` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '类目名称',
  `brand` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '品牌名称',
  `spec` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '规格',
  `status` int(1) NULL DEFAULT 1 COMMENT '商品状态 1-正常,2-下架,3-删除',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `status`(`status`) USING BTREE,
  INDEX `updated`(`update_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 50002 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '商品表' ROW_FORMAT = COMPACT;
INSERT INTO `tb_item` VALUES (10001, 'RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4', 'SALSA AIR', 16900, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp', '拉杆箱', 'RIMOWA', '{\"颜色\": \"红色\", \"尺码\": \"26寸\"}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');
INSERT INTO `tb_item` VALUES (10002, '安佳脱脂牛奶 新西兰进口轻欣脱脂250ml*24整箱装*2', '脱脂牛奶', 68600, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t25552/261/1180671662/383855/33da8faa/5b8cf792Neda8550c.jpg!q70.jpg.webp', '牛奶', '安佳', '{\"数量\": 24}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');
INSERT INTO `tb_item` VALUES (10003, '唐狮新品牛仔裤女学生韩版宽松裤子 A款/中牛仔蓝(无绒款) 26', '韩版牛仔裤', 84600, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t26989/116/124520860/644643/173643ea/5b860864N6bfd95db.jpg!q70.jpg.webp', '牛仔裤', '唐狮', '{\"颜色\": \"蓝色\", \"尺码\": \"26\"}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');
INSERT INTO `tb_item` VALUES (10004, '森马(senma)休闲鞋女2019春季新款韩版系带板鞋学生百搭平底女鞋 黄色 36', '休闲板鞋', 10400, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/29976/8/2947/65074/5c22dad6Ef54f0505/0b5fe8c5d9bf6c47.jpg!q70.jpg.webp', '休闲鞋', '森马', '{\"颜色\": \"白色\", \"尺码\": \"36\"}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');
INSERT INTO `tb_item` VALUES (10005, '花王(Merries)拉拉裤 M58片 中号尿不湿(6-11kg)(日本原装进口)', '拉拉裤', 38900, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t24370/119/1282321183/267273/b4be9a80/5b595759N7d92f931.jpg!q70.jpg.webp', '拉拉裤', '花王', '{\"型号\": \"XL\"}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');

CREATE TABLE `tb_item_stock`  (
  `item_id` bigint(20) NOT NULL COMMENT '商品id,关联tb_item表',
  `stock` int(10) NOT NULL DEFAULT 9999 COMMENT '商品库存',
  `sold` int(10) NOT NULL DEFAULT 0 COMMENT '商品销量',
  PRIMARY KEY (`item_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;
INSERT INTO `tb_item_stock` VALUES (10001, 99996, 3219);
INSERT INTO `tb_item_stock` VALUES (10002, 99999, 54981);
INSERT INTO `tb_item_stock` VALUES (10003, 99999, 189);
INSERT INTO `tb_item_stock` VALUES (10004, 99999, 974);
INSERT INTO `tb_item_stock` VALUES (10005, 99999, 18649);

第九步: 下载item-service.zip,解压后是item-service文件夹,是一个项目工程,用idea打开,并且修改application.yml的数据库连接信息

https://cowtransfer.com/s/353ec9dc3dd54d

img

​ 第十步: 运行ItemApplication引导类

访问主页: http://localhost:8081

img

访问商品: http://localhost:8081/item/10001

img

访问商品库存: http://localhost:8081/item/stock/10001

img

第十一步: 部署Nginx反向代理服务。

我们希望在查询接口增加缓存业务。设计如下。把item.html页面放在Nginx反向代理服务器,用户请求商品页面时,就把这个item.html页面返回给用户img

(1) 下载nginx-1.18.0.zip,下载后解压到D盘,下载链接如下

https://cowtransfer.com/s/9d4e834d28e345

img

img

(2) 修改nginx.conf文件

img

(3) win+r,输入cmd并回车,然后输入下面的命令启动Nginx

d:
cd nginx-1.18.0
start nginx.exe

img

(3)浏览器访问Nginx

localhost

img

(4) 浏览器访问Nginx的item.html页面。目前这个页面的数据是写死的假数据,后续我们会使用缓存向服务器查询数据,然后把请求到的数据渲染到页面

http://localhost/item.html?id=10001

img

3. JVM进程缓存案例实现

上面我们已经部署了必要的环境,请确保你的环境正常启动,然后我们就开始业务需求的实现

(1) 启动mysql

systemctl start docker # 启动docker服务
docker restart mysql #启动mysql容器

(2) 启动Nginx

d:
cd nginx-1.18.0
start nginx.exe

我们下面会使用Caffeine来实现 JVM进程缓存案例

3. JVM进程缓存案例实现

上面我们已经部署了必要的环境,请确保你的环境正常启动,然后我们就开始业务需求的实现

(1) 启动mysql

systemctl start docker # 启动docker服务
docker restart mysql #启动mysql容器

(2) 启动Nginx

d:
cd nginx-1.18.0
start nginx.exe

我们下面会使用Caffeine来实现 JVM进程缓存案例

一、初始Caffeine

Caffeine是一个缓存技术相关的库,读 kě fì,翻译过来就是咖啡因

【本地进程缓存】

缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力

我们把缓存分为如下两类

1、分布式(往往用在集群的环境下)缓存,例如Redis

  • 优点:存储容量更大、可靠性更好、可以在集群间共享
  • 缺点:访问缓存有网络开销
  • 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享

2、进程本地缓存,例如HashMap、GuavaCache

  • 优点:读取本地内存,没有网络开销,速度更快
  • 缺点:存储容量有限、可靠性较低、无法共享
  • 场景:性能要求较高,缓存数据量较小

Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine

GitHub地址:https://github.com/ben-manes/caffeine

Caffeine提供了三种缓存驱逐策略,也就是缓存过期策略:

1、基于容量:设置缓存的数量上限

// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
    .maximumSize(1) // 设置缓存大小上限为 1 
    .build();

2、基于时间:设置缓存的有效时间

// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
    .expireAfterWrite(Duration.ofSeconds(10)) // 设置缓存有效期为 10 秒,从最后一次写入开始计时
    .build();

3、基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用

在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐

二、Caffeine基本演示

用idea打开item-service项目,在item-service项目进行Caffeine基本API的使用

第一步(已做可跳过): 在item-service项目的pom.xml添加如下

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

第二步(已做可跳过): 打开CaffeineTest类,写入如下

package com.heima.item.test;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.junit.jupiter.api.Test;

import java.time.Duration;

public class CaffeineTest {

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

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

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

        // 取数据,不存在则去数据库查询
        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"));
    }

    /*
     基于时间设置驱逐策略:
     */
    @Test
    void testEvictByTime() throws InterruptedException {
        // 创建缓存对象
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(Duration.ofSeconds(3)) // 设置缓存有效期为 3 秒
                .build();
        // 存数据
        cache.put("gf", "柳岩");
        // 获取数据
        System.out.println("gf: " + cache.getIfPresent("gf"));
        // 休眠一会儿
        Thread.sleep(1200L);
        System.out.println("gf: " + cache.getIfPresent("gf"));
    }
}

第三步: 运行CaffeineTest类的testBasicOps方法,测试在缓存中存取数据的基本用法

img

第四步: 运行CaffeineTest类的testEvictByNum方法,测试给缓存设置一个过期策略,例如当缓存缓存上限超过1,那么旧缓存就会被清理

img

第五步: 运行CaffeineTest类的testEvictByTime方法,测试给缓存设置一个过期策略,例如当缓存缓存时间超过3秒,那么旧缓存就会被清理

img

三、Caffeine实现进程缓存

案例: 实现商品的查询的本地进程缓存,利用Caffeine实现下列需求

1、给根据id查询商品的业务添加缓存,缓存未命中时查询数据库

2、给根据id查询商品库存的业务添加缓存,缓存未命中时查询数据库

3、缓存初始大小为100

4、缓存上限为10000

具体操作如下

第一步(已做可跳过): 在item-service项目的pom.xml添加如下

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

第二步: 在item-service项目的com.heima.item目录新建config.CaffeineConfig类,写入如下

package com.heima.item.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 35238
 * @date 2023/6/26 0026 22:41
 */
@Configuration
public class CaffeineConfig {

    @Bean
    //下面的Cache接口是caffeine提供的
    public Cache<Long, Item> itemCache(){
        return Caffeine.newBuilder()
                //缓存初始大小为100
                .initialCapacity(100)
                //缓存上限为10000,写成10_000会被自动处理成10000
                .maximumSize(10_000)
                .build();
    }

    @Bean
    //下面的Cache接口是caffeine提供的
    public Cache<Long, ItemStock> stockCache(){
        return Caffeine.newBuilder()
                //缓存初始大小为100
                .initialCapacity(100)
                //缓存上限为10000,写成10_000会被自动处理成10000
                .maximumSize(10_000)
                .build();
    }
}

img

第三步: 查看ItemController类,分析一下我们要给什么请求加缓存

img

第四步: 在ItemController类添加如下。也就是引入依赖,修改findById、findStockById方法

//注入bean,用来做商品缓存
@Autowired
private Cache<Long,Item> itemCache;
    
//注入bean,用来做库存缓存
@Autowired
private Cache<Long,ItemStock> stockCache;

@GetMapping("/{id}")
public Item findById(@PathVariable("id") Long id){
	//给这个请求添加缓存功能。使用get,实现自动查缓存,缓存没查到查数据库
	//get的第一个参数是根据id优先查缓存,第二个参数是如果缓存没查到就根据Lambda表达式来查数据库
	return itemCache.get(id,key -> itemService.query()
		.ne("status", 3).eq("id", key)
		.one());
}

@GetMapping("/stock/{id}")
public ItemStock findStockById(@PathVariable("id") Long id){
	//给这个请求添加缓存功能。使用get,实现自动查缓存,缓存没查到查数据库
	//get的第一个参数是根据id优先查缓存,第二个参数是如果缓存没查到就根据Lambda表达式来查数据库
	return stockCache.get(id,key -> stockService.getById(key));
}

img

第五步: 重新运行ItemApplication引导类,浏览器测试商品功能

http://localhost:8081/item/10001

img

img

第六步: 重新运行ItemApplication引导类,浏览器测试库存功能

http://localhost:8081/item/stock/10001

img

img

4. Lua语法

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能

官网:https://www.lua.org/。Lua读 lū ǎ

img

一、安装Lua

第一步: 打开官网

img

第二步: 在linux终端输入如下。注意CentOS自带Lua环境

img

curl -R -O http://www.lua.org/ftp/lua-5.4.6.tar.gz
tar zxf lua-5.4.6.tar.gz
cd lua-5.4.6
make all test

img

二、初识Lua

第一步: 在Linux虚拟机的/root目录下,新建LuaDemo目录,里面新建一个hello.lua文件

cd /root
mkdir LuaDemo
cd LuaDemo && touch hello.lua
vi hello.lua

第二步: 在hello.lua文件里面写入如下

print("Hello World")

第三步: 运行hello.lua文件

cd /root/LuaDemo
lua hello.lua

img

三、变量和循环

【数据类型】

数据类型描述
nil这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)
boolean包含两个值:false和true
number表示双精度类型的实浮点数
string字符串由一对双引号或单引号来表示
function由 C 或 Lua 编写的函数
tableLua 中的表(table)其实是一个"关联数组"(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过"构造表达式"来完成,最简单构造表达式是{},用来创建一个空表

可以利用type函数测试给定变量或者值的类型:

img

【变量】

Lua声明变量的时候,并不需要指定数据类型,也可以去掉local表示全局:

-- 声明字符串
local str = 'hello'
-- 拼接字符串
local str2 = 'hello' .. 'world'
-- 声明数字
local num = 21
-- 声明布尔类型
local flag = true
-- 声明数组 key为索引的 table
local arr = {'java', 'python', 'lua'}
-- 声明table,类似java的map
local map =  {name='Jack', age=21}

访问table:

-- 访问数组,lua数组的角标从1开始
print(arr[1])
-- 访问table
print(map['name'])
print(map.name)

【循环】

用ipairs遍历数组:

-- 声明数组 key为索引的 table
local arr = {'java', 'python', 'lua'}
-- 遍历数组
for index,value in ipairs(arr) do
    print(index, value) 
end

用pairs遍历table:

-- 声明map,也就是table
local map = {name='Jack', age=21}
-- 遍历table
for key,value in pairs(map) do
   print(key, value) 
end

四、条件控制、函数

【函数】

定义函数的语法:

function 函数名( argument1, argument2..., argumentn)
    -- 函数体
    return 返回值
end

例如,定义一个函数,用来打印数组:

function printArr(arr)
    for index, value in ipairs(arr) do
        print(value)
    end
end

【条件控制】

类似Java的条件控制,例如if、else语法:

if(布尔表达式)
then
   --[ 布尔表达式为 true 时执行该语句块 --]
else
   --[ 布尔表达式为 false 时执行该语句块 --]
end

与java不同,布尔表达式中的逻辑运算是基于英文单词:

操作符描述示例
and逻辑与操作符。 若 A 为 false,则返回 A,否则返回 B(A and B) 为 false
or逻辑或操作符。 若 A 为 true,则返回 A,否则返回 B(A or B) 为 true
not逻辑非操作符。与逻辑运算结果相反,如果条件为 true,逻辑非为 falsenot(A and B) 为 true

案例: 自定义一个函数,可以打印table,当参数为nil时,打印错误信息。具体操作如下

第一步: 在/root/LuaDemo目录新建FunctionDemo.lua文件,写入如下

cd /root/LuaDemo
touch FunctionDemo.lua
vi FunctionDemo.lua
local function printXxx(arr)
  if (not arr) then
    print('数组不能为空!')
    return nil
  end
  for i, val in ipairs(arr) do
    print(val)
  end
end

local arr1 = {100,200,300}

printXxx(arr1)
printXxx(nil)

img

多级缓存的最终实现

一、安装OpenResty

OpenResty是一个基于 Nginx的高性能 Web 平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点:

1、具备Nginx的完整功能

2、基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块

3、允许使用Lua自定义业务逻辑、自定义库

官方网站: https://openresty.org/cn/。在下面的操作中,你完全可以把OpenResty认为是Nginx,并且OpenResty是基于Nginx实现的

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

yum install -y pcre-devel openssl-devel gcc --skip-broken

第二步: 安装OpenResty仓库。在你的 CentOS 系统中添加 openresty 仓库,这样就可以便于未来安装或更新软件包

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

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

yum install -y yum-utils

第三步: 安装OpenResty软件包

yum install -y openresty

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

yum install -y openresty-opm

第五步: 目录结构。默认情况下,OpenResty安装的目录是:/usr/local/openresty

注意其中的nginx目录,OpenResty就是在Nginx基础上集成了一些Lua模块

cd /usr/local/openresty
ll

或者直接执行下面那条命令
ll /usr/local/openresty

img

第七步: 配置nginx的环境变量,以后就能在任意目录启动和运行

打开配置文件:

vi /etc/profile

在最下面加入两行:

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

第八步: 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;
        }
    }
}

第九步: 启动和运行。由于OpenResty底层是基于Nginx的,查看OpenResty目录的nginx目录,结构与windows中安装的nginx基本一致

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

# 启动nginx,就是启动OpenResty
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stop
# 查看nginx运行状态
ps -ef | grep nginx

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

img

二、OpenResty快速入门

商品详情页面目前展示的是假数据,在浏览器的控制台可以看到查询商品信息的请求:

img

而这个请求最终被反向代理到虚拟机的OpenResty集群:

img

需求:实现商品详情页数据查询。在OpenResty中接收这个请求,并返回一段商品的假数据

第一步: 在nginx.conf的http下面,添加对OpenResty的Lua模块的加载

vi /usr/local/openresty/nginx/conf/nginx.conf
# 加载lua模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
# 加载c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";

第二步: 在nginx.conf的server下面,添加对/api/item这个路径的监听

vi /usr/local/openresty/nginx/conf/nginx.conf
location /api/item {
    # 响应类型,这里返回json
    default_type application/json;
    # 响应数据由 lua/item.lua这个文件来决定
    content_by_lua_file lua/item.lua;
}

img

第三步: 在 /usr/local/openresty/nginx 目录创建lua文件夹,在lua文件夹新建item.lua文件

cd /usr/local/openresty/nginx
mkdir lua
cd lua && touch item.lua

第四步: 在item.lua文件写入如下

-- 返回假数据,这里的ngx.say()函数,就是写数据到Response中
ngx.say('{"id":10001,"name":"SALSA AIR","title":"RIMOWA 999寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":99999,"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}')

img

第五步: 重新加载配置,可在任意目录执行下面那条命令。如果下面那条命令执行失败,就手打上面第一步、第二步的nginx.conf文件的命令,不要粘贴

nginx
nginx -s reload

第六步: 确保你Windows的Nginx正在运行。win+r,输入cmd并回车,然后输入下面的命令启动Nginx

d:
cd nginx-1.18.0
start nginx.exe
确保你Windows的Nginx配置正确

img

第七步: 不需要启动idea,不需要启动docker里面的mysql容器。浏览器访问如

localhost/item.html?id=10001

img

三、请求参数处理

刚刚我们在OpenResty快速入门中,学习了如何把假数据返回给用户,但是在实际开发中,我们需要根据不同请求去查询不同的商品数据,那么我们就需要先得到不同请求的参数,如何得到,下面就来学习请求参数处理,在OpenResty里面获取用户的请求参数,也就是如何在请求路径里获取参数

OpenResty提供了各种API用来获取不同类型的请求参数,如下表

参数格式参数示例参数解析代码示例
路径占位符/item/1001①正则表达式匹配: location ~ /item/(\d+) { content_by_lua_file lua/item.lua;}; ②匹配到的参数会存入ngx.var数组中,可以用角标获取,例如local id = ngx.var[1]
请求头id: 1001获取请求头,返回值是table类型,例如local headers = ngx.req.get_headers()
Get请求参数?id=10获取GET请求参数,返回值是table类型,例如local getParams = ngx.req.get_uri_args()
Post表单参数id=1001①读取请求体ngx.req.read_body(); ②获取POST表单参数,返回值是table类型,例如local postParams = ngx.req.get_post_args()
JSON参数{“id”: 1001}①读取请求体ngx.req.read_body(); ②获取body中的json参数,返回值是string类型,例如local jsonBody = ngx.req.get_body_data()

在查询商品信息的请求中,通过路径占位符的方式,传递了商品id到后台:

img

需求: 在OpenResty中接收这个请求,并获取路径中的id信息,拼接到结果的json字符串中返回

第一步: 在nginx.conf的server下面,给/api/item监听路径添加正则表达式,~表示正则表达式

我把整个代码都放出来了,不要复制,只是在原来的基础上修改了一点点,手打就行

vi /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;
    # 加载lua模块
    lua_package_path "/usr/local/openresty/lualib/?.lua;;";
    # 加载c模块
    lua_package_cpath "/usr/local/openresty/lualib/?.so;;";

    server {
        listen       8081;
        server_name  localhost;
        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;
        }
    }
}

img

第二步: 把/usr/local/openresty/nginx/lua 目录下的item.lua文件,修改为如下

vi /usr/local/openresty/nginx/lua/item.lua
-- 获取路径参数
local id = ngx.var[1]
-- 返回结果

ngx.say('{"id":'..id ..',"name":"SALSA AIR","title":"RIMOWA 999寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":99999,"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}')

第三步: 在任意目录执行如下命令,重新加载nginx(也就是OpenResty)的配置。执行报错的话,就自己把第一步的代码手打,不要直接复制粘贴

nginx
nginx -s reload

第四步: 确保你Windows的Nginx正在运行。win+r,输入cmd并回车,然后输入下面的命令启动Ngin

d:
cd nginx-1.18.0
start nginx.exe

确保你Windows的Nginx配置正确

img

第五步: 不需要启动idea,不需要启动docker里面的mysql容器。浏览器访问如下

http://localhost/item.html?id=10002         访问本地的页面 在这个页面会通过ajax访问具体的接口 然后通过Nginx反向代理到虚拟机里的OpenResty

img

四、查询Tomcat

img

【nginx内部发送Http请求】

nginx提供了内部API用以发送http请求:

local resp = ngx.location.capture("/path",{
    method = ngx.HTTP_GET,   -- 请求方式
    args = {a=1,b=2},  -- get方式传参数
    body = "c=3&d=4" -- post方式传参数
})

返回的响应内容包括如下:

1、resp.status:响应状态码

2、resp.header:响应头,是一个table

3、resp.body:响应体,就是响应数据

注意:上面的的path是路径,并不包含IP和端口。这个请求会被nginx内部的server监听并处理。但是我们希望这个请求发送到Tomcat服务器,所以还需要编写一个server来对这个路径做反向代理:

location /path {
     # 这里是windows电脑的ip和Java服务端口,需要确保windows防火墙处于关闭状态
     proxy_pass http://192.168.127.1:8081; 
 }

【封装http查询的函数】

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

在/usr/local/openresty/lualib目录下创建common.lua文件,在common.lua文件写入如

vi /usr/local/openresty/lualib/common.lua
-- 封装函数,发送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
-- 将方法导出
local _M = {  
    read_http = read_http
}  
return _M

【使用Http函数查询数据】

我们刚才已经把http查询的请求封装为一个函数,放到OpenResty函数库中,接下来就可以使用这个库了

把 /usr/local/openresty/nginx/lua 目录的item.lua文件,修改为如下

vi /usr/local/openresty/nginx/lua/item.lua
-- 引入自定义工具模块
local common = require("common")local read_http = common.read_http
-- 获取路径参数
local id = ngx.var[1]
-- 根据id查询商品
local itemJSON = read_http("/item/".. id, nil)
-- 根据id查询商品库存
local itemStockJSON = read_http("/item/stock/".. id, nil)

查询到的是商品、库存的json格式数据,我们需要将两部分数据组装,需要用到JSON处理函数库

【JSON结果处理】

OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。

官方地址: https://github.com/openresty/lua-cjson/

引入cjson模块:

local cjson = require "cjson"

序列化:

local obj = {
    name = 'jack',
    age = 21
}
local json = cjson.encode(obj)

反序列化:

local json = '{"name": "jack", "age": 21}'
-- 反序列化
local obj = cjson.decode(json);
print(obj.name)

案例: 获取请求路径中的商品id信息,根据id向Tomcat查询商品信息,注意这里要修改item.lua,满足下面的需求

1、获取请求参数中的id根据id

2、向Tomcat服务发送请求,查询商品信息

3、根据id向Tomcat服务发送请求,查询库存信息

4、组装商品信息、库存信息,序列化为JSON格式并返回

第一步: 在nginx.conf的server下面,再添加一个location

我把整个代码都放出来了,不要复制,只是在原来的基础上修改了一点点,手打就行

vi /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;
    # 加载lua模块
    lua_package_path "/usr/local/openresty/lualib/?.lua;;";
    # 加载c模块
    lua_package_cpath "/usr/local/openresty/lualib/?.so;;";

    server {
        listen       8081;
        server_name  localhost;

	   # 给/item请求地址做反向代理,ip地址是Windows的地址
	   # 作用是用户请求/item的时候,实际请求的是windows的idea的tomcat
        location /item {
            proxy_pass http://192.168.127.1:8081;
        }
        
        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;
        }
    }
}

img

第二步: 在/usr/local/openresty/lualib目录下创建common.lua文件,在common.lua文件写入如下

cd /usr/local/openresty/lualib
touch common.lua
vi common.lua
-- 封装函数,发送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
-- 将方法导出
local _M = {  
    read_http = read_http
}  
return _M

第三步: 为了实现把在lua文件中,把JSON类型的数据转为lua的table类型,我们需要用到OpenResty提供的cjson模块(也就是/usr/local/openresty/lualib 目录下的cjson.so文件),就可以处理JSON的序列化和反序列化。把 /usr/local/openresty/nginx/lua 目录下的item.lua文件修改为如下

vi /usr/local/openresty/nginx/lua/item.lua
-- 导入写好的common.lua文件,也就是导入common.lua函数库
local common = require('common')
local read_http = common.read_http
-- 导入OpenResty提供的cjson模块(也就是cjson.so文件),用于下面拼接商品、库存信息时,将JSON转化为lua的table类型
local cjson = require('cjson')

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

-- 查询商品信息
local itemJSON = read_http("/item/" .. id, nil)

-- 查询库存信息
local stockJSON = read_http("/item/stock/" .. id, nil)

-- 调用cjson函数,拼接商品、库存信息,一起返回给用户,也就是把商品、库存信息一起返回到页面
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
item.stock = stock.stock
item.sold = stock.sold

-- 上面的item此时是table类型,我们还要序列化回来,也就是把table类型转为JSON类型
-- 返回结果,这次没有假数据,全部都是通过虚拟机nginx(也就是OpenResty)去请求Windows的idea的Tomcat拿到的
ngx.say(cjson.encode(item))

第四步: 在任意目录执行如下,表示重新加载Nginx(也就是OpenResty)的配置文件

nginx
nginx -s reload

第五步: 由于现在是虚拟机的Nginx(也就是OpenResty)向Windows的idea的Tomcat查询数据,所以我们需要先启动一些必要的环境,如下

(1) 启动docker里面的mysql容器

systemctl start docker # 启动docker服务
docker restart mysql #启动mysql容器

(2) 启动Windows的Nginx。win+r,输入cmd回车,然后输入如下

d:
cd nginx-1.18.0
start nginx.exe

(3) 启动idea的item-service项目的ItemApplication引导类,浏览器访问如下

http://localhost/item.html?id=10003

img

五、Tomcat集群的负载均衡

img

具体操作如下

第一步: 在nginx.conf的http里面定义upstream作为要访问的地址(也就是Tomcat集群),把Nginx的负载均衡算法设置为基于url地址的负载均衡算法,然后把proxy_pass值,从原来的ip地址修改为集群名称,这样虚拟机的Nginx就会访问idea的Tomcat集群

我把整个代码都放出来了,不要复制,只是在原来的基础上修改了一点点,手打就行

vi /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;
    # 加载lua模块
    lua_package_path "/usr/local/openresty/lualib/?.lua;;";
    # 加载c模块
    lua_package_cpath "/usr/local/openresty/lualib/?.so;;";

    upstream tomcat-cluster {
        hash $request_uri;  #保证了每个id固定就访问到固定的服务器上
        server 192.168.127.1:8081;
        server 192.168.127.1:8082;
    }

    server {
        listen       8081;
        server_name  localhost;

	   # 给/item请求地址做反向代理,ip地址是Windows的地址
	   # 作用是用户请求/item的时候,实际请求的是windows的idea的tomcat
        location /item {
            proxy_pass http://tomcat-cluster;
        }
        
        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;
        }
    }
}

img

第二步: 在任意目录执行如下,表示重新加载Nginx(也就是OpenResty)的配置文件

nginx
nginx -s reload

第三步: 由于现在是虚拟机的Nginx(也就是OpenResty)向Windows的idea的Tomcat查询数据,所以我们需要先启动一些必要的环境,如下

(1) 启动docker里面的mysql容器

systemctl start docker # 启动docker服务
docker restart mysql #启动mysql容器

(2) 启动Windows的Nginx。win+r,输入cmd回车,然后输入如下

d:
cd nginx-1.18.0
start nginx.exe

(3) 如何启动两个Tomcat服务。打开idea软件,在item-service项目,进行下面操作

img

img

img

(4) 测试。浏览器访问10001~10004,看看哪个Tomcat响应来自虚拟机Nginx的请求,响应的规律是什么

http://localhost/item.html?id=10004

img

img

六、Redis缓存预热

img

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

缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。对于我们现在这个项目来说,由于数据量较少,所以可以在启动时将所有数据都放入缓存中

【缓存预热】

利用Docker安装Redis

docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes

在item-service服务中引入Redis依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置Redis地址

spring:
  redis:
    host: 192.168.127.180

编写初始化类

package com.heima.item.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import com.heima.item.service.IItemService;
import com.heima.item.service.IItemStockService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author 35238
 * @date 2023/6/28 0028 22:47
 */
@Component
//实现redis缓存的预热功能
public class RedisHandler implements InitializingBean {
    
    @Autowired
    //注入redis提供的StringRedisTemplate类,用于操作redis
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    //注入写好的IItemService接口,用于查询商品信息
    private IItemService itemService;
    
    @Autowired
    //注入写好的IItemStockService接口,用于查询库存信息
    private IItemStockService stockService;
    
    @Autowired
    //注入spring提供的JSON处理工具,用于序列化为JSON。为方便使用,我们写成静态常量
    private static final ObjectMapper MAPPER = new ObjectMapper();
    
    @Override
    //初始化缓存
    public void afterPropertiesSet() throws Exception {   //在完成初始化后执行  成员变量都赋值后
        
        //查询商品信息
        List<Item> itemList = itemService.list();
        //把查到的商品信息放入缓存
        for (Item item : itemList) {
            //把item(拿到的item是lua语言的table类型)序列化为JSON
            String json = MAPPER.writeValueAsString(item);//把任意对象转为json字符串
            //把序列化后的item存入redis
            //由于商品信息的key存入redis后,可能与后面的库存信息的key重复,
            //所以在存key的时候,我们要给key加上前缀,其实就是拼接一个字符串,冒号用于分隔便于观察
            redisTemplate.opsForValue().set("item:id:" +item.getId(),json);
        }

        //查询库存信息
        List<ItemStock> stockList = stockService.list();
        //把查到的库存信息放入缓存
        for (ItemStock stock : stockList) {
            //把item(拿到的item是lua语言的table类型)序列化为JSON
            String json = MAPPER.writeValueAsString(stock);//把任意对象转为json字符串
            //把序列化后的item存入redis
            //由于库存信息的key存入redis后,可能与前面的商品信息的key重复,
            //所以在存key的时候,我们要给key加上前缀,其实就是拼接一个字符串,冒号用于分隔便于观察
            redisTemplate.opsForValue().set("stock:id:" +stock.getId(),json);
        }
        
    }
}

七、查询Redis缓存

刚刚我们已经实现了redis(虚拟机的Redis)的缓存预热功能,提前把数据存入了Redis当中,下面我们就需要需要修改OpenResty(就是虚拟机的Nginx)的逻辑,让OpenResty优先查询Redis,Redis未命中再查询Tomcat(idea的Tomcat),我们需要先解决如何在OpenResty来操作Redis,解决: 使用OpenResty提供的操作Redis的模块

首先: 引入Redis模块,并初始化Redis对象

-- 引入redis模块
local redis = require("resty.redis")
-- 初始化Redis对象
local red = redis:new()
-- 设置Redis超时时间(建立连接的超时时间,发送请求的超时时间,响应结果的超时时间)
red:set_timeouts(1000, 1000, 1000)

然后: 封装函数,用来释放Redis连接,其实是放入连接池

-- 关闭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

然后: 封装函数,作用是从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

案例: 查询商品时,优先Redis缓存查询,需求如下

1、修改item.lua,封装一个函数read_data,实现先查询Redis,如果未命中,再查询tomcat

2、修改item.lua,查询商品和库存时都调用read_data这个函数

-- 封装函数,先查询redis,再查询http
local function read_data(key, path, params)
    -- 查询redis
    local resp = read_redis("127.0.0.1", 6379, key)
    -- 判断redis是否命中
    if not resp then
        -- Redis查询失败,查询http
        resp = read_http(path, params)
    end
    return resp
end

具体操作如下

第一步: 把/usr/local/openresty/lualib目录的common.lua文件,修改为如下

-- 导入OpenResty提供的Redis模块
local redis = require('resty.redis')

-- 初始化导入的Redis,也就是创建redis对象
local red = redis:new()

-- 为redis设置超时时间,括号内的参数分别表示'建立连接的超时时间'、'发送请求的超时时间'、'响应结果的超时时间'
red:set_timeouts(1000,1000,1000)

-- 释放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

-- 建立Redis的连接,并且读数据
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

-- 封装函数,发送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查询失败, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 将方法导出
local _M = {  
    read_http = read_http,
    read_redis = read_redis
}  
return _M

img

第二步: 把/usr/local/openresty/nginx/lua目录的item.lua文件,修改为如下

-- 导入写好的common.lua文件的common.lua函数、read_redis函数
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis

-- 导入OpenResty提供的cjson模块(也就是cjson.so文件),用于下面拼接商品、库存信息时,将JSON转化为lua的table类型
local cjson = require('cjson')

-- 封装函数
function read_data(key, path, params)
	-- 先查询Redis,也就是先查询虚拟机的Redis
	local resp = read_redis("192.168.127.180", 6379, key)
	-- 判断查询结果,如果未命中Redis,再查询数据库
	if not resp then
		-- 记录日志
		ngx.log("redis查询未命中,将要去查询数据库。查不到的key是: ",key)
		-- redis查询未命中 就查询数据库
		resp = read_http(path, params)
	end
	return resp
end

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

-- 查询商品信息,调用上面封装好的read_data函数。注意下面括号的第一个参数是Redis的key(用了前缀拼接),要跟你的idea的RedisHandler类里面的key一致(当时也用了前缀拼接)
local itemJSON = read_data("item:id:" .. id, "/item/" .. id, nil)

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

-- 调用cjson函数,拼接商品、库存信息,一起返回给用户,也就是把商品、库存信息一起返回到页面
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
item.stock = stock.stock
item.sold = stock.sold

-- 上面的item此时是table类型,我们还要序列化回来,也就是把table类型转为JSON类型
-- 返回结果,这次没有假数据,全部都是通过虚拟机nginx(也就是OpenResty)去请求Windows的idea的Tomcat拿到的
ngx.say(cjson.encode(item))

img

第三步: 启动nginx,就是启动OpenResty。不执行这一步的话,下面的第四步执行不了

nginx

第四步: 在任意目录执行如下,表示重新加载Nginx(也就是OpenResty)的配置文件

nginx -s reload

第五步: 需要先启动一些必要的环境,如下

(1) 启动docker里面的mysql容器、redis容器

systemctl start docker # 启动docker服务
docker restart mysql #启动mysql容器
docker start redis #启动redis容器

(2) 启动Windows的Nginx。win+r,输入cmd回车,然后输入如下

d:
cd nginx-1.18.0
start nginx.exe

第六步: 在idea的item-service项目,运行ItemApplication引导类、ItemApplication2引导类。让Nginx(也就是虚拟机的OpenResty)先去idea的Tomcat读取数据存入Redis(虚拟机的redis容器),然后停止运行ItemApplication引导类、ItemApplication2引导类,去浏览器访问如下地址,测试redis是否生效,也就是让Nginx去查询Redis,而不是查询Tomcat

http://localhost/item.html?id=10003

img

八、Nginx本地缓存

img

OpenResty为Nginx提供了 shard dict(中文意思是共享词典) 的功能,可以在nginx的多个worker之间共享数据,实现缓存功能

1、开启共享字典,在nginx.conf的http下添加配置:

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

2、操作共享字典:

-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache
-- 存储, 指定key、value、过期时间,单位s,默认为0代表永不过期
item_cache:set('key', 'value', 1000)
-- 读取
local val = item_cache:get('key')

案例: 在查询商品时,优先查询OpenResty的本地缓存。需求如下

1、修改item.lua中的read_data函数,优先查询本地缓存,未命中时再查询Redis、Tomcat

2、查询Redis或Tomcat成功后,将数据写入本地缓存,并设置有效期

3、商品基本信息,有效期30分钟

4、库存信息,有效期1分钟

具体操作如下

第一步: 把/usr/local/openresty/nginx/conf目录的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;
    # 加载lua模块
    lua_package_path "/usr/local/openresty/lualib/?.lua;;";
    # 加载c模块
    lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
    # 添加共享词典功能,也就是本地缓存功能。item_cache是自定义缓存名称,150m是缓存最多存到150MB
    lua_shared_dict item_cache 150m;

    upstream tomcat-cluster {
        hash $request_uri;
        server 192.168.127.1:8081;
        server 192.168.127.1:8082;
    }

    server {
        listen       8081;
        server_name  localhost;

	   # 给/item请求地址做反向代理,ip地址是Windows的地址
	   # 作用是用户请求/item的时候,实际请求的是windows的idea的tomcat
        location /item {
            proxy_pass http://tomcat-cluster;
        }
        
        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;
        }
    }
}

img

第二步: 把/usr/local/openresty/nginx/lua目录的item.lua文件,修改为如下

-- 导入写好的common.lua文件的common.lua函数、read_redis函数
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis

-- 导入OpenResty提供的cjson模块(也就是cjson.so文件),用于下面拼接商品、库存信息时,将JSON转化为lua的table类型
local cjson = require('cjson')

-- 导入OpenResty提供的共享词典,也就是导入Nginx本地缓存的模块
local item_cache = ngx.shared.item_cache

-- 封装函数。注意expire是下面需要把数据存入Nginx时,的缓存有效期,单位是秒
function read_data(key, expire, path, params)
	-- 不优先查Redis,而是优先查询Nginx(也就是OpenResty)本地缓存
	local val = item_cache:get(key)
	-- 判断得到的val是否为空,也就是Nginx本地缓存里面是否有用户要查询的数据
	if not val then
		ngx.log(ngx.ERR, "Nginx本地缓存查询未命中,将要去查询redis。查不到的key是: ",key)
		-- 然后才查询Redis,也就是先查询虚拟机的Redis
		val = read_redis("192.168.127.180", 6379, key)
		-- 判断查询结果,如果未命中Redis,再查询数据库
		if not val then
			-- 记录日志
			ngx.log(ngx.ERR, "redis查询未命中,将要去查询数据库。查不到的key是: ",key)
			-- redis查询未命中
			val = read_http(path, params)
		end
	end
	-- Nginx本地缓存->Redis->mysql,经过这一轮必然会查询成功,查询成功后,不着急返回,先把数据写入Nginx本地缓存,方便下次直接走Nginx本地缓存
	item_cache:set(key, val, expire)
	-- 返回数据
	return val
end

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

-- 查询商品信息,调用上面封装好的read_data函数。注意下面括号的第一个参数是Redis的key(用了前缀拼接),要跟你的idea的RedisHandler类里面的key一致(当时也用了前缀拼接)
local itemJSON = read_data("item:id:" .. id, 1800, "/item/" .. id, nil)

-- 查询库存信息。注意第二个参数是缓存在存入本地Nginx时的缓存有效期,单位是秒
local stockJSON = read_data("stock:id:" .. id, 60, "/item/stock/" .. id, nil)

-- 调用cjson函数,拼接商品、库存信息,一起返回给用户,也就是把商品、库存信息一起返回到页面
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
item.stock = stock.stock
item.sold = stock.sold

-- 上面的item此时是table类型,我们还要序列化回来,也就是把table类型转为JSON类型
-- 返回结果,这次没有假数据,全部都是通过虚拟机nginx(也就是OpenResty)去请求Windows的idea的Tomcat拿到的
ngx.say(cjson.encode(item))

img

第三步: 启动nginx,就是启动OpenResty。不执行这一步的话,下面的第四步执行不了

nginx

第四步: 在任意目录执行如下,表示重新加载Nginx(也就是OpenResty)的配置文件

nginx -s reload

第五步: 需要先启动一些必要的环境,如下,

(1) 启动docker里面的mysql容器、redis容器

systemctl start docker # 启动docker服务
docker restart mysql #启动mysql容器
docker start redis #启动redis容器

(2) 启动Windows的Nginx。win+r,输入cmd回车,然后输入如下

d:
cd nginx-1.18.0
start nginx.exe

(3) 在idea的item-service项目,运行ItemApplication引导类、ItemApplication2引导类。注意必须运行着,如果关了的话,第一次查询必然请求报错,第二次开始就不报错了,建议开着,这样第一次查询就不会出错

第六步: 测试。先打开Nginx的日志,使用tail命令进行实时监控日志信息的输出

cd /usr/local/openresty/nginx/logs
tail -f error.log

img

第七步: 测试。浏览器第一次访问如下,这是第一次查询,失败很正常,因为Nginx本地缓存还没有这条数据。下图纠正一下,我们设置的过期时间是60秒

http://localhost/item.html?id=10003

img

第八步: 测试。浏览器第二次访问如下,由于在第一次查询后,Nginx本地缓存就会有了这条数据。所以第二次查询走的是Nginx本地缓存

http://localhost/item.html?id=10003

img

总结: 在 ‘5. 多级缓存的最终实现’ 里面,我们实现了一条如下的请求链

用户 -> Windows的Nginx -> 虚拟机的Nginx(也就是OpenResty) -> 虚拟机的Nginx(也就是OpenResty)的本地缓存 -> 虚拟机的Redis容器 ->

idea的Tomcat -> idea的Tomcat的进程缓存 -> 数据库

在实现多级缓存后,还有一个很重要的问题,就是如何实现这么多级缓存之间的数据同步,下面将深入学习缓存同步

缓存同步

一、数据同步策略

缓存数据同步的常见方式有三种:

1、设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新

  • 优势:简单、方便
  • 缺点:时效性差,缓存过期之前可能不一致
  • 场景:更新频率较低,时效性要求低的业务

2、同步双写:在修改数据库的同时,直接修改缓存

  • 优势:时效性强,缓存与数据库强一致
  • 缺点:有代码侵入,耦合度高
  • 场景:对一致性、时效性要求较高的缓存数据

3、异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据

  • 优势:低耦合,可以同时通知多个缓存服务
  • 缺点:时效性一般,可能存在中间不一致状态
  • 场景:时效性要求一般,有多个服务需要同步

【具体实现就下面两种,我们采取的是第二种,也就是基于Canal的异步通知】

基于MQ的异步通知(这个可以实现,但我们下面不演示这种,因为这种是要对项目代码进行修改)

img

基于Canal的异步通知(我们下面演示这个,优点是使用Canal监听数据库的变化,不需要修改项目代码,代码零侵入、零耦合,效率最高):

img

二、安装Canal

Canal 读 kē nǒu,译意为水道/管道/沟渠,canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费

GitHub的地址:https://github.com/alibaba/canal

Canal是基于mysql的主从同步来实现的,MySQL主从同步的原理如下:

1、MySQL master 将数据变更写入二进制日志( binary log),其中记录的数据叫做binary log events

2、MySQL slave 将 master 的 binary log events拷贝到它的中继日志(relay log)

3、MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

img

Canal就是把自己伪装成MySQL的一个slave节点,从而监听master的binary log变化。再把得到的变化信息通知给Canal的客户端,进而完成对其它数据库的同步

img

由于Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。我们在前面的 ‘2. JVM进程缓存案例搭建’ 里面搭建了一个mysql容器,以这个Docker运行的mysql容器为例。下面我们会在docker里面,使用Canal镜像创建Canal容器。安装Canal的具体操作如下:

第一步: 启动docker里面的mysql容器,并且进入mysql容器终端,登录mysql,查一下你业务使用的是哪个数据库

systemctl start docker # 启动docker服务
docker restart mysql #启动mysql容器
docker exec -it mysql bash #进入mysql容器的终端
mysql -u root -p123 #登录mysql
show databases #查看有哪些database

img

第二步: 在mysql容器挂载的日志文件,添加如下

vi /tmp/mysql/conf/my.cnf
log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=JvmDataShop

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

img

第三步: 再次进入mysql容器终端,登录mysql,创建一个用于数据同步的用户,用户名和密码都为canal,为canal用户授予所有权限

docker exec -it mysql bash #进入mysql容器的终端
mysql -u root -p123 #登录mysql
create user 'canal'@'%' IDENTIFIED by 'canal'; # 创建用户
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal'; #授权
FLUSH PRIVILEGES; # 刷新权限

第四步: 重启mysql容器,然后查看/tmp/mysql/data目录,里面有没有mysql-bin.000001文件

exit # 退出mysql的视图
exit # 退出mysql容器终端的视图
docker restart mysql
ll /tmp/mysql/data

img

第五步: 现在开始安装Canal。先创建一个网络,将mysql容器放到这个Docker网络中

docker network create heima
docker network connect heima mysql

img

第六步: 下载链接提供的canal的镜像压缩包到Windows,然后上传到虚拟机的 /root 目录

canal的镜像压缩包下载: https://cowtransfer.com/s/ff2ee7d6bf2644

img

第七步: 把 /root 目录下的canal镜像压缩包导入docker,然后查看docker里面是否有canal镜像

cd /root
docker load -i canal.tar
docker images

img

第八步: 在docker,使用canal镜像,来创建并运行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=JvmDataShop\\..* \
--network heima \
-d canal/canal-server:v1.1.5

# --name canal: 自定义容器名称,例如叫canal
# -p 11111:11111: 这是canal的默认监听端口
# -e canal.destinations: 自定义canal的实例名称
# -e canal.instance.master.address=mysql:3306:数据库地址和端口,由于canal和mysql会在同一网络,所以可以使用容器名进行访问,用来代替ip地址
# -e canal.instance.dbUsername=canal: 数据库用户名   主数据库提供的
# -e canal.instance.dbPassword=canal: 数据库密码
# -e canal.instance.filter.regex=: canal监听的是mysql的哪个库哪个表,监听的写法是有讲究的,常见语法如下

表名称监听支持的语法:

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

img

第九步: 查看canal容器是否创建和运行成功,查看的是docker的日志

docker logs -f canal

img

第十步: 验证canal容器是否跟mysql容器建立了连接。我们需要进入canal容器的终端,再通过tail命令,查看的是canal的日志、查看heima网络的日志

docker exec -it canal bash
tail -f canal-server/logs/canal/canal.log
tail -f canal-server/logs/heima/heima.log

img

纠正上图: canal不会去读取mysql的数据,只是监听mysql的数据有没有变化。heima并不是网络,而是上面第八步设置的canal的实例名称

三、Canal实现缓存同步

下面的操作是紧接着上面的 ‘二、安装Canal’,请先完成 ‘二、安装Canal’,再进行下面的操作

我们刚刚使用canal(虚拟机的canal容器)监听到了mysql(虚拟机的mysql容器)的数据日志,当canal监听到mysql的数据发生了变化,怎么才能告知redis(虚拟机的redis容器)或Tomcat(idea的item-service项目)呢 ? 下面就来学习

Canal提供了各种语言(包括java语言)的客户端,当Canal监听到binlog变化时,会通知Canal的客户端

我们只需要在idea的Tomcat里面编写这个客户端,让这个客户端在接收到canal的通知之后,去更新Redis,让Redis的缓存保持最新数据

img

我们使用GitHub上的第三方开源的canal-starter。地址: https://github.com/NormanGyllenhaal/canal-client

具体操作如下

第一步: 在item-service项目的pom.xml文件,添加如下

<!--引入canal依赖-->
<dependency>
	<groupId>top.javatool</groupId>
	<artifactId>canal-spring-boot-starter</artifactId>
	<version>1.2.1-RELEASE</version>
</dependency>

第二步: 在item-service项目的application.yml文件,添加如下

# 配置Canal的地址、实例名称
canal:
  destination: heima
  server: 192.168.127.180:11111

img

第三步: Canal推送给canal-client的是被修改的这一行数据(row),而我们引入的canal-client则会帮我们把行数据封装到Item实体类中。这个过程中需要知道数据库与实体的映射关系,要用到JPA的几个注解。把Item类修改为如下

package com.heima.item.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import javax.persistence.Column;
import java.util.Date;

@Data
@TableName("tb_item")
public class Item {
    @TableId(type = IdType.AUTO)

    @Id //表中的id字段
    private Long id;//商品id

    @Column(name = "name") //表中与属性名不一致的字段。我们目前是一样的,这里写的是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//不属于表中的字段,将来canal会忽略这个字段
    private Integer stock;

    @TableField(exist = false)
    @Transient//不属于表中的字段,将来canal会忽略这个字段
    private Integer sold;
}

img

第四步: 在RedisHandler类,添加如下,用于操作redis

//往Redis写入数据
public void saveItem(Item item){
	try {
		//把item(拿到的item是lua语言的table类型)序列化为JSON
		String json = MAPPER.writeValueAsString(item);//把任意对象转为json字符串
		//把序列化后的item存入redis
		//由于商品信息的key存入redis后,可能与后面的库存信息key重复,所以存key的时候,我们给key加上前缀,也就是拼接一个字符串,冒号用于分隔便于观察
		redisTemplate.opsForValue().set("item:id:" +item.getId(),json);
	} catch (JsonProcessingException e) {
		throw new RuntimeException(e);
	}
}
    
//删除在redis的数据
public void deleteItemById(Long id){
	redisTemplate.delete("item:id:" + id);
}

img

第五步: 编写监听器,监听Canal消息。在item-service项目的com.heima.item目录新建canal.ItemHandler类,写入如下

package com.heima.item.canal;

import com.github.benmanes.caffeine.cache.Cache;
import com.heima.item.config.RedisHandler;
import com.heima.item.pojo.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import top.javatool.canal.client.annotation.CanalTable;
import top.javatool.canal.client.handler.EntryHandler;

/**
 * @author 35238
 * @date 2023/6/30 0030 16:29
 */
@CanalTable("tb_item")//指定要监听的表
@Component
//下面的Item是tb_item表对应的实体类
public class ItemHandler implements EntryHandler<Item> {

    @Autowired
    //注入caffeine提供的Cache接口,用来操作Tomcat进程缓存(也就是JVM进程缓存)
    private Cache<Long, Item> itemCache;

    @Autowired
    //注入刚刚添加了操作redis方法的RedisHandler类
    private RedisHandler redisHandler;

    @Override
    //insert方法是canal提供的EntryHandler接口里面的
    //canal监听数据库,如果发生新增数据,要做什么事情
    public void insert(Item item) {
        //写数据到Tomcat进程缓存(也就是JVM进程缓存)。put方法的第一个参数是key,第二个参数是Value
        itemCache.put(item.getId(), item);
        //写数据到redis缓存
        redisHandler.saveItem(item);
    }

    @Override
    //insert方法是canal提供的EntryHandler接口里面的
    //canal监听数据库,如果发生修改数据,要做什么事情。下面的before是更新前的数据,after是更新后的数据
    public void update(Item before, Item after) {
        //写数据到Tomcat进程缓存(也就是JVM进程缓存)。put方法的第一个参数是key,第二个参数是Value
        itemCache.put(after.getId(), after);
        //修改数据到redis缓存
        redisHandler.saveItem(after);
    }

    @Override
    //insert方法是canal提供的EntryHandler接口里面的
    //canal监听数据库,如果发生删除数据,要做什么事情
    public void delete(Item item) {
        //删除数据到Tomcat进程缓存(也就是JVM进程缓存),根据id进行删除
        itemCache.invalidate(item.getId());
        //删除数据到redis缓存
        redisHandler.deleteItemById(item.getId());
    }
}

img

第六步: 需要先启动一些必要的环境,如下,

(1) 启动docker里面的mysql容器、redis容器

systemctl start docker # 启动docker服务
docker restart mysql #启动mysql容器
docker start redis #启动redis容器
docker start canal #启动canal容器
nginx  # 启动nginx,就是启动OpenResty

(2) 启动Windows的Nginx。win+r,输入cmd回车,然后输入如下

d:
cd nginx-1.18.0
start nginx.exe

第七步: 运行idea的item-service项目,运行ItemApplication引导类、ItemApplication2引导类。查看idea控制台信息

img

第八步: 测试。当我们对数据库(虚拟机的mysql容器)的数据进行修改时,是否能被canal监控到,从而告知Redis、Tomcat进程缓存去及时更新最新数据。注意由于我们没有去让canal操作Nginx(虚拟机的OpenResty)本地缓存,只是让canal去操作Redis、Tomcat,所以为了直观看到测试结果,直接访问本地,我们可以根据如下步骤进行

(1) 浏览器访问如下,拿到id为10001的数据

http://localhost:8081/item/10001

(2) 浏览器访问如下,我们去修改id为10001的数据,就能够触发canal

http://localhost:8081

img

img

(3) 查看idea控制台

img

(4) 浏览器访问如下,看一下数据有没有更新过来

http://localhost:8081/item/10001

img

(5) 那用户拿到的数据,到底是redis还是tomcat返回的呢 ? 首先,缓存刚更新的时候,用户拿的是tomcat的进程缓存,因为tomcat的处理速度比redis快,等redis也把自己的本地缓存更新之后,就会一直拿redis的本地缓存,如果Nginx也有,就优先拿Nginx的,也就是下面那条请求链

用户页面->通过ajax发送请求 -> Windows的Nginx -> 虚拟机的Nginx(也就是OpenResty) -> 虚拟机的Nginx(也就是OpenResty)的本地缓存 -> 虚拟机的Redis容器 ->

idea的Tomcat -> idea的Tomcat的进程缓存 -> 数据库

四、总结

注意在上面的请求链中,我们除了没有对Nginx的缓存进行同步(但是有缓存失效,我们当时设置的是60秒,所以其实对Nginx也是进行了缓存同步,只不过是被动的)之外,所以Nginx缓存一些实时性不高的数据,对redis和tomcat都进行了缓存同步,并且是使用canal做的缓存同步,数据永远都是最新的,redis和tomcat缓存一些实时性高的数据

整体来说,这个请求链是实现了多级缓存+缓存同步,也就是下图的全部功能

img

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值