文章目录
- 安装OpenResty
- OpenResty快速入门
- OpenResty获取请求参数
- 封装Http请求
- 向Tomcat发送http请求
- 根据商品id对tomcat集群负载均衡
- Redis缓存预热
- 查询Redis缓存
- Nginx本地缓存
安装OpenResty
OpenResty快速入门
nginx是没有业务能力的,我们是把请求转发到openResty,然后在openResty中部署集群。
那怎样在openResty中接收并处理这样的请求呢?
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文件,但我们这里先不搞那么复杂,先返回一个假数据
假数据直接从页面中拷贝一个,为了以示区别,把其中的数据稍微改一下,我把行李箱尺寸由21改成了26
重启后,重新访问我们的商品详情页
可以看出,我们成功的把前端请求通过nginx, 然后nginx转发到了后台的openresty上,然后由lua脚本返回了我们设置的参数。
OpenResty获取请求参数
这样就可以拿到路径参数,这个参数会存到变量里,我们就可以去到了。
回来测试
返回的id确实随着id的改变而改变了
如果在openresty中执行nginx -s reload
没变化,又可能是重新加载不行,我们直接关掉openresty,
sh
nginx -s stop
在启动
sh
nginx
但是启动的时候报了个错误
提示:nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [error] open() "/usr/local/nginx/logs/nginx.pid" failed (2: No such file or directory)
解决办法,直接杀掉所有的nginx进程
killall -9 nginx 杀掉nginx 进程 然后重启就行了
在执行启动
sh
nginx
封装Http请求
我们现在的请求可以打到Openresty中了,但现在我们如何把Openresty的请求打进tomcat中去呢?我们的Openresty在虚拟机中,tomcat也就是我们的springBoot项目在windows环境下,这得在从虚拟机打出来。
这里有个技巧就是,只要你的windows机器的ip地址和你的虚拟即的网段设置成一样的,最后的主机号设置成1,就可以把请求从虚拟机打到windows主机上。(注意:防火墙得关闭)
nginx反向代理把请求发到tomcat中
先给openresty的nginx配置反向代理(注意结合自己的网段)
因为发请求的API经常要使用,我们可以给他封装成函数。
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
因为我们在conf文件里配置了
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
所以这个lualib包下面的所有.lua文件都会被加载到
向Tomcat发送http请求
我们这里修改我们的lua脚本,调用这个API,把请求发给tomcat,先简单返回一个商品的数据,拼接商品和库存的动作以后在做
lua
-- 导入common函数库
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)
在浏览器测试一下,发现果然可以返回商品数据了
那该怎么样完成数据的拼接呢?
我们得把数据转换为table,只有转换成了table才能完成数据的拼接这些操作
cjson地址:https://github.com/openresty/lua-cjson
可以看出在lualib中已经有了cjson, 那就可以直接导入使用了
我们在item.lua脚本中完成组合数据并返回
lua
-- 导入common函数库
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))
回浏览器再次测试
发现返回的数据已经有销量和库存了
根据商品id对tomcat集群负载均衡
我们刚刚测试的tomcat是单机的,实际中tomcat一定是个集群。
问题描述:
因为我们实际的tomcat一定是集群的,那我们的nginx会代理到tomcat的某一台服务器上(8081),在tomcat这一台服务器查询后返回结果,会形成一个JVM进程缓存,但是我们的nginx的默认负载均衡策略是轮询,下一次查询同一个商品id,nginx就把请求发到另一个tomcat服务器上(8082),那上次tomcat形成的jvm进程(8081)缓存无法命中,因为jvm进程缓存是不能共享的,所以,还得让8082服务器再次处理,那每次都这样,必然降低我们的命中率,降低性能。
所以,我们要修改nginx的负载均衡策略,把轮询的负载均衡策略修改为hash,这样每次根据商品id计算hash值,只要hash值一致,就能保证每次都请求同一台tomcat服务器,从而提高命中率。
修改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;;";
upstream tomcat-cluster {
hash $request_uri;
server 192.168.10.1:8081;
server 192.168.10.1:8082;
}
server {
listen 8081;
server_name localhost;
location /item {
proxy_pass http://tomcat-cluster;
}
location ~ /api/item/(\d+) {
# 响应类型,这里返回json
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服务器
去浏览器访问测试,不管访问任何商品,只要访问了一次,都会生成进程缓存,而且确保了进程缓存永远生效,不管访问多少次,都可以直接在缓存中得到数据。
Redis缓存预热
按照我们最初的多级缓存的设想,我们在查询的时候不要直接打到tomcat上,而是应该先打到redis查询,如果redis查询失败在查询tomcat。我们应该加入redis的缓存进行预热。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
我这里是因为虚拟机里的docker有很多redis,这个开启的redis配置的映射的是6399端口,所以根据自己情况修改
然后我们来编写初始化类
java
@Component
public class RedisHandler implements InitializingBean {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private IItemService itemService;
@Autowired
private IItemStockService stockService;
// spring中的默认json处理工具
private static final ObjectMapper MAPPER = new ObjectMapper();
// afterPropertiesSet()会在bean创建完,Autowired注入以后执行,实现缓存预热效果
@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
stringRedisTemplate.opsForValue().set("item:id:" + item.getId(), json);
}
// 1. 查询库存信息,
List<ItemStock> stockList = stockService.list();
// 2. 放入缓存
for (ItemStock stock : stockList) {
// 2.1 item序列化为JSON
String json = MAPPER.writeValueAsString(stock);
// 2.2 存入redis
stringRedisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
}
}
}
启动项目,就可以在redis中完成预热。
查询Redis缓存
OpenResty中操作redis函数在这里
我们把这个代码封装一下,放到我们的common.lua中去。
修改我们的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 查询失败, 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
lua
-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入cjson库
local cjson = require('cjson')
-- 封装查询函数
function read_data(key, path, params)
-- 查询redis
local resp = read_redis("127.0.0.1", 6399, key)
-- 判断查询结果
if not resp then
ngx.log("redis查询失败, 产生查询http, key: ", key)
-- redis 查询失败,去查询http
resp = read_http(path, params)
end
return resp
end
-- 获取路径参数
local id = ngx.var[1]
-- 查询商品信息
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))
这就是使用lua来先查询redis, 当redis查询失效后在查询我们的tomcat服务器。
Nginx本地缓存
现在我们的多级缓存就差OpenResty的本地缓存了。
怎么实现呢?
我们在nginx.conf中配置共享词典
在item.lua的业务逻辑中导入我们的共享词典,并且把我们的查询逻辑进行修改,在查询redis之前,先查询本地缓存。
我们的业务逻辑item.lua要修改成这个样子了
java
-- 导入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, expire, path, params)
-- 查询本地缓存
local val = item_cache:get(key)
if not val then
ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询redis, key: ", key)
-- 查询redis
val = read_redis("127.0.0.1", 6399, key)
-- 判断查询结果
if not val then
ngx.log(ngx.ERR, "redis查询失败, 产生查询http, key: ", key)
-- redis 查询失败,去查询http
val = read_http(path, params)
end
-- 查询成功, 把数据写入本地缓存
item_cache:set(key, val, expire)
end
-- 返回数据
return val
end
-- 获取路径参数
local id = ngx.var[1]
-- 查询商品信息
local itemJSON = read_data("item:id:" .. id, 1800 ,"/item/" .. id, nil)
-- 查询库存信息
local stockJSON = read_data("item:stock:id:" .. id, 60 ,"/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))
这样我们在去浏览器测试一次,这样就完整实现了我们的多级缓存。
so cool!!!