Bootstrap

第四章 Redis多级缓存案例

1. 本地进程缓存

1.1 简述分布式缓存与本地缓存的优缺点各是什么?

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

1.2 Caffeine的缓存驱逐策略有几种,分别是什么?

Caffeine提供了三种缓存驱逐策略:
1.基于容量:设置缓存的数量上限
2.基于时间:设置缓存的有效时间
3.基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

2. Lua语言

2.1 字符串拼接

> local str='hello' .. ' world' .. '!' print(str)
hello world!

2.2 for语句

local arr={"java","python","lua"}
local map={name="tom",age="22",addr="JiNan"}

--遍历数组,ipairs表示递增成对的遍历,i:Incremental,递增,pairs:一对
for index,value in ipairs(arr) do
        print(index,value)
end

--遍历集合
for key,value in pairs(map) do
        print(key,value)
end

2.3 函数

--定义函数实现数组的遍历
local function printArr(arr)
	--判断数组是否为空
	if(not arr) then --nil也表示false,此时数组为空
		print('数组不能为空')
		return nil;
	end
	--正常情况
	for index,value in ipairs(arr) do
		print(index,value)
	end
end


printArr(arr)

2.4 其他细节

1.数组的下标是从1开始的

2.变量范围

#local 修饰的变量是局部变量
#直接定义的变量是全局变量
> arr={10,'ABC',true,nil}
> print(arr)
table: 0x21ffd90
> print(arr[1])
10
> print(arr[2])
ABC
> print(arr[4])
nil

3. OpenResty

3.1 OpenResty是什么?有哪些特点?

概念:
OpenResty是一个基于Nginx的高性能Web平台,它结合了Nginx和Lua脚本语言的功能。OpenResty允许开发人员使用Lua脚本在Nginx服务器上编写自定义的动态网页应用程序,而无需将请求转发给后端应用服务器。

OpenResty具有以下特点:
①高性能
②可扩展性
③Lua脚本支持
④内置丰富的模块
⑤高度集成

3.2 使用OpenResty

1.Centos安装OpenResty后默认路径在:

/usr/local/openresty

2.修改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 {
		#默认的响应类型
		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;
        }
    }
}

3.在nginx目录中创建lua/item.lua文件

--向客户端发送响应
ngx.say('{"id":10001,"name":"SALSA AIR","title":"RIMOWA 22寸托运箱拉杆箱 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 OpenResty获取请求参数

在这里插入图片描述

3.4 OpenResty查询Tomcat

3.4.1 编写请求响应的配置文件

路径:/usr/local/openresty/nginx/lua/item.lua

--导入common.lua函数库
local common=require('common')
local read_http=common.read_http
--导入cjson的函数库
local cjson=require("cjson")
--获取路径参数
local id=ngx.var[1]
--查询商品信息
local itemJSON=read_http("/item/" .. id,nil)
--查询库存信息
local stockJSON=read_http("/item/stock/" .. id,nil)
--JSON转化为lua的table
local item=cjson.decode(itemJSON)
local stock=cjson.decode(stockJSON)
--组合数据
item.stock=stock.stock
item.sold=stock.sold
--把item序列化为json,返回结果
ngx.say(cjson.encode(item))

3.4.2 编写nginx的配置文件,实现反向代理

	#反向代理
	location /item {
		proxy_pass http://192.168.137.1:8081;
	}

3.5 访问Tomcat集群

3.5.1 配置nginx的参数

        #指定Tomcat集群
        upstream tomcat-cluster {
		#hash $request_uri;:这一行指定了请求分发的策略是使用请求URI的哈希值。这意味着相同的请求URI会被分发到同一个后端服务器上,有助于保持会话的连续性,特别是对于那些需要会话状态的应用。
                hash $request_uri;
                server 192.168.137.1:8081;
                server 192.168.137.1:8082;
        }
#-----------------------------------------------
        #反向代理
        location /item {
                proxy_pass http://tomcat-cluster;
        }

3.6 开启本地缓存

在nginx的配置文件中添加命令:

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

4. Redis缓存

OpenResty发起请求后,不会直接访问tomcat,而是先访问Redis集群,若Redis集群中没有,再访问Tomcat。

4.1 编写Redis缓存的配置文件

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.impl.ItemService;
import com.heima.item.service.impl.ItemStockService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @ClassName RedisHandler
 * @Description redis缓存配置类
 * @Author 孙克旭
 * @Date 2024/11/22 16:17
 */
@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private ItemService itemService;
    @Autowired
    private ItemStockService stockService;
    /**
     * 序列化工具
     */
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void afterPropertiesSet() throws Exception {
        //初始化缓存,缓存预热
        //1.添加热点数据(目前添加全部的数据)
        List<Item> itemList = itemService.list();
        //2.放入缓存
        for (Item item : itemList) {
            //2.1 item序列化成JSON
            String json = MAPPER.writeValueAsString(item);
            //2.2 存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        }
        //3.添加库存数据
        List<ItemStock> stockList = stockService.list();
        //4.放入缓存
        for (ItemStock stock : stockList) {
            //4.1 stock序列化成JSON
            String json = MAPPER.writeValueAsString(stock);
            //4.2 存入redis
            redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
        }
    }
}

该配置类会在SpringBoot启动后,自动执行类中的参数,实现Redis集群缓存预热。原因是实现了 InitializingBean接口,所以会自动执行。

4.2 实现openResty与Redis的多级缓存

4.2.1 编写通用配置文件

文件路径:/usr/local/openresty/lualib/common.lua

-- 导入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 not found, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 将方法导出
local _M = {  
	read_http = read_http,
	read_redis=read_redis
}  
return _M

4.2.2 编写OpenResty访问的数据源文件

路径:/usr/local/openresty/nginx/lua/item.lua

--导入common.lua函数库
local common=require('common')
local read_http=common.read_http --向Tomcat发送请求的方法
local read_redis=common.read_redis --查询redis的方法
--导入cjson的函数库,实现序列化
local cjson=require("cjson")
--获取前端请求的路径参数
local id=ngx.var[1]
--封装查询函数,先查询redis,若结果为空,再查询tomncat
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查询失败,尝试查询http,key:",key)
		--redis查询失败,查询http
		resp=read_http(path,params)
	end
	return resp
end
--查询商品信息
local itemJSON=read_data("item:id:"..id,"/item/" .. id,nil)
--查询库存信息
local stockJSON=read_data("item:stock:id:"..id,"/item/stock/" .. id,nil)
--JSON转化为lua的table
local item=cjson.decode(itemJSON)
local stock=cjson.decode(stockJSON)
--组合数据
item.stock=stock.stock
item.sold=stock.sold
--把item序列化为json,返回结果
ngx.say(cjson.encode(item))

5. 缓存同步

5.1 缓存同步策略

在这里插入图片描述

5.2 Canal

canal:
译意为水道/管道/沟渠,是基于java开发,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。

原理:
canal是基于mysql的主从同步来实现的,canal把自己伪装成mysql的一个slave节点,从而监听 master的binary log的变化。再把得到的变化消息通知给canal的客户端,进而完成对其它数据库的同步。

5.2.1 安装Canal

1.Canal会伪装成mysql的一个slave,所以先明确mysql的binarylog的位置,修改mysql的my.conf文件:

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这个库

2.将mysql和cancal放入同一网络

创建网络:docker network create heima

让mysql加入网络:docker network connect heima mysql

3.添加cancal到容器:

docker run -p 11111:11111 --name canal \
-e canal.destinations=heima \ #cancal集群名称
-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\\..* \ #监听mysql库的名称
--network heima \
-d canal/canal-server:v1.1.5

5.3 多级缓存架构

在这里插入图片描述

6. 踩坑记录

1.用Docker挂载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=123456 \
 --privileged \
 -d \
 mysql:5.7.25

2.有请求体的请求协议:Post、Put、Patch

3.复制单词:

(1)方式一:在命令模式下,将光标移动到单词上,输入 yiw即可,y 是复制,i 是内部,w 是单词

(2)方式二:进入可视模式,通过光标选择单词,使用 y复制,p粘贴

4.OpenResty设置本地缓存失败

原因是编写的配置文件有错误,原来是

--判断查询结果
		if not val then
			ngx.log("redis查询失败,尝试查询http,key:",key)
			--redis查询失败,查询http
			val=read_http(path,params)
		end

后经老师发现,log方法使用错误,修改后:

--判断查询结果
		if not val then
			ngx.log(ngx.ERR,"redis查询失败,尝试查询http,key:",key)
			--redis查询失败,查询http
			val=read_http(path,params)
		end

我也学会了查看nginx的日志,路径:

/usr/local/openresty/nginx/logs/error.log

5.cancal异常:

报错信息:com.alibaba.otter.canal.protocol.exception.CanalClientException: java.net.ConnectException: Connection refused: connect

重启canal容器时发现指定的端口是11111,而我的配置参数是1111🌝

看来需要更加认真一点。

6.canal添加的数据时间格式问题。

当我在mysql中添加一行数据时,redis也能同步增加数据,但是发现有关时间的字段只有年月日,时分秒全为0.

后来想找一下canal的配置注解,指定时间格式,发现用@JsonFormat就行:

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;//创建时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;//更新时间
;