【微服务全家桶】-高级篇-4-多级缓存


0 多级缓存

在这里插入图片描述

在这里插入图片描述

首先需要导入商品案例

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

0.1 安装MySQL

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

0.1.1 准备目录

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

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

0.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

0.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

0.1.4 重启

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

docker restart mysql

0.2 导入SQL

Navicat连接mysql,创建utfmb4格式的heima数据库

接下来,利用Navicat客户端连接MySQL,然后导入提供的sql文件:

在这里插入图片描述
其中包含两张表:

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

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

0.3 导入Demo工程

下面导入提供的工程:
在这里插入图片描述
项目结构如图所示:

在这里插入图片描述

其中的业务包括:

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

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

0.3.1 分页查询商品

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

在这里插入图片描述

0.3.2 新增商品

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

在这里插入图片描述

0.3.3 修改商品

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

在这里插入图片描述

0.3.4 修改库存

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

在这里插入图片描述

0.3.5 删除商品

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

在这里插入图片描述

这里是采用了逻辑删除,将商品状态修改为3

0.3.6 根据id查询商品

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

在这里插入图片描述

这里只返回了商品信息,不包含库存

0.3.7 根据id查询库存

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

在这里插入图片描述

0.3.8 启动

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

在这里插入图片描述

需要修改为自己的虚拟机地址信息、还有账号和密码。

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

0.4 导入商品查询页面

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

部署方式如图:

在这里插入图片描述

我们需要准备一个反向代理的nginx服务器,如上图红框所示,将静态的商品页面放到nginx目录中。

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

0.4.1 运行nginx服务

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

我们找到课前资料的nginx目录:

在这里插入图片描述

将其拷贝到一个非中文目录下,运行这个nginx服务。

运行命令:

start nginx.exe

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

在这里插入图片描述

0.4.2 反向代理

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

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

在这里插入图片描述

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

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

在这里插入图片描述

其中的关键配置如下:

在这里插入图片描述

其中的192.168.150.101是我的虚拟机IP,也就是我的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.204.129: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;
        }
    }
}

1 JVM进程缓存

在这里插入图片描述

1.1本地进程缓存-Caffeine 入门demo

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

    @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);
    }
gf = 迪丽热巴
defaultGF = 柳岩

进程已结束,退出代码为 0

1.2 Caffine三种缓存驱逐策略

在这里插入图片描述

1.2.1 基于容量

    @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"));
    }

先延迟10ms,给清理线程一点时间,驱逐需要时间,所以都能查到

gf1: 柳岩
gf2: 范冰冰
gf3: 迪丽热巴

//Thread.sleep(10L);注释解除,则前两次存储被驱逐

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

1.2.2 基于时间

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

修改为10s

gf: 柳岩
gf: 柳岩

1.3 实现商品本地进程缓存

在这里插入图片描述

1.3.1 声明并注入Bean

因为对两个表进行缓存,所以是两个缓存机制,最好两个都声明成Bean放到Spring容器中去,所以在com.heima.item引入配置类config,里面缓存类CaffeineConfig,有两个Cache类的Bean对象itemCache和stockCache

@Configuration
public class CaffeineConfig {
    //商品Cache
    @Bean
    public Cache<Long, Item> itemCache() {
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10000)
                .build();
    }
    //库存Cache
    @Bean
    public Cache<Long, Integer> stockCache() {
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10000)
                .build();
    }
}

因为需要在查询时使用缓存,所以在Controller中修改。

首先Autowired注入刚刚的Bean对象

    @Autowired
    private Cache<Long,Item> itemCache;
    @Autowired
    private Cache<Long, ItemStock> stockCache;

1.3.2 修改查询业务

修改商品查询业务,使用itemCache

    @GetMapping("/{id}")
    public Item findById(@PathVariable("id") Long id){
        return itemService.query()
                .ne("status", 3).eq("id", id)
                .one();
    }

修改为

    @GetMapping("/{id}")
    public Item findById(@PathVariable("id") Long id){
        return itemCache.get(id, key -> itemService.query()
                .ne("status", 3)
                .eq("id", key)
                .one());
    }

修改库存查询业务,使用stockCache

    @GetMapping("/stock/{id}")
    public ItemStock findStockById(@PathVariable("id") Long id){
        return stockService.getById(id);
    }

修改为

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

1.3.3 测试

重启后访问 localhost:8081/item/10001

在这里插入图片描述

显示有一条访问数据库,当我们清空idea控制台,再次访问
在这里插入图片描述

没有显示访问数据库,说明是访问的缓存

于是我们在进行访问,查询库存 localhost:8081/item/stock/10001

在这里插入图片描述

2 Nginx开发语言-Lua

在这里插入图片描述

2.1 入门

2.1.1 Helloworld

centos自带lua

print("Hello,world")

shell控制台输入

[root@localhost mysql]# cd /tmp/Lua
[root@localhost Lua]# lua HelloWorld.lua 
Hello,world

2.1.2 变量和循环

在这里插入图片描述
在这里插入图片描述

lua中字符串使用…拼接的

local str1 = 'hello'..'world'

在这里插入图片描述

2.1.3 函数和条件控制

在这里插入图片描述
在这里插入图片描述

案例

在这里插入图片描述

arr1={'java','c++','python','lua','c'}
arr2={name1='jack',name2='rose',name3='sjb'}
arr3=nil

function printArr(arr)
    if(arr==nil)
    then
        print('arr==null')
        return 0
    else
        for key, value in pairs(arr) do
            print(key,value)
        end
    end
end

printArr(arr1)
print('================================')
printArr(arr2)
print('================================')
printArr(arr3)
[root@localhost Lua]# lua table-demp.lua 
1       java
2       c++
3       python
4       lua
5       c
================================
name1   jack
name3   sjb
name2   rose
================================
arr==null

3 多级缓存

3.1 OpenResty

在这里插入图片描述

3.1.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/openresty

在这里插入图片描述

看到里面的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

3.1.2 启动和运行

OpenResty底层是基于Nginx的,查看OpenResty目录的nginx目录,结构与windows中安装的nginx基本一致:

在这里插入图片描述

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

# 启动nginx
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stop

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

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

#user  nobody;
worker_processes  1;
error_log  logs/error.log;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       8081;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

在Linux的控制台输入命令以启动nginx:

nginx

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

3.1.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; 

3.2 OpenResty快速入门

案例

在这里插入图片描述

3.2.1 修改conf文件

在这里插入图片描述

修改openresty的nginx.conf下http,加载Lua模块并并且监听/api/item

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 {
            # 默认相应类型
            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;
        }
    }
}

因为content_by_lua_file lua/item.lua;决定返回的数据是在nginx下lua目录下的item.lua文件,

这是我们要编写的,没有就要创建!

3.2.2 编写item.lua文件

在这里插入图片描述

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

nginx -s reload

清除win上的nginx,再重启,重新访问http://localhost/item.html?id=10001

在这里插入图片描述

与之前静态页面已经不同

{"id":10001,"name":"SALSA AIR","title":"RIMOWA 21寸托运箱拉杆箱 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}

3.3 请求参数处理

在这里插入图片描述

案例

在这里插入图片描述

3.3.1 路径占位符

修改nginx.conf

        location ~ /api/item/(\d+) {
            # 默认相应类型
            default_type application/json;
            # 响应结果由lua/item.lua文件来决定
            content_by_lua_file lua/item.lua;
        }

修改item.lua

-- 获取路径参数
local id=ngx.var[1]
-- 拼接结果并返回
ngx.say('{"id":'..id..',"name":"SALSA AIR","title":"RIMOWA 29寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":19900,"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

nginx -s reload

访问http://localhost/item.html?id=10003,实际转发的时候是http://localhost/api/item/10003

在这里插入图片描述

3.4 查询Tomcat

应该先查缓存再查Tomcat,这里我们演示先查Tomcat

在这里插入图片描述

案例

在这里插入图片描述
在这里插入图片描述

3.4.1 封装http查询的函数

添加location

        location /item {
            proxy_pass http://192.168.204.1:8081;
        }
        location ~ /api/item/(\d+) {
            # 默认相应类型
            default_type application/json;
            # 响应结果由lua/item.lua文件来决定
            content_by_lua_file lua/item.lua;
        }

封装http查询的函数

在这里插入图片描述

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

nginx发送的location实际是被自己捕获了,所以要转发proxy_pass到http://192.168.204.1:8081的Tomcat中,再通过read_http来访问http://192.168.204.1:8081的Tomcat返回的数据;

修改item.lua,引入common.lua

先返回一部分看一下

--导入common函数库
local common = require('common')
local read_http = common.read_http

-- 获取路径参数
local id=ngx.var[1]
-- 拼接结果并返回
--ngx.say('{"id":'..id..',"name":"SALSA AIR","title":"RIMOWA 29寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":19900,"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}')

-- 查询商品信息
local itemJSON=read_http("/item/"..id,nil)
-- 查询库存信息
local stockJSON=read_http("/item/stock/"..id,nil)
--拼接结果并返回
ngx.say(itemJSON)

重新加载nginx

nginx -s reload

成功访问

3.4.2 组装json数据

要把两个JSON拼装在一起,要转换为Lua对象-Table,则需要cjson模块

在这里插入图片描述

-- 导入common函数库
local common = require('common')
local read_http = common.read_http
-- 导入cjson函数库
local cjson=require('cjson')
-- 获取路径参数
local id=ngx.var[1]
-- 拼接结果并返回
--ngx.say('{"id":'..id..',"name":"SALSA AIR","title":"RIMOWA 29寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":19900,"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}')

-- 查询商品信息
local itemJSON=read_http("/item/"..id,nil)
-- 查询库存信息
local stockJSON=read_http("/item/stock/"..id,nil)
-- 要转换为Lua对象-Table
local item=cjson.decode(itemJSON)
local stock=cjson.decode(stockJSON)
-- 组合数据
item.stock=stock.stock
item.sold=stock.sold
-- 转为json结果并返回
ngx.say(cjson.encode(item))

重启nginx,nginx -s reload,成功查到库存

在这里插入图片描述

3.5 负载均衡

在这里插入图片描述

修改openresty中nginx.conf,用hash $request_uri;来做哈希,访问tomcat

 upstream tomcat-cluster {
        hash $request_uri;
        server http://192.168.204.1:8081;
        server http://192.168.204.1:8082;
    }
    server {
        listen       8081;
        server_name  localhost;
        location /item {
            proxy_pass http://tomcat-cluster;
        }

启动8082的tomcat

在这里插入图片描述

然后访问http://localhost/item.html?id=10001,是8082服务的

访问http://localhost/item.html?id=10002,是8081服务的

3.6 添加Redis缓存

在这里插入图片描述
在这里插入图片描述

引入依赖

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

配置地址

  redis:
    host: 192.168.204.129

在config包中添加RedisHandler类,用ObjectMapper来进行序列化

@Component
public class RedisHandler implements InitializingBean {
    @Autowired
    private StringRedisTemplatee redisTemplate;
    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService itemStockService;
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void afterPropertiesSet() throws Exception {
        //初始化缓存
        //1.查询所有的商品
        List<Item> itemList = itemService.list();
        //1.1 将商品信息存入redis
        for (Item item : itemList) {
            //1.2 将商品信息序列化
            String itemJson = MAPPER.writeValueAsString(item);
            //1.3 将商品信息存入redis
            redisTemplate.opsForValue().set("item:id" + item.getId(), itemJson);
        }
        //2.查询所有的库存
        List<ItemStock> itemStockList = itemStockService.list();
        //2.1 将库存信息存入redis
        for (ItemStock itemStock : itemStockList) {
            //2.2 将库存信息序列化
            String itemStockJson = MAPPER.writeValueAsString(itemStock);
            //2.3 将库存信息存入redis
            redisTemplate.opsForValue().set("item:stock:id" + itemStock.getId(), itemStockJson);
        }
    }
}

重新启动发现,Spring初始化时有两次查询

在这里插入图片描述

查看RedisManager

如果RedisManager乱码则需要配置RedisTemplate的序列化器,在config包下创建RedisConfig类

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        //配置redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

在这里插入图片描述

3.7 OpenResty操作Redis

在这里插入图片描述

先修改common.lua,引入读取redis的方法

-- 导入redis
local redis=require('resty.redis')
-- 初始化Redis
local red = redis:new();
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的方法 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


-- 封装函数,发送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

修改item.lua,封装查询方法

-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis=common.read_redis

-- 封装查询函数
function read_data(key,path,params)
    -- 优先查redis
    local resp = read_redis("127.0.0.1",6379,key)

    -- 判断查询结果
    if not resp then
        ngx.log(ngx.ERR,"redis查询失败查询tomcat,key:",key)
        -- redis查询失败查询tomcat
        resp=read_http(path,params)
    else
        ngx.log(ngx.ERR, "redis查询成功", ", key: ", key)
    end
    return resp
end

-- 导入cjson函数库
local cjson=require('cjson')
-- 获取路径参数
local id=ngx.var[1]
-- 拼接结果并返回
--ngx.say('{"id":'..id..',"name":"SALSA AIR","title":"RIMOWA 29寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":19900,"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}')

-- 查询商品信息
local itemJSON=read_data("item:id:"..id,"/item/"..id,nil)
-- 查询库存信息
local stockJSON=read_data("item:stock:id:"..id,"/item/stock/"..id,nil)
-- 要转换为Lua对象-Table
local item=cjson.decode(itemJSON)
local stock=cjson.decode(stockJSON)
-- 组合数据
item.stock=stock.stock
item.sold=stock.sold
-- 转为json结果并返回
ngx.say(cjson.encode(item))

在Spring加载时将数据库数据放入Redis中,即使关闭数据库,nginx也能从redis中拿到数据

3.8 为OpenResty添加Redis本地缓存

在这里插入图片描述

在OpenResty的nginx.conf中加入共享词典(本地缓存)

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;;";  
    # 添加共享词典
    lua_shared_dict item_cache 150m;

就可以在/usr/local/openresty/nginx/lua下item.lua中导入共享词典

-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis=common.read_redis
-- 导入cjson函数库
local cjson=require('cjson')
-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache

案例

在这里插入图片描述

3.8.1 修改item.lua逻辑

expire为有效期

-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis=common.read_redis
-- 导入cjson函数库
local cjson=require('cjson')
-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache


-- 封装查询函数
function read_data(key,path,params,expire)
    -- 1.先查询openresty的本地缓存
    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查询失败查询tomcat,key:",key)
            -- 3.redis查询失败查询http
            val=read_http(path,params)
        end
    end
    -- 4.查询成功 把数据写入本地缓存
    item_cache:set(key,val,expire)
    -- 5.返回数据
    return val
end


-- 获取路径参数
local id=ngx.var[1]
-- 拼接结果并返回
--ngx.say('{"id":'..id..',"name":"SALSA AIR","title":"RIMOWA 29寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":19900,"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}')

-- 查询商品信息
local itemJSON=read_data("item:id:"..id,"/item/"..id,nil,1800)
-- 查询库存信息
local stockJSON=read_data("item:stock:id:"..id,"/item/stock/"..id,nil,60)
-- 要转换为Lua对象-Table
local item=cjson.decode(itemJSON)
local stock=cjson.decode(stockJSON)
-- 组合数据
item.stock=stock.stock
item.sold=stock.sold
-- 转为json结果并返回
ngx.say(cjson.encode(item))

重新加载nginx,nginx -s reload

查看日志

tail -f error.log

完成本地缓存

4 缓存同步

4.1 缓存同步策略

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.2 Canal

在这里插入图片描述
在这里插入图片描述

4.2.1 安装和配置Canal

下面我们就开启mysql的主从同步机制,让Canal来模拟salve

4.2.1.1 开启MySQL主从

Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。

这里以之前用Docker运行的mysql为例:

1)开启binlog

打开mysql容器挂载的日志文件,我的在/tmp/mysql/conf目录:

在这里插入图片描述

修改文件:

vi /tmp/mysql/conf/my.cnf

添加内容:

log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima

配置解读:

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

最终效果:

[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
2)设置用户权限

接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对heima这个库的操作权限。

create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
FLUSH PRIVILEGES;

重启mysql容器即可

docker restart mysql

测试设置是否成功:在mysql控制台,或者Navicat中,输入命令:

show master status;

在这里插入图片描述

4.2.1.2 安装Canal
1)创建网络

我们需要创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:

docker network create heima

让mysql加入这个网络:

docker network connect heima mysql
2)安装Canal

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

在这里插入图片描述

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

docker load -i canal.tar

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

docker run -p 11111:11111 --name canal \
-e canal.destinations=heima \
-e canal.instance.master.address=mysql:3306  \
-e canal.instance.dbUsername=canal  \
-e canal.instance.dbPassword=canal  \
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false  \
-e canal.instance.filter.regex=heima\\..* \
--network heima \
-d canal/canal-server:v1.1.5

说明:

  • -p 11111:11111:这是canal的默认监听端口
  • -e canal.instance.master.address=mysql:3306:数据库地址和端口,如果不知道mysql容器地址,可以通过docker inspect 容器id来查看
  • -e canal.instance.dbUsername=canal:数据库用户名
  • -e canal.instance.dbPassword=canal :数据库密码
  • -e canal.instance.filter.regex=:要监听的表名称

表名称监听支持的语法:

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

进入容器内部

docker exec -it canal bash

在这里插入图片描述

查看canal日志

tail -f canal-server/logs/canal/canal.log
2024-03-14 14:11:52.993 [main] INFO  com.alibaba.otter.canal.deployer.CanalLauncher - ## set default uncaught exception handler
2024-03-14 14:11:53.040 [main] INFO  com.alibaba.otter.canal.deployer.CanalLauncher - ## load canal configurations
2024-03-14 14:11:53.053 [main] INFO  com.alibaba.otter.canal.deployer.CanalStarter - ## start the canal server.
2024-03-14 14:11:53.111 [main] INFO  com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[172.22.0.3(172.22.0.3):11111]
2024-03-14 14:11:54.773 [main] INFO  com.alibaba.otter.canal.deployer.CanalStarter - ## the canal server is running now ......

再查看heima的log

tail -f canal-server/logs/heima/heima.log 

在这里插入图片描述

退出docker容器

4.3 使用Canal

在这里插入图片描述
在这里插入图片描述

1)引入依赖

        <dependency>
            <groupId>top.javatool</groupId>
            <artifactId>canal-spring-boot-starter</artifactId>
            <version>1.2.1-RELEASE</version>
        </dependency>

2)配置canal

canal:
  destination: heima
  server: 192.168.204.129:11111

3)编写Canal客户端

在这里插入图片描述

进行映射

在这里插入图片描述

@Data
@TableName("tb_item")
public class Item {
    @TableId(type = IdType.AUTO)
    @Id
    private Long id;//商品id
    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;
}

编写RedisHandler的逻辑

    public void saveItem(Item item) {
        try {
            String itemJson = MAPPER.writeValueAsString(item);
            redisTemplate.opsForValue().set("item:id:" + item.getId(), itemJson);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
    public void deleteItemById(Long id) {
        redisTemplate.delete("item:id:" + id);
    }

编写itemHandler

@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) {
        //1.将新增的商品信息存入本地JVM缓存
        itemCache.put(item.getId(),item);
        //2.将新增的商品信息存入redis
        redisHandler.saveItem(item);
    }

    @Override
    public void update(Item before, Item after) {
        //1.将新增的商品信息存入本地JVM缓存
        itemCache.put(after.getId(),after);
        //2.将新增的商品信息存入redis
        redisHandler.saveItem(after);
    }

    @Override
    public void delete(Item item) {
        //1.删除本地JVM缓存
        itemCache.invalidate(item.getId());
        //2.删除redis
        redisHandler.deleteItemById(item.getId());
    }
}

重启会修改数据库的同时同步修改jvm和redis的缓存

  • 26
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AAA码农宝哥.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值