当前位置: 首页 > news >正文

从零开始学习Redis(五):多级缓存

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

1· 请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈

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

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

用作缓存的Nginx是业务Nginx,需要部署为集群,再有专门的Nginx来做反向代理。

请求大多在Nginx处理,在Nginx查redis,避免对Tomcat性能造成影响,压力给到了Nginx

当redis缓存失效时,数据不会直接打到数据库,而是先到Tomcat读本地进程缓存

我们要在Tomcat编写进程缓存(JVM进程缓存),在Nginx内部编程,需要学习Lua语言,之后就可以实现多级缓存缓存同步策略了。

JVM进程缓存

缓存由于存储在内存中,数据读取速度非常快,能大量减少对数据库的访问压力。

缓存分两类:

  • 分布式缓存,例如Redis:
    • 优点:Redis是独立在Tomcat之外的,可以在集群间共享;Redis自己也有集群模式,存储容量更大;Redis的主从模式和哨兵机制保证了更好的可靠性
    • 缺点:因为Redis是独立在Tomcat之外的,Tomcat访问Redis有网络开销,性能受制于网络
    • 场景:缓存数据量大,可靠性要求高,集群共享
  • 进程本地缓存,如HashMap,GuavaCache:
    • 优点:读取本地内存,无网络开销,速度更快
    • 缺点:存储容量有限,可靠性低,无法在Tomcat共享
    • 场景:性能要求高,缓存数据量小

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

Caffeine基本使用:

Caffeine的缓存过期(驱逐)策略

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

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

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

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

配置商品和库存的本地进程缓存,给查询商品和库存业务添加缓存,缓存未命中时查数据库

Lua常用语法

以前我们用Tomcat+Java做业务开发,现在我们在Nginx的业务集群实现本地缓存,需要使用Nginx+Lua做业务开发。

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

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

运行脚本 lua xx.lua

利用type函数测试给定变量或值的类型:print(type("hello world"))  ,结果是string

Lua声明变量时不需要指定数据类型

--声明字符串
local str='hello'
-- 拼接字符串用..
local str1='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)

数组,table都可以利用for循环遍历

--遍历数组
local arr={'java','python','lua'}
for index,value in ipairs(arr) doprint(index,value)
end--遍历table
local map={name='Jack',age=21}
for key,value in pairs(map) doprint(key,value)
end

函数

--定义函数
function 函数名(argument1,argument2...,argumentn)--函数体return 返回值
end--例如,定义一个用来打印数组的函数
local function printArr(arr)for index,value in ipairs(arr) doprint(value)end
end

条件控制,类似java的条件控制,如if,else

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

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

and相当于&&,or相当于||,not相当于!

--自定义一个函数,可以打印table,当参数为nil时,打印错误信息
local function  printArr(arr)if(not arr) thenprint('数组不能为空')return nilendfor index,value in ipairs(arr) doprint(value)end
end

多级缓存

我们在Nginx里完成查询Redis,查询Tomcat等业务是依赖于OpenResty完成的

部署OpenResty

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

· 具备Nginx的完整功能

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

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

官方网站:https://openresty.org/cn/

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

参数格式参数示例参数解析代码示例
路径占位符/item/1001# 1.正则表达式匹配:
location ~ /item/(\d+) {
content_by_lua_file lua/item.lua;
-- 2. 匹配到的参数会存入ngx.var数组中,
-- 可以用角标获取
local id = ngx.var[1]
请求头id: 1001-- 获取请求头,返回值是table类型
local headers = ngx.req.get_headers()
Get请求参数?id=1001-- 获取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()

查询Tomcat

部署完Nginx后,我们不先查Redis缓存,因为服务刚启动时Redis缓存里没有缓存,还是要去Tomcat里查,因此我们先查询Tomcat,而查询Tomcat缓存需要发送Http请求

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

local resp = nginx.location.capture("/path",{ method=ngx.HTTP_GET, --请求方式args={a=1,b=2}, --get方式传参数body="c=3&d=4"  --post方式传参数
})--path是路径,不包含IP和端口,这个请求会被nginx内部的server监听并处理,但是我们是要把这个请求发到Tomcat服务器,所以还要编写一个server来对此路径做反向代理location /path{--这里是windows电脑的ip和java服务端口,要确保windows防火墙处于关闭状态proxy_pass http://192.168.100.1:8081; --tomcat的端口
}

返回的响应内容包括:

  • resp.status:响应状态码
  • resp.header:响应头,是一个table
  • resp.body:响应体,就是响应数据

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

1 在/usr/local/openresty/lualib目录下创建commin.lua文件

vi /usr/local/openresty/lualib/common.lua

2 在common.lua中封装http查询的函数

-- 封装函数,发送http请求,并解析响应
local function read_http(path, params)local resp = ngx.location.capture(path,{method = ngx.HTTP_GET,args = params,})if not resp then-- 记录错误信息,返回404ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)ngx.exit(404)endreturn resp.body
end
-- 将方法导出
local _M = {  read_http = read_http
}  
return _M

返回数据我们要将JSON对象转为lua的table,类似与JSON转JAVA的对象,OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化

--1 引入cjson模块
local cjson=require "cjson"--2 序列化
local obj={name='jack',age=21
}
local json=cjson.encode(obj)--3 反序列化
local json='{"name":"jack","age",21}'
local obj=cjson.decode(json);
print(obj.name)

刚才我们是利用OpenResty向一个Tomcat发送请求,但在实际情况下,tomcat肯定是个集群,所以OpenResty发请求时要对tomcat集群实现负载均衡

1 tomcat集群配置

我们要保证同一个id的商品永远访问同一台tomcat,因为进程缓存是不共享的,而nginx默认的负载均衡模式轮询无法满足这个需求,所以要修改nginx负载均衡算法。

upstream tomcat-cluster{--对请求路径做hash运算,得到的结果对tomcat数量取模判断访问哪一台服务器,之后访问只要数值是相同的,就能保证相同数据永远访问同一台服务器hash $request_uri; server 192.168.100.1:8081;server 192.168.100.1:8082;
}

2 反向代理配置 将/item路径的请求代理到tomcat集群

location /item{proxy_pass http://tomcat-cluster;
}

OpenResty在实际开发中也不止一台,和Tomcat一样,我们也需要在Nginx反向代理上配置OpenResty的集群,并且修改负载均衡算法。

upstream nginx-cluster{hash $request_uri; server 192.168.150.101:8081;server 192.168.150.101:8082;server 192.168.150.101:8083;
}

Redis缓存预热

之前我们直接查Tomcat是因为redis里没有缓存,根据多级缓存的思想,现在我们要添加redis缓存,Nginx请求优先查询redis,redis缓存未命中再查询tomcat

但redis有一个问题:

冷启动:服务刚刚启动时,Redis里没有缓存,如果所有数据都在第一次查询是添加缓存,会给数据库带来巨大压力,因此我们在数据启动时先做缓存预热

缓存预热:实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将热点数据提前查询保存到Redis中

这里我们给商品和库存添加Redis缓存预热

1 利用docker安装redis

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

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

3 配置redis地址(虚拟机ip地址)

4 配置初始化类

查询Redis缓存

缓存预热完成后,就可以查询redis缓存了,OpenResty提供了操作redis的模块,直接引入就可使用

1 引入Redis模块,初始化Redis对象

--引入redis模块
local redis=require("resty.redis")
--初始化redis对象
local red=redis:new()
--设置redis超时时间
red:set_timeouts(1000,1000,1000)

2  封装函数,用来释放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 thenngx.log(ngx.ERR, "放入redis连接池失败: ", err)end
end

3 封装函数,从redis读数据并返回

-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port, key)-- 获取一个连接local ok, err = red:connect(ip, port)if not ok thenngx.log(ngx.ERR, "连接redis失败 : ", err)return nilend-- 查询redislocal resp, err = red:get(key)-- 查询失败处理if not resp thenngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)end--得到的数据为空处理if resp == ngx.null thenresp = nilngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)endclose_redis(red)return resp
end

Nginx本地缓存

我们已经实现了tomcat的jvm缓存,实现了从openresty到tomcat的负载均衡远程调用,实现了先查redis,redis未命中再查tomcat,现在还需要在OpenResty搭建本地缓存

OpenResty为Nginx提供了shard dict的功能,可以在nginx的多个worker之间共享数据(多个OpenResty之间不共享),实现缓存功能。

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')

流程:优先查本地缓存,未命中查redis缓存,redis未命中查tomcat,查询redis或tomcat成功后,将数据写入本地缓存,并设置有效期

--查询函数
function read_data(key,expire,path,params)--查询本地缓存local val=item_cache:get(key)if not val thenngx.log(ngx.ERR,"本地缓存查询失败,尝试查询Redis,key:",key)--查询Redisval=read_redis("127.0.0.1",6379,key)if not val thenngx.log(ngx.ERR,"Redis缓存查询失败,尝试查询http,key:",key)--redis查询失败,去查httpval=read_http(path,params)endend--查询成功,把数据写入本地缓存item_cache:set(key,val,expire)--返回数据return val
end

缓存同步策略

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

  • 设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新
    • · 优势:简单、方便
    • · 缺点:时效性差,缓存过期之前可能不一致
    • · 场景:更新频率较低,时效性要求低的业务
  • 同步双写:在修改数据库的同时,直接修改缓存
    • · 优势:时效性强,缓存与数据库强一致
    • · 缺点:有代码侵入,耦合度高;
    • · 场景:对一致性、时效性要求较高的缓存数据
  • 异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
    • · 优势:低耦合,可以同时通知多个缓存服务
    • · 缺点:时效性一般,可能存在中间不一致状态
    • · 场景:时效性要求一般,有多个服务需要同步

我们以前使用MQ发送异步通知,MQ虽然可以实现异步通知,但仍需要修改业务发消息,具有一定侵入性。这里我们学习一个新的异步通知方法:基于Canal的异步通知,可以监听mysql的变化,代码侵入性更低

Canal,译意为水道/管道,是一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。

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

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

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

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

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

Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。这里我们会使用GitHub上的第三方开源的canal-starter。

1 引入依赖

2 编写配置

3 编写监听器,监听Canal消息

@CanalTable("tb_item") //指定要监听的表
@Component
public class ItemHandler implements EntryHandler<Item> { //指定表关联的实体类@Autowiredprivate RedisHandler redisHandler;@Autowiredprivate Cache<Long,Item> itemCache;//监听到数据库的增的消息@Overridepublic void insert(Item item) {//写数据到JVM进程缓存itemCache.put(item.getId(), item);//写数据到redisredisHandler.saveItem(item);}//监听到数据库的改的消息@Overridepublic void update(Item before, Item after) {//写数据到JVM进程缓存itemCache.put(after.getId(), after);//写数据到redisredisHandler.saveItem(after);}//监听到数据库的删的消息@Overridepublic void delete(Item item) {//删除JVM进程缓存数据itemCache.invalidate(item.getId());//删除redis数据redisHandler.deleteItemById(item.getId());}
}

通过注解声明数据库表与实体类的映射关系

多级缓存的总体流程

我们将一个item.html页面放在Windows的nginx上,作为静态资源服务器和反向代理服务器,当用户访问浏览器,nginx将页面返回给用户,

用户发送请求查询数据,Nginx将请求反向代理给OpenResty集群,OpenResty集群优先从本地缓存查询,因为OpenResty集群之间不共享数据,所有我们要修改负载均衡算法,不再是轮询,而是根据请求的url,url里包含请求数据的id,url不变,id不变,数据不变,保证同一数据访问到同一OpenResty,

OpenResty未命中查询Redis

Redis未命中访问Tomcat集群的进程缓存,而进程缓存也不共享数据,因此我们在这里也要修改对Tomcat的负载均衡算法,

进程缓存未命中再去查数据库

这样就形成了  本地缓存——Redis缓存——Tomcat进程缓存  的多级缓存架构。

此外,当数据库数据发生修改,本地缓存,Redis缓存和进程缓存都要做数据同步

在OpenResty本地缓存里我们设置超时同步,到期自动删除,适合存数据更新频率较低的数据;

对于Redis和进程缓存就可以放任意类型的数据了,要保证它们的时效性,我们使用Canal监听数据库,数据库一旦发生修改,Canal发送通知给Java客户端,Java客户端立即去修改Redis缓存和进程缓存。

http://www.dtcms.com/a/560929.html

相关文章:

  • 解码LVGL样式
  • 山西响应式网站建设价位企业培训计划
  • 深入浅出 C++ 多态:从概念到原理
  • 多实现类(如IService有ServiceA/ServiceB)的注入配置与获取
  • web自动化测试-Selenium04_iframe切换、窗口切换
  • 分类与回归算法(一)- 模型评价指标
  • 浙江十大建筑公司排名用v9做网站优化
  • 江门网站建设自助建站站内seo和站外seo区别
  • 嵌入式Linux:线程同步(自旋锁)
  • RHCE复习第一次作业
  • 2025年山西省职业院校技能大赛应用软件系统开发赛项竞赛样题
  • 铁路机车乘务员心理健康状况的研究进展
  • 人才市场官方网站装修公司网站平台
  • Flink 2.1 SQL:解锁实时数据与AI集成,实现可扩展流处理
  • 【软件安全】什么是AFL(American Fuzzy Lop)基于覆盖率引导的模糊测试工具?
  • 山西省最新干部调整佛山网站建设优化
  • 背包DP合集
  • Docker 拉取镜像:SSL 拦截与国内镜像源失效问题解决
  • full join优化改写经验
  • 软件测试:黑盒测试用例篇
  • 【Linux】Linux第一个小程序 - 进度条
  • ubuntu新增用户
  • 青州市网站建设长沙招聘网58同城招聘发布
  • 江苏中南建设集团网站是多少长沙互联网网站建设
  • 从零开始的云原生之旅(十一):压测实战:验证弹性伸缩效果
  • 民宿网站的建设wordpress gallery
  • 【开题答辩全过程】以 广州网红点打卡介绍网站为例,包含答辩的问题和答案
  • Taro 源码浅析
  • Chart.js 混合图:深度解析与应用技巧
  • redis 大key、热key优化技巧|空间存储优化|调优技巧(一)