Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))

Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第1张图片
Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第2张图片
Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第3张图片
Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第4张图片

Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第5张图片

1、JVM进程缓存

Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第6张图片
Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第7张图片

1.1 导入商品查询案例

案例导入说明

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

1.安装MySQL

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

1.1.准备目录

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

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

1.2.运行命令

进入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=123 \
 --privileged \
 -d \
 mysql:5.7.25

1.3.修改配置

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

1.4.重启

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

docker restart mysql

2.导入SQL

接下来,利用Navicat客户端连接MySQL,然后导入课前资料提供的sql文件:
Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第8张图片
其中包含两张表:

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

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

3.导入Demo工程

下面导入课前资料提供的工程:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第9张图片
项目结构如图所示:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第10张图片
其中的业务包括:

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

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

3.1.分页查询商品

com.heima.item.web包的ItemController中可以看到接口定义:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第11张图片

3.2.新增商品

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

Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第12张图片

3.3.修改商品

com.heima.item.web包的ItemController中可以看到接口定义:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第13张图片

3.4.修改库存

com.heima.item.web包的ItemController中可以看到接口定义:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第14张图片

3.5.删除商品

com.heima.item.web包的ItemController中可以看到接口定义:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第15张图片
这里是采用了逻辑删除,将商品状态修改为3

3.6.根据id查询商品

com.heima.item.web包的ItemController中可以看到接口定义:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第16张图片
这里只返回了商品信息,不包含库存

3.7.根据id查询库存

com.heima.item.web包的ItemController中可以看到接口定义:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第17张图片

3.8.启动

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

Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第18张图片
需要修改为自己的虚拟机地址信息、还有账号和密码。

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

4.导入商品查询页面

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

部署方式如图:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第19张图片
我们需要准备一个反向代理的nginx服务器,如上图红框所示,将静态的商品页面放到nginx目录中。

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

4.1.运行nginx服务

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

我们找到课前资料的nginx目录:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第20张图片
将其拷贝到一个非中文目录下,运行这个nginx服务。

运行命令:

start nginx.exe

然后访问 http://localhost/item.html?id=10001即可:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第21张图片

4.2.反向代理

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

打开控制台,可以看到页面有发起ajax查询数据:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第22张图片
而这个请求地址同样是80端口,所以被当前的nginx反向代理了。

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

Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第23张图片
其中的关键配置如下:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第24张图片
其中的192.168.150.101是我的虚拟机IP,也就是我的Nginx业务集群要部署的地方:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第25张图片

完整内容如下:

```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;
        }
    }
}

2.JVM进程缓存

为了演示多级缓存的案例,我们先准备一个商品查询的业务。

2.1.导入案例

参考课前资料的:《案例导入说明.md》Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第26张图片

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

  • 分布式缓存,例如Redis:
    • 优点:存储容量更大、可靠性更好、可以在集群间共享
    • 缺点:访问缓存有网络开销
    • 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
  • 进程本地缓存,例如HashMap、GuavaCache:
    • 优点:读取本地内存,没有网络开销,速度更快
    • 缺点:存储容量有限、可靠性较低、无法共享
    • 场景:性能要求较高,缓存数据量较小

我们今天会利用Caffeine框架来实现JVM进程缓存。

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

Caffeine的性能非常好,下图是官方给出的性能对比:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第27张图片
可以看到Caffeine的性能遥遥领先!

缓存使用的基本API:

@Test
void testBasicOps() {
    // 构建cache对象
    Cache<String, String> cache = Caffeine.newBuilder().build();

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

    // 取数据
    String gf = cache.getIfPresent("gf");
    System.out.println("gf = " + gf);

    // 取数据,包含两个参数:
    // 参数一:缓存的key
    // 参数二:Lambda表达式,表达式参数就是缓存的key,方法体是查询数据库的逻辑
    // 优先根据key查询JVM缓存,如果未命中,则执行参数二的Lambda表达式
    String defaultGF = cache.get("defaultGF", key -> {
        // 根据key去数据库查询数据
        return "柳岩";
    });
    System.out.println("defaultGF = " + defaultGF);
}

Caffeine既然是缓存的一种,肯定需要有缓存的清除策略,不然的话内存总会有耗尽的时候。

Caffeine提供了三种缓存驱逐策略:

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

    // 创建缓存对象
    Cache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(1) // 设置缓存大小上限为 1
        .build();
    
  • 基于时间:设置缓存的有效时间

    // 创建缓存对象
    Cache<String, String> cache = Caffeine.newBuilder()
        // 设置缓存有效期为 10 秒,从最后一次写入开始计时 
        .expireAfterWrite(Duration.ofSeconds(10)) 
        .build();
    
    
  • 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

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

2.3.实现JVM进程缓存

2.3.1.需求

利用Caffeine实现下列需求:

  • 给根据id查询商品的业务添加缓存,缓存未命中时查询数据库
  • 给根据id查询商品库存的业务添加缓存,缓存未命中时查询数据库
  • 缓存初始大小为100
  • 缓存上限为10000

2.3.2.实现

首先,我们需要定义两个Caffeine的缓存对象,分别保存商品、库存的缓存数据。

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

@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<Long, Item> itemCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }

    @Bean
    public Cache<Long, ItemStock> stockCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }
}

然后,修改item-service中的com.heima.item.web包下的ItemController类,添加缓存逻辑:

@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) {
        return itemCache.get(id, key -> itemService.query()
                .ne("status", 3).eq("id", key)
                .one()
        );
    }

    @GetMapping("/stock/{id}")
    public ItemStock findStockById(@PathVariable("id") Long id) {
        return stockCache.get(id, key -> stockService.getById(key));
    }
}

3.Lua语法入门

Nginx编程需要用到Lua语言,因此我们必须先入门Lua的基本语法。

3.1.初识Lua

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。官网:https://www.lua.org/Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第28张图片
Lua经常嵌入到C语言开发的程序中,例如游戏开发、游戏插件等。

Nginx本身也是C语言开发,因此也允许基于Lua做拓展。

3.1.HelloWorld

CentOS7默认已经安装了Lua语言环境,所以可以直接运行Lua代码。

1)在Linux虚拟机的任意目录下,新建一个hello.lua文件在这里插入图片描述
2)添加下面的内容

print("Hello World!")  

3)运行在这里插入图片描述

3.2.变量和循环

学习任何语言必然离不开变量,而变量的声明必须先知道数据的类型。

3.2.1.Lua的数据类型

Lua中支持的常见数据类型包括:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第29张图片
另外,Lua提供了type()函数来判断一个变量的数据类型:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第30张图片

3.2.2.声明变量

Lua声明变量的时候无需指定数据类型,而是用local来声明变量为局部变量:

-- 声明字符串,可以用单引号或双引号,
local str = 'hello'
-- 字符串拼接可以使用 ..
local str2 = 'hello' .. 'world'
-- 声明数字
local num = 21
-- 声明布尔类型
local flag = true

Lua中的table类型既可以作为数组,又可以作为Java中的map来使用。数组就是特殊的table,key是数组角标而已:

-- 声明数组 ,key为角标的 table
local arr = {'java', 'python', 'lua'}
-- 声明table,类似java的map
local map =  {name='Jack', age=21}

Lua中的数组角标是从1开始,访问的时候与Java中类似:

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

Lua中的table可以用key来访问:

-- 访问table
print(map['name'])
print(map.name)

3.2.3.循环

对于table,我们可以利用for循环来遍历。不过数组和普通table遍历略有差异。

遍历数组:

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

遍历普通table

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

3.3.条件控制、函数

Lua中的条件控制和函数声明与Java类似。

3.3.1.函数

定义函数的语法:

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

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

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

3.3.2.条件控制

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

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

与java不同,布尔表达式中的逻辑运算是基于英文单词:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第31张图片

3.3.3.案例

需求:自定义一个函数,可以打印table,当参数为nil时,打印错误信息

function printArr(arr)
    if not arr then
        print('数组不能为空!')
    end
    for index, value in ipairs(arr) do
        print(value)
    end
end

4.实现多级缓存

多级缓存的实现离不开Nginx编程,而Nginx编程又离不开OpenResty。

4.1.安装OpenResty

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

  • 具备Nginx的完整功能
  • 基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块
  • 允许使用Lua自定义业务逻辑自定义库

官方网站: https://openresty.org/cn/Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第32张图片
安装Lua可以参考课前资料提供的《安装OpenResty.md》:

安装OpenResty

1.安装

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

1)安装开发库

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

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

2)安装OpenResty仓库

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

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

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

yum install -y yum-utils 

然后再重复上面的命令

3)安装OpenResty

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

yum install -y openresty

4)安装opm工具

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

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

yum install -y openresty-opm

5)目录结构

默认情况下,OpenResty安装的目录是:/usr/local/openrestyRedis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第33张图片
看到里面的nginx目录了吗,OpenResty就是在Nginx基础上集成了一些Lua模块。

6)配置nginx的环境变量

打开配置文件:

vi /etc/profile

在最下面加入两行:

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

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

然后让配置生效:

source /etc/profile

2.启动和运行

OpenResty底层是基于Nginx的,查看OpenResty目录的nginx目录,结构与windows中安装的nginx基本一致:Redis--高级篇 D5 多级缓存(JVM进程缓存、Lua语法、OpenResty安装(通过lua扩展nginx))_第34张图片
所以运行方式与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

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

3.备注

加载OpenResty的lua模块:

#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块     
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";  

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

释放Redis连接API:

-- 关闭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数据的API:

-- 查询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

开启共享词典:

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

你可能感兴趣的:(lua,缓存,redis)