目录
为什么要用多级缓存?
JVM进行缓存
进程缓存:
实现商品的查询的本地进程缓存
Lua语法
Lua中的数据类型
lua语法+函数
lua条件控制
案例:自定义函数,打印table,条件参数为nil打印提示信息
多级缓存实现
实践测试
请求参数处理
案例返回商品数据:
我们的nginx怎么去请求到tomcat中呢?
我们的请求处理(service)需要编写一个lua脚本在openresty下的lualib中
common.lua获取nginx.conf的http请求作出处理响应
Tomcat集群的负载均衡
添加redis缓存的需求
查询Redis缓存
在openresty中添加一个本地缓存
案例:优先openresty本地缓存
为什么要用多级缓存?
首先我们来说说传统缓存的问题
1.用户请求达到tomcat后,先查询redis再到数据库,但是tomcat的连接是
2.Redis缓存失效后,会直接对数据库进行冲击(也就是缓存击穿现象:没有并且对着没有的地方进行冲击)
nginx跟tomcat差不多,除了做反向代理之外还能自己编写业务->这里作为缓存;
1.我们的浏览器作为客户端的缓存,比如静态资源,当访问相同的静态资源时做出响应;
2.nginx:缓存我们的动态数据,在请求还没有到底tomcat时候,如果nginx中缓存了该数据就响应,如果没查到就去redis查,之前是在tomcat后查,现在是在nginx后查;
3.进程缓存:在redis缓存没有被命中的时候->到达tomcat,在服务器内部利用类似map保存数据,如果命中就返回;
好处:
1.这样层层缓存到达tomcat的请求就会减少,而Tomcat的性能就不会成为系统的瓶颈,我们之前的层层剥削也就是为了解决因为tomcat而导致并发量较低的情况——>所以说,我们在一定程度上还提高了并发量
2.减少对于数据库的冲击,在一定程度上防止了缓冲穿透
细节:
需要实现JVM进程缓存与Lua语法+缓存同步策略
JVM进行缓存
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.然后我们导入工程访问数据
我们的目的是利用nginx反向代理向后面的本地缓存中查询数据然后渲染到客户端保存的页面中
2.然后我们执行nginx,注意配置文件nginx,访问数据,发现nginx代理请求到本地
start nginx.exe
访问localhost/item.html?id=10001
然后我们看看后台,发现前台请求了一个ajax请求,并且这是一个nginx代理请求,发送到我们的nginx缓存中
看看nginx的配置文件,帮助我们的请求接口做了一个负载均衡(nginx-cluster)
如果我们这个本地缓存nginx有多个,那么我们上面负载均衡就配置多个端口
进程缓存:
像我们这种进程缓存,他只是针对本地这台机子上的JVM缓存,不同机子是访问不了的,而且数据量不能太大,不然项目启动会出问题
1.用Caffenine
Caffenine的API
@Test
void test01(){
//1创建缓存对象
Cache cache = Caffeine.newBuilder().build();
cache.put("Curry","30");
cache.put("FOX","5");
//2取数据,如果不存在则返回null
String fox = cache.getIfPresent("FOX");
System.out.println("name="+fox);
String deFaultName = cache.get("deFaultName", key -> {
return "你最喜欢的是库里";
});
System.out.println("defaultName="+deFaultName);
}
设置缓存策略
/*
基于大小设置驱逐策略:
*/
@Test
void testEvictByNum() throws InterruptedException {
// 创建缓存对象
Cache 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"));
}
因为速度比较快,还没来的及驱逐完毕
基于时间的驱逐策略
/*
基于时间设置驱逐策略:
*/
@Test
void testEvictByTime() throws InterruptedException {
// 创建缓存对象
Cache 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"));
}
我们也可以将我们的缓存自定义,然后对外暴露给其他人使用
实现商品的查询的本地进程缓存
1.先配置一个缓存配置类
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;
/**
* @author diao 2022/6/16
*/
@Configuration
public class CaffeineConfig {
/**
* 定义缓存
* @return
*/
@Bean
public CacheitemCache(){
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10_000)
.build();
}
@Bean
public CachestockCache(){
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10_000)
.build();
}
}
2.控制层对于本地缓存的实现
@GetMapping("/{id}")
public Item findById(@PathVariable("id") Long id) {
itemCache.get(id, key -> {
itemService.query().
ne("status", 3).eq("id", key)
.one();
return itemService.query().ne("status",3)
.eq("id",id)
.one();
});
return itemService.query()
.ne("status", 3).eq("id", id)
.one();
}
@GetMapping("/stock/{id}")
public ItemStock findStockById(@PathVariable("id") Long id) {
//先根据id来查本地缓存,如果没有命中->再根据数据库来查
return stockCache.get(id,key->
stockService.getById(key)
);
}
第一次会走数据库,以后都是走本地缓存
Lua语法
测试:
直接输入lua进入lua的控制台
lua声明变量,定义数组+map集合并且打印里面的值(注意lua中数组下标是没有0这个概念的)
我们可以通过解析数组来得到数组中的所有值
数组下标对应着键,value为其值,数组为ipairs,table为pairs
没有大括号直接返回
then代表大括号开始,end代表结束
if()里面默认代表判断是否为nil
多级缓存实现
初识OpenResty
OpenResty® - Official Site
菜鸟
OpenResty 使用介绍 | 菜鸟教程 (runoob.com)
配置完后,openresty文件默认是在/user/local/下
lualib里面都是第三方模块:redis、mysql之类的
我们可以发现openresty是基于Nginx,除了nginx的执行文件其他都有
我们进入openresty的bin目录发现可执行文件就是nginx的可执行文件
利用了一个软引用,所以直接启动nginx下的执行文件也是可以的
所以我们这里还需要配置nginx的环境变量
然后利用source命令在当前环境下读取配置文件
然后nginx启动,先进入openresty中nginx下的配置目录将配置修改
然后启动nginx
实践测试
我们反向代理的最终地址就是openresty集群中的一个地址 (openresty的业务集群)
所以我们需要openresty接收请求
这里我们也明确openresty的用意:就是为了实现本地缓存版本的nginx
1.我们在nginx配置下添加对/api/item监听当访问这个负载均衡接口,响应一个lua/item.lua的文件,响应类型为json数据
添加两个加载模块+路径监听以及配置
然后我们在lua写业务(类似service)
加载openresty的两个模块:扩展nginx的作用,并且location根据接口路径响应lua内容(这些在openresty中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;
}
}
}
lua会去nginx所在目录下找,创建item.lua即可
然后nginx -s reload再次请求成功——>说明我们的数据是由openresty给的
而这个openresty相当于nginx的缓存版本
请求参数处理
我们的openresty返回的数据不能是假数据
1.~就是正则表达式的一个匹配,我们的正则表达式的元素值会被储存到数组中,我们可以利用数组获取里面的元素
2.Get、Post、JSON的参数值可以直接从uri,表单,body中获取
1.先修改openresty下nginx的配置文件,将接口+占位符
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/(\d+) {
# 默认的响应类型
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;
}
}
}
2.修改nginx下lua下的lua文件,然后获取占位符内容
local id=ngx.var[1]
ngx.say('"id":'..id..',"name":"SALSA","title":"RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色
820.70.36.4","price":"999","image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWARIMOWA","spec":"{"颜色": "红色", "尺码": "26寸"}","status":1')
查询Tomcat
我们先去除中间的redis部分,openresty直接访问tomcat
我们的nginx怎么去请求到tomcat中呢?
这里需要用到反向代理,nginx内部提供.location.capture()的api,可以将我们的目标请求进行响应,但是这里只是一个nginx内部的server监听并且监听,那我们要请求到Tomcat服务器,就要编写一个server对这个路径进行反向代理,我们这里代理的也应该就是controller下的请求;
我们响应的内容就是里面这个body
resp.body:响应体,响应数据
我们最后这个proxy_pass就是服务tomcat的ip
我们的请求处理(service)需要编写一个lua脚本在openresty下的lualib中
我们回顾一下之前在openresty中nginx的conf配置中是不是配置了加载lua模块与c模块,意思就是配置目录下的.lua都会被加载
所以我们这里下面common.lua一定会被加载
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
细节处理:
之前我们的请求/api/item不是会路由到指定的lua上吗,我们lua返回json数据,之前我们item.lua中是假数据,需要实现
导入common.lua函数库,查询商品+库存信息
完成openresty目录下nginx中item.lua缓存数据获取的配置
一个商品信息一个库存信息,信息从common.lua中获取(里面放入请求的数据和信息)
item.lua的编写,目的就是将缓存数据进行返回->通过过read_http,而这个方法又是common.lua中的,会读取请求路径
我们的common.lua中配置了请求路径,然后反向代理到我们的tomcat服务器,最后返回body数据
local common = require('common')
local read_http = common.read_http
local id = ngx.var[1]
local itemJSON = read_http("/item/" .. id,nil)
local stockJSON = read_http("/item/stock/" .. id,nil)
ngx.say(itemJSON)
JSON结果处理
当有多台tomcat时,我们本地缓存openresty,也就是nginx对其进行访问,可能会出现在一台服务器上有缓存,而在另外一台服务器上没缓存的情况
两种思路:1.我认为是可以搭建一个redis的,我们的tomcat请求的数据存入redis中,然后openresty封装的本地缓存从redis中取,相当于说redis就是一个大杂烩,连接着本地缓存与进程缓存;
2.我们可以利用一个hash算法,也就是说,根据我们的请求路径,进行取模运算得到请求到哪一个服务器上,不会请求到其他服务器,这样就保证了缓存获取的一个作用,可以一直命中
openresty下nginx配置:
#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;;";
#搭建一个tomcat集群
upstream tomcat-cluster{
hash $request_uri;
server 192.168.184.1:8081;
server 192.168.184.1:8082;
}
server {
listen 8081;
server_name localhost;
location /item{
proxy_pass http://tomcat-cluster;
}
location ~/api/item/(\d+) {
# 默认的响应类型
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;
}
}
}
配置好tomcat集群后重启openresty下nginx,进行访问,发现缓存生效
添加redis缓存的需求
缓存预热主要防止缓存击穿,过期热点数据访问
1.启动redis容器
docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes
2.实现项目一启动就进行缓存初始化
每次初始化都会开启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.IItemService;
import com.heima.item.service.IItemStockService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author diao 2022/6/17
*/
@Component
public class RedisHandler implements InitializingBean {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private IItemService itemService;
@Autowired
private IItemStockService stockService;
private static final ObjectMapper MAPPER=new ObjectMapper();
@Override
public void afterPropertiesSet() throws Exception {
/**
* 1.查询商品详情信息
*/
List- itemList = itemService.list();
//放入缓存
for (Item item : itemList) {
String json = MAPPER.writeValueAsString(item);
redisTemplate.opsForValue().set("item:id:"+item.getId(),json);
}
/**
* 2.查询商品库存信息
*/
List
stockList = stockService.list();
//放入缓存中
for (ItemStock stock : stockList) {
String json = MAPPER.writeValueAsString(stock);
redisTemplate.opsForValue().set("item:stock:id:"+stock.getId(),json);
}
}
}
3.启动docker exec ...打开redis客户端
docker exec -it redis redis-cli
然后redis-cli客户端即可操作
发现数据全部都在缓存中
查询Redis缓存
目的:openresty优先查询Redis缓存中数据再访问tomcat
openresty中的Redis模块:
我们需要再common.lua中引入redis模块
1.引入Redis模块,创建Redis对象;
2.封装函数用于释放Redis连接,将Redis放入连接池
common.lua总配置
还需要暴露函数,供给itema.lua获取
操作模块,从redis读取数据返回——>itema.lua
然后我们去/lua/itema.lua中修改配置,将读取数据的方式修改为优先redis读取后tomcat本地缓存读取
-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入cjson函数库
local cjjson = require('cjson')
-- 封装查询函数
function read_data(key,path,params)
local resp = read_redis("127.0.0.1",6379,key)
if not resp then
ngx.log("redis查询失败,尝试查询http,key:",key)
resp = read_http(path,params)
end
return resp
end
local id = ngx.var[1]
local itemJSON = read_http("item:id:" .. id,"/item/" .. id,nil)
local stockJSON = read_http("item:stock:id:" .. id,"/item/stock/" .. id,nil)
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
item.stock = stock.stock
item.sold = stock.sold
ngx.say(cjson.encode(item))
在openresty中添加一个本地缓存
在itema.lua中导入本地缓存
然后我们的查询函数需要更改:先本地缓存查询再redis再Tomcat
案例:优先openresty本地缓存
common.lua配置
local redis = require('resty.redis')
local red = redis:new()
red:set_timeouts(1000,1000,1000)
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
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
local function read_http(path, params)
local resp = ngx.location.capture(path,{
method = ngx.HTTP_GET,
args = params,
})
if not resp then
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
itema.lua配置
-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入cjson函数库
local cjjson = require('cjson')
-- 导入本地缓存共享词库
local item_cache = ngx.shared.item_cache
-- 封装查询函数
function read_data(key,expire,path,params)
local val = item_cahce:get(key)
if not val then
ngx.log(ngx.ERR,"本地缓存查询失败,尝试redis,key:",key)
val = read_redis("127.0.0.1",6379,key)
if not val then
ngx.log("redis查询失败,尝试查询http,key:",key)
val = read_http(path,params)
end
return val
end
local id = ngx.var[1]
-- 查询商品信息
local itemJSON = read_http("item:id:" .. id,1800,"/item/" .. id,nil)
local stockJSON = read_http("item:stock:id:" .. id,60,"/item/stock/" .. id,nil)
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
item.stock = stock.stock
item.sold = stock.sold
ngx.say(cjson.encode(item))
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;;";
#添加共享词典,本地缓存
lua_shared_dict item_cache 150m;
#搭建一个tomcat集群
upstream tomcat-cluster{
hash $request_uri;
server 192.168.184.1:8081;
server 192.168.184.1:8082;
}
server {
listen 8081;
server_name localhost;
location /item{
proxy_pass http://tomcat-cluster;
}
location ~/api/item/(\d+) {
# 默认的响应类型
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;
}
}
}
开日志进行查看