第四章 Redis多级缓存案例

1. 本地进程缓存

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

分布式缓存,例如Redis:

优点:存储容量更大、可靠性更好、可以在集群间共享

缺点:访问缓存有网络开销

场景:缓存数据量较大、可靠性要求较高、需要在集群间共享

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

优点:读取本地内存,没有网络开销,速度更快

缺点:存储容量有限、可靠性较低、无法共享

场景:性能要求较高,缓存数据量较小

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

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

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

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

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

2. Lua语言

2.1 字符串拼接

bash 复制代码
> local str='hello' .. ' world' .. '!' print(str)
hello world!

2.2 for语句

bash 复制代码
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 函数

bash 复制代码
--定义函数实现数组的遍历
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.变量范围

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

properties 复制代码
#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文件

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

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的配置文件,实现反向代理

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

3.5 访问Tomcat集群

3.5.1 配置nginx的参数

properties 复制代码
        #指定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的配置文件中添加命令:

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

4. Redis缓存

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

4.1 编写Redis缓存的配置文件

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

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

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文件:

mysql 复制代码
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到容器:

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

bash 复制代码
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设置本地缓存失败

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

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

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

lua 复制代码
--判断查询结果
		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就行:

java 复制代码
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;//创建时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;//更新时间
相关推荐
qq_5298353531 分钟前
对计算机中缓存的理解和使用Redis作为缓存
数据库·redis·缓存
月光水岸New3 小时前
Ubuntu 中建的mysql数据库使用Navicat for MySQL连接不上
数据库·mysql·ubuntu
狄加山6753 小时前
数据库基础1
数据库
我爱松子鱼3 小时前
mysql之规则优化器RBO
数据库·mysql
chengooooooo3 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
Rverdoser4 小时前
【SQL】多表查询案例
数据库·sql
Galeoto4 小时前
how to export a table in sqlite, and import into another
数据库·sqlite
希忘auto5 小时前
详解Redis在Centos上的安装
redis·centos
人间打气筒(Ada)5 小时前
MySQL主从架构
服务器·数据库·mysql
leegong231115 小时前
学习PostgreSQL专家认证
数据库·学习·postgresql